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.
event | Use for | Forwarded as |
|---|---|---|
purchase | A one-time payment | Purchase |
subscribe | A subscription's first charge | Subscribe |
start_trial | A free trial start | StartTrial |
lead | A lead or deal created | Lead |
schedule | A booked meeting or demo | Schedule |
complete_registration | A signup | CompleteRegistration |
refund | A reversal of a payment | Not forwarded — recorded so revenue stays net |
Revenue events (purchase, subscribe, refund) require a positive amount_cents with a currency; start_trial accepts 0.
Fields
| Field | Required | Notes |
|---|---|---|
external_id | yes | Your 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. |
event | yes | One of the seven events above. |
email | yes | The customer's email — the identity key that joins the conversion to their visits and ad click. |
amount_cents | for revenue events | Amount in minor units (cents). Requires currency. |
currency | with amount_cents | ISO 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_at | no | ISO 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_token | no | The 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_keys | no | Name, 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": "…" } ] } }
| Status | code | Meaning | What to do |
|---|---|---|---|
401 | unauthorized | Missing, invalid, or revoked API key. | Fix the key. Don't retry with the same one. |
422 | validation_error | Bad JSON or a field failed validation — details lists each failing field. | Fix the request. Retrying unchanged will fail identically. |
500 | internal_error | Something 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.