Skip to content

Conversions

Closing the loop between creator output and business outcome, defining conversion events, sending conversions, and thinking about attribution.

Updated Oct 8, 2026 · 8 min read

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.

bash
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-events

A few things worth getting right the first time, because they are not changeable later:

  • key is permanent. It's the identifier you'll paste into your backend. Keep it lowercase and boring: signup, purchase, demo_booked.
  • currency is 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_cents is 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:

Patterndefault_value_centsPer-conversion value_cents
Count-style, signups, installs, follows0omit
Money-style: purchases, subscriptions0send the real order value
Fixed-worth: a lead you value at a flat $252500omit

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:

bash
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>/conversions

That records one signup attributed to that creator's TikTok account.

A revenue example, attributed to a specific video, with a real order value:

bash
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>/conversions

quantity 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:

TargetWhen to use it
video_idYou know which specific post drove it (a per-video link or code)
account_idYou know the platform account but not the post
platform + handleSame as account_id, but you only have the handle
creator_idYou 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:

LayerScopeProtects against
Idempotency-Key header24 hours, per keyThe request timed out and you don't know if it landed
external_id fieldForever, per projectAnything, ever, resending the same event

How rollup works#

Attribution rolls up the hierarchy:

video  →  account  →  creator

A 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:

bash
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=voided or ?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#

  1. Define one event

    Pick the outcome you already report on internally. One event, correct, beats five events, half-wired.

  2. 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.

  3. 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.

  4. Check it landed

    GET /conversions?creator_id=<uuid> and confirm the rows look right: target type, value, currency, timestamp.

  5. Void, don't delete, when outcomes reverse

    Wire refunds and chargebacks to a DELETE with a reason. Do this early; retrofitting it later means a period of overstated revenue.

  6. Add more event types once the first is trustworthy

    Then compare cost (payouts) against outcome (conversions) per creator.