trackagoat logotrackagoat/Docs

Command Palette

Search for a command to run...

Getting started

  • Welcome
  • Quickstart
  • Core concepts

Guides

  • Creators & Accounts
  • Creators
  • Instagram tracking
  • YouTube tracking
  • Videos
  • Campaigns
  • Creator Goals
  • Tracking Inbox
  • Content calendar
  • How scraping works
  • Analytics & metrics
  • Similar creator pools
  • Over-posting & suppression
  • Program Health
  • Sentiment Radar
  • API keys
  • Limits & plan tiers
  • Notifications
  • Payouts
  • Conversions

API reference

  • Overview
  • Authentication
  • Errors
  • Projects
  • Creators
  • Accounts
  • Videos
  • Content Groups
  • Campaigns
  • Analytics
  • Aggregate Analytics
  • Goal Compliance
  • Payouts
  • Conversions
  • Schema

For agents

  • Agent guide
  • Data model
  • MCP & tooling

Platform

  • Brand
  • Changelog
  • Support
DocsGuides

Conversions

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

PreviousPayoutsNextOverview

On this page

  • The two pieces
  • Defining your first conversion event
  • Which value should an event carry?
  • How many events can I define?
  • Sending your first conversion
  • Picking a target
  • Don't skip external_id
  • How rollup works
  • How to think about attribution
  • Attribution is frozen at receipt time
  • Trackagoat records attribution, it doesn't decide it
  • Handling refunds: void, don't delete
  • A sensible rollout
  • Related

Conversions

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.

Define events in the app, send conversions from your backend

Conversion events are defined in Trackagoat under Tracking → Conversions → Manage events, where you can also deactivate them and review what each one has recorded. No API key is needed for that.

An API key is needed for the other half: recording conversions, which your own system does over the v2 API. There is no button in the app to add a conversion. They always arrive from your backend. The app is where you define event types and read results. Create one from Org Settings → API keys. See the API keys guide. The key needs the write scope. Everything in the app is also available through the API if you would rather provision events programmatically.

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.

platform + handle never creates an account

Attributing by handle resolves only to an account that already exists in that project. Matching is case-insensitive and the @ is optional, so @CharliDAmelio and charlidamelio both work. A handle trackagoat doesn't know returns 404 account_handle_not_found and creates nothing. A typo in a webhook should surface as an error, not spawn a junk account and burn an account slot.

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

A fresh key with a repeated external_id is not an error

If you retry with a new Idempotency-Key but the same external_id, you get HTTP 200, deduped: true, and the original conversion's ID. Nothing was created and nothing went wrong. Treat it as success.

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.

Hard delete is for mistakes, not reversals

?mode=hard removes the row permanently and requires an API key with the admin scope. Use it for test data and genuine mistakes: a conversion recorded against the wrong project, say. Never use it for a refund: you would lose the fact that revenue was ever recorded, and your history would no longer reconcile with your own books.

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

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
  • Limits & plan tiers: how max_conversion_events_per_project is enforced
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.