Trackagoat tracks creator output (views) and creator cost (payouts). Conversions add creator outcome: signups, purchases, installs, or whatever else you count as a result.
There are two resources:
Conversion events, the definitions, scoped to a project. signup, purchase, trial_started. Each carries a default value and a currency.
Conversions: the facts. Each one references a conversion event and is attributed to a creator, an account, or a video.
All endpoints require a Bearer API key. Responses follow the standard { data, error, meta } envelope. Money is in integer cents. Timestamps are ISO 8601.
Scopes
GET needs the read scope. POST and PATCH need write. DELETE needs write for the default (soft) mode and admin for ?mode=hard: conversions are financial records, and a write-scoped webhook key should not be able to erase revenue history. A key without the required scope is rejected with 403 insufficient_scope.
Conversion Events
GET /api/v2/projects//conversion-events
List the conversion event definitions in a project. Active events only by default.
Returns an array sorted newest first, with meta.count.
Response fields
Field
Type
Description
id
uuid
Conversion event ID
project_id
uuid
Owning project
org_id
uuid
Owning organization
key
string
Stable slug used by API callers (e.g. purchase)
name
string
Human label (e.g. Purchase)
POST /api/v2/projects//conversion-events
Define a new conversion event. Requires the write scope.
bash
curl -X POST \ -H "Authorization: Bearer tga_<key>" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "key": "purchase", "name": "Purchase", "description": "A completed checkout attributed to a creator link", "default_value_cents": 0, "currency": "USD" }' \ https://www.trackagoat.com/api/v2/projects/<projectId>/conversion-events
Body
Field
Type
Required
Description
key
string
✓
Lowercase slug, max 63 chars. Must start with a letter or digit and may contain letters, digits, ., -, and _. Unique per project (case-insensitive)
name
string
✓
Display name, max 100 chars
description
string
Max 500 chars
default_value_cents
number
Returns 201 with the created event.
Currency is fixed at definition time
A conversion event's currency cannot be changed later, and individual conversions have no currency field of their own. They inherit the event's. One event type means one currency, which is what makes SUM(total_value_cents) well-defined for any set of conversions. If you need to record purchases in EUR as well as USD, define two events (purchase_usd, purchase_eur).
Reusing an existing key in the same project returns 409. Exceeding the plan's max_conversion_events_per_project returns 402 limit_reached. See Limits.
GET /api/v2/projects//conversion-events/
Fetch a single event. Returns 404 conversion_event_not_found if the event does not exist in that project.
PATCH /api/v2/projects//conversion-events/
Update an event. Requires the write scope. At least one field must be supplied.
key and currency are deliberately not updatable. The key is the stable identifier you hard-code into your backend; changing the currency would retroactively reinterpret every conversion already recorded under the event.
Changing default_value_cents affects only conversions recorded after the change. The value is copied onto each conversion at write time.
DELETE /api/v2/projects//conversion-events/
Deactivates by default. Requires the write scope (admin for ?mode=hard).
bash
# Deactivate: the event stops accepting new conversions, history is preservedcurl -X DELETE -H "Authorization: Bearer tga_<key>" \ "https://www.trackagoat.com/api/v2/projects/<projectId>/conversion-events/<eventId>"# Hard delete: removes the definition AND every conversion recorded under itcurl -X DELETE -H "Authorization: Bearer tga_<key>" \ "https://www.trackagoat.com/api/v2/projects/<projectId>/conversion-events/<eventId>?mode=hard"
Parameter
Type
Description
mode
hard
Anything else (including omitting it) deactivates
Returns { id, mode } with meta.conversions_affected: the number of conversions currently attached to the event. Check that number before hard-deleting.
Hard delete cascades
?mode=hard deletes the event row, and conversions cascade with it. That is revenue history and it cannot be recovered. Deactivating (is_active: false) is almost always what you want, because the event stops accepting new conversions while every past conversion stays queryable.
Deactivating also frees a slot against max_conversion_events_per_project, which counts active events only.
Conversions
GET /api/v2/projects//conversions
List conversions in a project, newest occurred_at first.
bash
# All conversions rolled up to one creator, in a date windowcurl -H "Authorization: Bearer tga_<key>" \ "https://www.trackagoat.com/api/v2/projects/<projectId>/conversions?creator_id=<uuid>&from=2026-07-01T00:00:00Z"
Query parameters
Parameter
Type
Default
Description
conversion_event_id
uuid
Only conversions of this event type
creator_id
uuid
Rollup filter. See below
account_id
uuid
Rollup filter. See below
video_id
uuid
Only conversions attributed to this video
source
string
creator_id and account_id are rollup filters
creator_id and account_id match the rolled-up attribution, not just what the caller originally targeted. ?creator_id=X returns every conversion recorded against creator X plus those recorded against X's accounts and X's videos. See Rollup below.
from and to are full ISO 8601 timestamps (2026-07-01T00:00:00Z), not bare dates.
Response
meta carries { hasMore, nextCursor }. Cursors are opaque base64url strings. Pass nextCursor back verbatim as cursor, do not parse or modify it.
Conversion fields
Field
Type
Description
id
uuid
Conversion ID
conversion_event_id
uuid
The event definition
conversion_event_key
string | null
The event's key, denormalized for convenience
conversion_event_name
string | null
The event's name
project_id
uuid
Owning project
POST /api/v2/projects//conversions
Record one conversion, or a batch. Requires the write scope.
Body: a single conversion object, or { "conversions": [ ... ] } with 1–500 objects. Each object:
Field
Type
Required
Description
conversion_event_id
uuid
one of
The event definition, by ID
conversion_event_key
string
one of
The event definition, by key (case-insensitive)
creator_id
uuid
one of
Attribute to a creator
account_id
uuid
one of
Attribute to a platform account
video_id
Supply exactly one of conversion_event_id or conversion_event_key, and exactly one target: creator_id, account_id, video_id, or the platform + handle pair. Supplying none, or more than one, returns 400 invalid_body. platform and handle must be supplied together.
Response. A single-conversion request returns 201 with the created conversion. A batch returns 201 with an array in request order. meta.deduped_count reports how many rows in the request were deduplicated against an existing external_id.
Choosing a target
You supply
target_type
Resolution
video_id
video
Video must be in the project and have an owning account, else 409 video_unattributed
account_id
account
Account must be in the project and linked to a creator, else 409 account_unlinked
creator_id
creator
Creator must be in the project
platform + handle
platform + handle matching is case-insensitive and a leading @ is optional. It resolves only to an account that already exists in that project: a handle trackagoat does not know returns 404 account_handle_not_found and no account is created. A typo in a webhook should surface as an error, not silently spawn a junk account.
Rollup
Attribution rolls up the hierarchy, and the rollup is written onto the row at insert time:
video → account → creator
A conversion recorded against a video also counts for that video's account and that account's creator. A conversion recorded against an account also counts for its creator. target_type preserves what you actually attributed to, so a report can still say "attributed to video X" while creator-level totals include it.
That is why ?creator_id=X on the list endpoint returns conversions recorded against X's accounts and videos too.
Attribution is frozen
The rollup is computed once, when the conversion is received. If a video is later reassigned to a different account, its past conversions do not move by default. They were earned under the ownership that existed when they happened, and silently rewriting them would make past reports non-reproducible.
To override that for a single video, POST /api/videos/{videoId}/conversions with {"mode": "reassign"} rewrites every recorded conversion on that video to its current creator; {"mode": "freeze"} explicitly keeps history and dismisses the drift notice. Both require an org admin session. This endpoint is not part of the API-key surface.
Value and quantity
total_value_cents = value_cents × quantity
value_cents falls back to the conversion event's default_value_cents, so a count-style event (a signup) needs no value on every call. quantity defaults to 1. Currency comes from the event. A conversion has no currency field.
Idempotency
Two independent layers apply, and they answer different questions.
1. The Idempotency-Key header: replays the whole response for 24 hours.
Send a fresh UUID per logical request. If the call times out, retry with the same key: the server returns the original response body and status unchanged, with an Idempotency-Replay: true header, and creates nothing new. Reusing a key against a different endpoint returns 409 idempotency_conflict. Keys must be ≤ 255 characters.
2. The external_id field: permanent per-project dedupe.
external_id is unique per project, forever. If you POST a conversion whose external_id already exists in the project, trackagoat does not create a duplicate and does not error: it returns the original conversion's id with deduped: true.
The case that trips people up
A retry with a freshIdempotency-Key but a repeatedexternal_id is not an error. The header cache misses (new key), so the handler runs, but the external_id collides, so nothing is inserted. You get HTTP 200 with deduped: true and the original conversion's id. Treat that as success. The conversion is already recorded.
(Status 200 rather than 201 applies to the single-conversion form, where the write created nothing. A batch always returns 201; check each row's deduped flag and meta.deduped_count.)
Send external_id whenever you have a natural key: an order ID, a user ID, a webhook event ID. It is the difference between "my retry logic is correct" and "my revenue numbers are correct".
GET /api/v2/projects//conversions/
Fetch a single conversion. Returns 404 conversion_not_found if it does not exist in that project.
DELETE /api/v2/projects//conversions/
Two modes, because both are legitimately needed.
bash
# Void (default): soft, auditable, reversible in the sense that the row survivescurl -X DELETE -H "Authorization: Bearer tga_<key>" \ "https://www.trackagoat.com/api/v2/projects/<projectId>/conversions/<conversionId>?reason=Refunded"# Hard delete: irreversible, requires the admin scopecurl -X DELETE -H "Authorization: Bearer tga_<key>" \ "https://www.trackagoat.com/api/v2/projects/<projectId>/conversions/<conversionId>?mode=hard"
Parameter
Type
Default
Description
mode
void | hard
void
hard requires the admin scope
reason
string
Max 500 chars. Stored as voided_reason (void mode only)
mode=void sets status: 'voided', records voided_at, voided_by, and voided_reason, and excludes the conversion from every total. The row stays queryable via ?status=voided or ?status=all. This is the right answer for a refund, a chargeback, or a fraudulent order. Voiding an already-voided conversion is a no-op, not an error: safe to retry. Returns the conversion with mode: "void".
mode=hard deletes the row outright. For genuine mistakes and test data only. Requires the admin scope. Returns { id, mode: "hard", deleted: true }.
Limits
max_conversion_events_per_project caps how many conversion event types a project may define. It counts active events only: deactivating one frees a slot.
Plan
Conversion events per project
Free
3
Starter
25
Ultra
Unlimited
Conversion volume is not capped. Silently dropping a customer's revenue data at a cap would be worse than the revenue the cap protects. Exceeding the event-type limit returns 402 limit_reached with meta.current and meta.max.
Error codes
Code
HTTP
Description
invalid_api_key
401
Missing, expired, or invalid Bearer token
insufficient_scope
403
Key lacks read, write, or (for ?mode=hard) admin
limit_reached
402
max_conversion_events_per_project exceeded
invalid_body
400
Zod validation failed: including a missing or ambiguous target. See meta.errors
A duplicate conversion event key returns 409 with a message naming the key.
See the Conversions guide for a walkthrough of defining your first event and sending your first conversion.
description
string | null
Free-form description
default_value_cents
number
Value applied to a conversion that does not supply its own
currency
string
ISO 4217 code. Applies to every conversion recorded under this event
is_active
boolean
Inactive events reject new conversions
metadata
object
Agent-writable structured data
created_at
ISO 8601
updated_at
ISO 8601
Integer ≥ 0. Default 0
currency
string
3-letter ISO 4217 code, uppercased. Default USD
metadata
object
Agent-writable structured data. Default {}
Exact match on the source field
status
recorded | voided | all
recorded
Voided conversions are hidden unless you ask for them
from
ISO 8601 with offset
occurred_at on or after this instant. Bare dates and offset-less timestamps are rejected.
to
ISO 8601 with offset
occurred_at on or before this instant. Bare dates and offset-less timestamps are rejected.
limit
number
50
Items per page (max 200)
cursor
string
Pagination cursor from meta.nextCursor
target_type
creator | account | video
What the caller attributed to
creator_id
uuid | null
Always populated (rollup)
account_id
uuid | null
Populated for account and video targets
video_id
uuid | null
Populated for video targets only
value_cents
number
Unit value. Falls back to the event's default_value_cents
quantity
number
Units. Default 1
total_value_cents
number
value_cents × quantity, computed by the database
currency
string
Inherited from the conversion event
external_id
string | null
Your own ID for this conversion (permanent dedupe key)
source
string | null
Free-form attribution source label
label
string | null
Short label, max 200 chars
note
string | null
Longer note, max 1000 chars
metadata
object
Agent-writable structured data
status
recorded | voided
Voided conversions are excluded from totals
voided_at
ISO 8601 | null
voided_reason
string | null
occurred_at
ISO 8601 with offset
When the conversion happened (defaults to receipt time). Must carry an explicit offset: 2026-07-21T14:03:00Z or +01:00. A bare local timestamp is a 400.
created_at
ISO 8601
When trackagoat received it
uuid
one of
Attribute to a video
platform + handle
string + string
one of
Attribute to an existing account by handle
value_cents
number
Integer ≥ 0. Defaults to the event's default_value_cents
quantity
number
Integer ≥ 1, max 1,000,000. Default 1
external_id
string
Your own ID, max 255 chars. Permanent per-project dedupe key
source
string
Max 100 chars
label
string
Max 200 chars
note
string
Max 1000 chars
metadata
object
Default {}
occurred_at
ISO 8601
When it happened. Defaults to now
account
Matches an account already in the project
invalid_query
400
Bad query parameter
invalid_cursor
400
Cursor could not be decoded. Pass meta.nextCursor back verbatim
conversion_event_not_found
404
No conversion event with that ID or key in the project
creator_not_found
404
No creator with that ID in the project
account_not_found
404
No account with that ID in the project
video_not_found
404
No video with that ID in the project
account_handle_not_found
404
No account with that platform + handle in the project. Add the account first
conversion_not_found
404
No conversion with that ID in the project
conversion_event_inactive
409
The event is deactivated and cannot accept new conversions
account_unlinked
409
The account is standalone (not attached to a creator), so nothing can roll up
video_unattributed
409
The video has no owning account
idempotency_conflict
409
Idempotency-Key reused against a different endpoint