CAPI when the sale happens outside your site
Every CAPI guide assumes the purchase happens on your domain. With digital products it happens on the platform's checkout, and that changes the event_id, the status filter and where user_data comes from.

Conversions API guides are almost all written for stores: the person browses, adds to cart and pays, all on the same domain. Anyone selling digital products lives a different reality — the purchase happens on the platform's checkout, and news of the sale arrives by webhook, minutes or days later.
That changes four decisions. None of them is hard, and all of them are wrong by default.
1. event_id cannot be generated at send time
The event identifier is what stops Meta counting the same purchase twice. If you keep the browser pixel and turn on CAPI, both sides must send the same value.
Because the sale arrives by webhook, the temptation is to generate an identifier at send time. By definition it will differ from what the browser sent. The right value comes from the business: the transaction code the platform returns, which is stable and identical on both paths.
2. Status is not what you think it is
This is where the segment's most expensive mistake lives. Digital product platforms have far more states than paid and unpaid: invoice printed, payment initiated, payment expired, purchase approved, purchase complete, disputed, refunded, charged back, overdue.
Sending all of it as conversions teaches the campaign to find people who start payments and never finish — and it obeys very well. The correct filter is usually a pair of states, and nothing else.
There is a detail almost nobody notices: the state name arriving in the webhook is not always the name shown in the platform's table. We have measured cases where the screen shows one name and the payload delivers another — and a filter written by looking at the screen never matches.
The practical rule is measure before mapping: pull a sample of recent real events, list the states present, and write the filter against that list — never against the documentation.
The second event of the same sale
Several platforms emit a purchase approved event immediately and a purchase complete event days later, when the refund window closes. Those are two events for one sale. Filtering both as conversions doubles your revenue; filtering only the second delays campaign learning by a week.
3. user_data does not come from the webhook
The sale payload carries the buyer's email, and it is tempting to stop there. But the webhook carries neither the ad click cookie nor the browser cookie — they stayed on the site, in the visit that happened earlier.
Without them you send a conversion with an email and no browser signal at all, losing direct attribution on the very event that matters. Correct user_data assembly crosses both: what arrived in the webhook and what the platform already knew about that person.
4. History sync must not become a conversion
Every digital product platform offers an API to fetch past sales, and it is common to sync hourly to keep reports current. That is great for reports and terrible for sending.
If every sync re-emits the sales in its window, the same purchase becomes a conversion several times a day. In one case we measured, a single integration produced roughly seven deliveries per sale — and Meta's report showed revenue that did not exist.
The rule is to separate the two paths: the webhook triggers conversions, the sync feeds reports. When the sync finds a sale the webhook missed, then it emits — once.
The summary worth pinning to the wall
| Decision | The wrong default | What to do |
|---|---|---|
| event_id | Generated at send time | The platform's transaction code |
| Status | Everything that arrives | Only sale states, measured on the real payload |
| user_data | Only what came in the webhook | Crossed with the person's history |
| History sync | Re-emits the whole window | Emits only what is new |
In CrazyLeads these four decisions are the default behaviour for Hotmart and Kiwify: business identity as event_id, filters written against a sample of your account's real events, user_data assembled from the graph, and a sync that only emits what the webhook did not bring.