Developer Documentation

Install tracking, push revenue, and integrate webhooks. Every example below reflects the live API.

Overview and authentication

The Attrevo API is a JSON HTTP API. All application endpoints are served under https://api.attrevo.com/api.

There are three distinct authentication modes:

ModeUsed byCredential
Session (Clerk JWT)Dashboard requests from the browserAuthorization: Bearer <jwt>
API keyServer-to-server, e.g. the revenue APIX-Attrevo-API-Key: atv_live_…
PublicTracking ingestion, consent, snippetNone — rate limited per domain

A handful of routes sit outside the /api prefix because they are called by browsers or load balancers: /health, /events/track, /events/consent, /snippet/:tenantId/attrevo.js and the payment webhooks.

Was this helpful?

Tracking snippet installation

One script tag, on every page, immediately before the closing </body> tag. Replace YOUR_TENANT_ID with the value from Settings → Tracking.

index.html
<!-- Paste immediately before </body> on every page -->
<script async src="https://api.attrevo.com/snippet/YOUR_TENANT_ID/attrevo.js"></script>

The snippet is deliberately small and self-contained. It:

  • Assigns a pseudonymous visitor ID in a first-party cookie (attrevo_vid, 2 years) and a session ID in sessionStorage.
  • Reads utm_source, utm_medium, utm_campaign, utm_content and utm_term, falling back to the referrer.
  • Sends via navigator.sendBeacon where available, so navigation is never delayed.
  • Holds every event until consent is granted, then stores the decision in attrevo_consent.

Nothing is transmitted before consent. If the visitor declines, no touchpoint is sent at all.

Was this helpful?

Custom event tracking

To track a call-to-action, add data-attrevo="cta" to any element. The snippet listens on document click and walks up the tree, so it works on dynamically rendered markup with no extra wiring.

cta.html
<!-- Any element with data-attrevo="cta" reports a cta_click -->
<a href="/pricing" data-attrevo="cta">See pricing</a>
<button data-attrevo="cta" id="hero-signup">Start free trial</button>

Each click sends a cta_click event carrying the element's trimmed text (up to 120 characters), its id, and its href when present.

Was this helpful?

Revenue API reference

POST /api/revenue/record — record a payment from a platform Attrevo does not integrate with directly. Authenticated with an API key, not a session.

Request

FieldTypeRequiredNotes
sourcestringYesMax 32 chars, e.g. selar
amountnumberYesPositive, in major units (naira, not kobo)
referencestringYesYour unique ID; used to de-duplicate
customerEmailstringConditionalAt least one of email or phone is required
customerPhonestringConditionalNormalised to +234 before matching
currencystringNo3-letter code, defaults to NGN
occurredAtstringNoISO 8601; defaults to now
metadataobjectNoArbitrary key/value pairs, stored as-is
request
curl -X POST https://api.attrevo.com/api/revenue/record \
  -H "X-Attrevo-API-Key: atv_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "selar",
    "amount": 150000,
    "currency": "NGN",
    "customerEmail": "ada@example.com",
    "customerPhone": "+2348012345678",
    "reference": "SELAR-TX-91823",
    "occurredAt": "2025-07-26T09:15:00.000Z",
    "metadata": { "product": "Attribution Masterclass" }
  }'

Response

200 OK
{
  "recorded": true,
  "revenueEvent": {
    "id": "clx8f2k9p0001",
    "source": "selar",
    "amount": 150000,
    "currency": "NGN",
    "reference": "SELAR-TX-91823",
    "matchedLeadId": "clx7a1b2c0003",
    "matchMethod": "email",
    "occurredAt": "2025-07-26T09:15:00.000Z"
  }
}

matchMethod is email, phone or null. A null match still records the revenue — it simply cannot be attributed to a channel yet, and will be re-matched if the lead is identified later.

Was this helpful?

Webhook integration

Webhooks are the preferred path for supported platforms: revenue arrives in near real time with no polling.

PlatformEndpointVerification
Paystack/webhooks/paystackHMAC SHA512 over the raw body
Flutterwave/api/webhooks/flutterwaveShared secret header
Selar/api/webhooks/selarShared secret header
Generic/api/webhooks/genericShared secret + field mapping
paystack charge.success
{
  "event": "charge.success",
  "data": {
    "reference": "PSK-8817263",
    "amount": 5375000,
    "currency": "NGN",
    "customer": { "email": "ada@example.com" },
    "paid_at": "2025-07-26T09:15:00.000Z"
  }
}

Signature verification runs against the raw request body before parsing — re-serialising JSON changes the bytes and breaks the HMAC. Deliveries are idempotent on the platform reference, so retries are safe.

The generic endpoint lets you map your own field names onto Attrevo's when configuring the source, for platforms with no native integration.

Was this helpful?

CSV import

For historical revenue or offline sales, import a CSV under Revenue → Import. Each row is matched using the same email-then-phone logic as the API.

revenue.csv
email,phone,amount,currency,reference,occurred_at
ada@example.com,+2348012345678,150000,NGN,INV-001,2025-07-01
chidi@example.com,,89500,NGN,INV-002,2025-07-03
,+2348098765432,240000,NGN,INV-003,2025-07-05
  • email or phone — at least one per row.
  • amount — major units. Do not include currency symbols or thousands separators.
  • reference — must be unique; duplicates are skipped rather than double-counted.
  • occurred_atYYYY-MM-DD or full ISO 8601.

Each import is recorded as a batch, so a bad file can be identified and reversed rather than leaving orphaned rows.

Was this helpful?

API keys

Create keys under Settings → API keys. Keys are prefixed atv_live_ and are shown exactly once at creation — only a hash is stored, so a lost key must be revoked and replaced.

  • Send as X-Attrevo-API-Key, never in a query string.
  • Each key is scoped to one tenant and carries no user identity.
  • Revoking takes effect immediately on the next request.
  • Keys belong on a server. Anything in browser JavaScript is public — use the snippet there instead.
Was this helpful?

Rate limits

Tracking ingestion is rate limited per domain in Redis to protect the platform from misconfigured or malicious pages. Authenticated endpoints are additionally bounded by your plan's monthly event allowance.

Exceeding a limit returns 429. Two distinct cases share that status, distinguished by the body:

  • Transport rate limit — retry with exponential backoff.
  • Plan limit (error: "PLAN_LIMIT_REACHED") — retrying will not help until the plan is upgraded or the month rolls over.

Batch where you can: one CSV import is far cheaper than a thousand individual calls.

Was this helpful?

Error codes

Errors use standard HTTP status codes with a consistent JSON body.

401
{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Missing X-Attrevo-API-Key header"
}
StatusMeaningWhat to do
400Validation failedRead the message — it names the offending field.
401Missing or invalid credentialCheck the header name and that the key is not revoked.
403Authenticated but not permittedOften no tenant yet — call POST /api/tenants/bootstrap.
404Not found, or wrong method on a valid pathConfirm the verb: a GET on a POST-only route also returns 404.
429Rate or plan limitSee the section above.
500Server errorSafe to retry idempotent calls; contact support if it persists.
Was this helpful?