Conversions
Closing the loop between creator output and business outcome, defining conversion events, sending conversions, and thinking about attribution.
Trackagoat already tells you what your creators produced (views, engagement, posting consistency) and what they cost (payouts). Conversions tell you what they produced for the business: signups, purchases, installs, bookings, whatever counts as a result for you.
Once conversions are flowing, questions like "which creator actually drove revenue last month, not just views?" become answerable.
The two pieces#
Conversions have exactly two moving parts.
Conversion events are the definitions. They live in a project and you create a handful of them, once. signup. purchase. trial_started. Each definition carries a key (the stable slug your backend hard-codes), a default value, and a currency.
Conversions are the facts. Each one references a conversion event and is attributed to a creator, an account, or a video. You send these from your own system. Your checkout webhook, your signup handler, your nightly export: as they happen.
Defining your first conversion event#
Start with one. Resist the urge to model your whole funnel on day one. You can always add more, and every event type you define is a decision your backend has to keep honouring.
A good first event is the thing you already report on internally. If you run a paid product, that's purchase. If you run a free product, that's signup.
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{
"key": "signup",
"name": "Signup",
"description": "A new account created from a creator link",
"default_value_cents": 0,
"currency": "USD"
}' \
https://www.trackagoat.com/api/v2/projects/<projectId>/conversion-eventsA few things worth getting right the first time, because they are not changeable later:
keyis permanent. It's the identifier you'll paste into your backend. Keep it lowercase and boring:signup,purchase,demo_booked.currencyis permanent. It lives on the definition, not on individual conversions, which is what makes any sum of conversions well-defined. If you sell in two currencies, define two events (purchase_usd,purchase_eur).default_value_centsis not permanent, but changing it only affects conversions recorded afterwards. The value is copied onto each conversion as it arrives.
Which value should an event carry?#
Two patterns cover almost everything:
| Pattern | default_value_cents | Per-conversion value_cents |
|---|---|---|
| Count-style, signups, installs, follows | 0 | omit |
| Money-style: purchases, subscriptions | 0 | send the real order value |
| Fixed-worth: a lead you value at a flat $25 | 2500 | omit |
For a count-style event, you just want volume, so leave the value at zero and read total_conversions. For a money-style event, send the actual order total on every call.
How many events can I define?#
max_conversion_events_per_project caps the number of event types per project: 3 on Free, 25 on Starter, unlimited on Ultra. Only active events count, so deactivating an event you no longer use frees a slot.
Conversion volume is never capped. Trackagoat will not quietly drop your revenue data at a limit.
Sending your first conversion#
The minimum viable conversion is a conversion event and a target:
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"conversion_event_key": "signup",
"platform": "tiktok",
"handle": "charlidamelio",
"external_id": "user_88213"
}' \
https://www.trackagoat.com/api/v2/projects/<projectId>/conversionsThat records one signup attributed to that creator's TikTok account.
A revenue example, attributed to a specific video, with a real order value:
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"conversion_event_key": "purchase",
"video_id": "<uuid>",
"value_cents": 4999,
"quantity": 2,
"external_id": "order_10021",
"occurred_at": "2026-07-21T14:03:00Z",
"note": "Bundle promo"
}' \
https://www.trackagoat.com/api/v2/projects/<projectId>/conversionsquantity multiplies: this records a total of $99.98 (value_cents × quantity), which trackagoat computes for you as total_value_cents.
If you're batching from a nightly job, send up to 500 conversions in one request as {"conversions": [ ... ]}.
Picking a target#
You attribute a conversion by supplying exactly one of these:
| Target | When to use it |
|---|---|
video_id | You know which specific post drove it (a per-video link or code) |
account_id | You know the platform account but not the post |
platform + handle | Same as account_id, but you only have the handle |
creator_id | You know the person, not the platform |
Supplying none of them, or two: is a 400. That's deliberate: an ambiguous target is a silent data-quality bug, and it's better to fail the webhook loudly.
Don't skip external_id#
external_id is your own ID for the thing that converted: the order ID, the user ID, the webhook event ID. It is unique per project, forever.
If your webhook fires twice, or your retry logic double-fires, or you re-run last night's export, the second attempt does not create a duplicate. It returns the original conversion's ID with deduped: true, as a success. This is the single cheapest thing you can do to keep your numbers correct.
There is also an Idempotency-Key header, which replays the entire response for 24 hours if you retry with the same key. The two work together and cover different failure modes:
| Layer | Scope | Protects against |
|---|---|---|
Idempotency-Key header | 24 hours, per key | The request timed out and you don't know if it landed |
external_id field | Forever, per project | Anything, ever, resending the same event |
How rollup works#
Attribution rolls up the hierarchy:
video → account → creatorA conversion recorded against a video also counts for that video's account, and for that account's creator. A conversion recorded against an account also counts for its creator.
This means you can attribute at whatever precision your data actually supports, and still read totals at whatever level you want to report at. If your checkout only knows which creator sent the customer, attribute to the creator. If you run per-video discount codes, attribute to the video, and the creator's total still includes it.
Concretely: GET /conversions?creator_id=X returns conversions recorded against creator X, against X's accounts, and against X's videos. You don't have to fan out the query yourself.
The original precision isn't lost. Every conversion keeps a target_type (creator, account, or video) recording what you actually attributed to, so a per-video report is still exact.
How to think about attribution#
Two properties are worth internalising, because they explain most "wait, why does it say that?" moments.
Attribution is frozen at receipt time#
The rollup is computed once, when the conversion arrives. If a video is later moved to a different account, its past conversions do not move with it.
That's intentional. A conversion is a historical fact: it was earned under the ownership that existed when it happened. Silently rewriting old conversions when today's org chart changes would make last quarter's report irreproducible: you'd run the same query twice and get two answers.
If you're staring at a creator's total wondering why some conversions "didn't follow" a reassigned video, this is why.
You can override it deliberately. Freezing is the default, not a hard rule. When trackagoat notices that a video has moved since its conversions were recorded, the video's Conversions tab shows an attribution-drift notice with two choices:
- Freeze. Keep history as-is (the default, and usually correct).
- Reassign: rewrite that video's recorded conversions to its current creator.
Reassign is restricted to org admins, and it applies only to the video you're viewing. Use it when a video was attributed to the wrong account by mistake, not to reflect a legitimate change of ownership.
Trackagoat records attribution, it doesn't decide it#
Trackagoat has no view into your funnel. It doesn't set cookies, run last-click logic, or guess. It stores the attribution you send it.
That means the quality of your conversion data is entirely the quality of the decision your backend makes before it POSTs. Whatever you already use to connect a customer to a creator: a UTM parameter, a discount code, a tracked link, a "how did you hear about us" answer: is the thing to encode in the target. The source field is there to record which of those mechanisms was responsible, so you can compare them later.
Handling refunds: void, don't delete#
Business outcomes get reversed. Orders get refunded, cards get charged back, fraudulent signups get cleaned up. The answer is to void the conversion:
curl -X DELETE -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/projects/<projectId>/conversions/<conversionId>?reason=Refunded%20-%20order%2010021"Voiding is the default behaviour of DELETE. You don't need ?mode=void. It:
- sets the conversion's status to
voided - excludes it from every total and every default list
- records when, who, and why (
reason) - keeps the row queryable via
?status=voidedor?status=all
So your creator totals drop by exactly the refunded amount, and you can still answer "what did we void last month, and why". Voiding an already-voided conversion is a no-op, so a retry is safe.
Because voiding lowers any unpaid payout earned from that conversion, voiding in the app (the conversion's detail drawer) is limited to org admins, the same as voiding a payout accrual.
The same logic applies to conversion events themselves. DELETE on an event deactivates it: it stops accepting new conversions while all its history survives. ?mode=hard on an event deletes every conversion ever recorded under it. The response tells you how many conversions are attached (meta.conversions_affected). Read that number before you do anything irreversible.
A deactivated event still holds its key, so re-creating it returns 409 rather than a second copy. It is also hidden from the event pickers used when building a payout structure. Bring it back under Tracking → Conversions → Manage events → Reactivate, or with PATCH .../conversion-events/{eventId} and {"is_active": true}. Reactivating counts against max_conversion_events_per_project again, so it can return a 402 if the project is at its cap.
A sensible rollout#
Define one event
Pick the outcome you already report on internally. One event, correct, beats five events, half-wired.
Wire your backend to POST it
From the place that already knows the outcome happened. Your checkout webhook, your signup handler. Always send an
external_id.Attribute at the precision you actually have
Creator-level is fine. Rollup means you can start coarse and get more precise later without losing comparability.
Check it landed
GET /conversions?creator_id=<uuid>and confirm the rows look right: target type, value, currency, timestamp.Void, don't delete, when outcomes reverse
Wire refunds and chargebacks to a
DELETEwith areason. Do this early; retrofitting it later means a period of overstated revenue.Add more event types once the first is trustworthy
Then compare cost (payouts) against outcome (conversions) per creator.
Related#
- Conversions API reference: every endpoint, field, and error code
- API keys: creating a key and choosing scopes
- Payouts: the cost side of the same equation
- Plans and limits: how
max_conversion_events_per_projectis enforced