Conversion tracking
Conversion tracking connects the purchases and sign-ups happening in your product to the campaign or automation email that drove them — count, attributed revenue, conversion rate, and revenue per recipient, on the reports you already read. This page explains the model exactly: what a goal is, how attribution decides, where the numbers surface, and what the model deliberately does not claim.
Conversion goals
A goal defines what counts as a conversion for your account. You manage goals under Settings → Analytics; each one has:
- An event binding. Every conversion enters the platform as a trigger-event fire — there is exactly one ingestion pipeline. Bind the goal to any event you already fire: a Stripe event such as
stripe.invoice.paid, a Shopify event such asshopify.order.paid, or one of your own custom events. Or pick Generic API and the goal provisions its own reserved event (conversion.<goal-uid>), fired byPOST /api/v1/conversions— see below. - A revenue path (optional). A dot-path into the fire's payload where the amount lives —
total_pricefor Shopify orders,order.amount_paid, or the defaultamountfor generic goals. The value is recorded verbatim as a number in your account's own currency; the platform never converts currencies. Leave the path empty (or let it miss) and conversions are still counted — just without revenue. - Two attribution windows. How many days before the conversion to look back for a click (default 5) and for an open (default 1). Each window accepts 0–90 days; 0 disables that touch type.
- An active toggle. Paused goals stop matching new fires; everything already recorded stays reportable.
The attribution model
One model, applied to every conversion: last-touch, clicks beat opens.
- When a goal-matching event fires, an attribution job finds every send the converting address received from you — campaign sends and automation sends both qualify.
- It looks for the most recent click inside the goal's click window before the conversion. If one exists, that email's campaign or automation gets the conversion, attribution
click. Click priority is absolute: a qualifying click wins even over a newer open. - Only when no click qualifies does it look for the most recent open inside the (typically shorter) open window — attribution
open. - No qualifying touch at all? The conversion is recorded with attribution
none— counted honestly in your totals, credited to no email. A touch after the conversion never qualifies; you cannot be converted by an email you read later.
Touches are matched by email address across all your lists — the converting human is one person, however many subscriber rows share the address. Each event fire attributes at most once per goal: replays and webhook retries of the same fire cannot inflate your numbers.
Recording conversions via the API
For stacks without a Stripe or Shopify connection, POST /api/v1/conversions records a conversion directly. It requires the analytics scope and internally fires the goal's bound trigger event through the standard pipeline — a conversion is a fire, so automations listening for the event enroll the contact as usual.
# Record a purchase against a goal (find goal uids in Settings → Analytics
# or via GET /api/v1/conversions/goals)
curl -X POST https://emailflow.ai/api/v1/conversions \
-H "Authorization: Bearer efa_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-8412-conversion" \
-d '{
"goal_uid": "68a91b2c7de31",
"email": "alice@example.com",
"amount": 129.90,
"occurred_at": "2026-07-05T13:30:00Z"
}'
# Response (201) — attribution resolves asynchronously
{
"data": {
"fire_id": "68a91b2c7de99",
"goal_uid": "68a91b2c7de31",
"email": "alice@example.com",
"status": "recorded",
"occurred_at": "2026-07-05T13:30:00+00:00"
}
}
amountis optional and lands on the fire payload'samountkey — picked up automatically by generic goals, whose revenue path defaults toamount.occurred_at(optional, ISO 8601) backdates the conversion — the attribution windows anchor to it, so a webhook that reaches you an hour late still attributes against the right day. Defaults to now.- An inactive goal, or a goal whose bound event was deleted, refuses with
409 CONFLICT; an unknowngoal_uidis404. Retries are safe with an Idempotency-Key header. GET /api/v1/conversions/goalslists your goals read-only (uid, bound event, windows, lifetime conversion count) so integrating code can discover uids without hard-coding them.
Where the numbers appear
Once at least one goal exists, a Conversions card appears on the surfaces you already use:
- Campaign reports — conversions attributed to that campaign: count, attributed revenue, conversion rate per delivered recipient, and revenue per recipient.
- Automation statistics — the same four numbers for conversions attributed to the flow's emails.
- The dashboard — an account-wide 30-day rollup: total conversions, attributed revenue, how many were attributed to email, and how many were counted unattributed.
- Settings → Analytics — the same 30-day summary above the goals table, plus a lifetime count and revenue per goal.
The aggregates also ground the analytics Q&A chat — once revenue flows, questions like “which campaign drove the most revenue this month?” are answered from your own attributed numbers.
What the model does not claim
Attribution is a model, not a confession — these are its documented edges:
- No retroactivity. Attribution starts the moment a goal is created; the UI shows each goal's “tracking since” date. Conversions that happened before the goal existed were never recorded, and no backfill invents them.
- One model. Last-touch with click-over-open priority — no configurable multi-touch, no fractional credit. Every number on every card is explainable by the four steps above.
- Marketing sends only. Campaign and automation emails are qualifying touches. Transactional messages are deliberately not — a password-reset email should never claim your revenue.
- Amounts are verbatim. Whatever number sits at the revenue path is recorded as-is, assumed to be your account's currency. Mixed-currency stores should normalize before sending.
- Deleting a goal deletes its history. The goal's recorded conversions go with it — reports stop showing them immediately. Pause a goal instead when you only want to stop new matching.