Conversions API

POST /api/v1/conversions — record payments, trials, leads, and refunds from your own backend. One required identifier (email), idempotent by design.

The conversions API is for revenue that doesn't come from a built-in integration: your own billing system, a CRM we don't support yet, or events you define yourself. It's one endpoint, and email is the only identifier you need.

curl -X POST https://attributely.co/api/v1/conversions \
  -H "Authorization: Bearer ar_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "ord_8842",
    "event": "subscribe",
    "email": "alice@acme.com",
    "amount_cents": 4900,
    "currency": "usd"
  }'

Your API key (ar_live_...) lives under Integrations → API in the app. Keep it server-side; it's a bearer token.

The full machine-readable contract is public: /api/v1/conversions/openapi.json — an OpenAPI 3.1 document generated from the same schemas that validate requests, so it's always current. Point a code generator or an AI agent at it.

Events

Seven values are accepted: the six forwarded events, plus refund for reversals.

eventUse forForwarded as
purchaseA one-time paymentPurchase
subscribeA subscription's first chargeSubscribe
start_trialA free trial startStartTrial
leadA lead or deal createdLead
scheduleA booked meeting or demoSchedule
complete_registrationA signupCompleteRegistration
refundA reversal of a paymentNot forwarded — recorded so revenue stays net

Revenue events (purchase, subscribe, refund) require a positive amount_cents with a currency; start_trial accepts 0.

Fields

FieldRequiredNotes
external_idyesYour ID for this conversion (order ID, charge ID, deal ID). This is the idempotency key: sending the same external_id again returns the existing conversion instead of creating a duplicate, so retries are always safe.
eventyesOne of the seven events above.
emailyesThe customer's email — the identity key that joins the conversion to their visits and ad click.
amount_centsfor revenue eventsAmount in minor units (cents). Requires currency.
currencywith amount_centsISO 4217. Any real code is accepted; the ~30 currencies on the ECB reference list (USD, EUR, GBP, INR, JPY, and the other majors) convert into your reporting currency, the rest are recorded and shown unconverted.
occurred_atnoISO 8601 timestamp. Defaults to receipt time. Backdating is fine — but a conversion older than an ad platform's acceptance window is recorded without being forwarded.
ar_tokennoThe pixel's visitor token, read live from the current request's cookies — see The visitor token. Email already attributes the sale; this only sharpens the match. Never cache or hardcode one.
match_keysnoName, phone, billing city/state/zip/country, your customer ID. Raises ad-platform match quality. Each value is normalized to platform spec and hashed before forwarding; anything that doesn't validate is dropped, never sent wrong.

Unknown fields are rejected rather than ignored — a typo'd field name fails loudly at the boundary instead of silently doing nothing.

Responses

A new conversion returns 201; replaying an external_id you've already sent returns 200 with the originally recorded conversion. Both carry the same shape:

{
  "data": {
    "id": "…",
    "event_id": "…",
    "external_id": "ord_8842",
    "event": "subscribe",
    "status": "recorded",
    "amount_cents": 4900,
    "currency": "usd",
    "email": "alice@acme.com",
    "occurred_at": "2026-06-11T12:00:00Z",
    "created_at": "2026-06-11T12:00:01Z"
  }
}

On a replay, event echoes what was stored — if the original send said purchase, a replay that says subscribe still returns purchase. The first write wins; that's what idempotent means here.

Errors use one envelope:

{ "error": { "code": "validation_error", "message": "…", "details": [ { "field": "email", "message": "…" } ] } }
StatuscodeMeaningWhat to do
401unauthorizedMissing, invalid, or revoked API key.Fix the key. Don't retry with the same one.
422validation_errorBad JSON or a field failed validation — details lists each failing field.Fix the request. Retrying unchanged will fail identically.
500internal_errorSomething failed on our side, including a key check we couldn't complete.Retry with the same external_id — idempotency makes retries free.

The distinction worth coding against: a 401 is always your credential, never our outage — if our key check itself fails, you get a 500, so an automated integration can safely treat 401 as "stop and alert" and 500 as "retry."

What happens after you POST

The conversion is recorded immediately and resolves to the customer's identity by email. If their history contains an ad click, the conversion routes to that platform under the standard forwarding rules; if not, it's attributed in your dashboard and visibly skipped for forwarding. Either way you can see the result — every conversion shows its per-platform forwarding outcome on the Events page.