Payouts
Manage payout methods, structures, assignments, accruals, and payments via the trackagoat API (v2).
All payout endpoints require a Bearer API key. Responses follow the standard { data, error, meta } envelope. Amounts are in cents (integer). Timestamps are ISO 8601 UTC.
Payout Methods#
Payout methods are org-scoped. Default methods (Venmo, Cash App, PayPal) cannot be deleted: use PATCH { is_active: false } to disable.
GET/api/v2/payout-methods#
List org's payout methods.
curl -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/payout-methods"
# Include disabled methods
curl -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/payout-methods?include_disabled=1"Query parameters
| Parameter | Type | Description |
|---|---|---|
include_disabled | 1 | true | Include inactive methods (org admins only) |
Response fields: id, org_id, name, icon_url, is_default, is_active, metadata, readme, created_at
POST/api/v2/payout-methods#
Create a custom payout method.
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{"name": "Zelle"}' \
https://www.trackagoat.com/api/v2/payout-methodsBody
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✓ | Display name (max 100 chars) |
icon_path | string | null | Storage path of an uploaded icon. A new method's id isn't known until it's created, so add the icon afterwards: see Method icons | |
metadata | object | Agent-writable structured data | |
readme | string | null | Markdown notes |
Returns 201 with the created method row.
GET/api/v2/payout-methods/#
Fetch a single method.
PATCH/api/v2/payout-methods/#
Update name, active state, metadata, or readme.
curl -X PATCH \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{"is_active": false}' \
https://www.trackagoat.com/api/v2/payout-methods/<uuid>Body: name?, is_active?, icon_path?, metadata?, readme? (at least one required). icon_path must be inside this method's folder (orgs/{orgId}/{methodId}/…), as returned by the icon upload URL below; null removes the icon.
Method icons#
Icons are PNG, JPEG or WebP images up to 1 MB, stored in a public bucket. Only org admins can change them (403 forbidden otherwise). Two ways to upload:
Inline (one call). POST /api/v2/payout-methods/{id}/icon with the image as base64. It is checked against content_type (content_type_mismatch otherwise), stored, and set as the method's icon_path; the updated method is returned.
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d "{\"content_type\": \"image/png\", \"content_base64\": \"$(base64 < wise.png | tr -d '\n')\"}" \
https://www.trackagoat.com/api/v2/payout-methods/<uuid>/iconSigned URL. POST /api/v2/payout-methods/{id}/icon/upload-url with { "content_type": "image/png" } returns signed_url, path, max_bytes and expires_in_seconds (2 hours). PUT the raw image to signed_url with the same Content-Type, then PATCH /api/v2/payout-methods/{id} with { "icon_path": "<path>" }.
curl -X PUT -H "Content-Type: image/png" --data-binary @wise.png "<signed_url>"DELETE/api/v2/payout-methods/#
Delete a custom method. Returns 409 default_method for seeded default methods.
Payout Structures#
Payout structures define earning rules for creators. Scoped to a project. Rule edits create new immutable versions.
GET/api/v2/payout-structures#
List structures in the org (optionally filtered to a project).
curl -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/payout-structures?project_id=<uuid>"Query parameters
| Parameter | Type | Description |
|---|---|---|
project_id | uuid | Filter to a project |
is_active | true | false | Filter by active state |
limit | number | Items per page (max 100, default 50) |
cursor | string | Pagination cursor |
POST/api/v2/payout-structures#
Create a payout structure (creates version 1 atomically).
# Recurring $50/month
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{
"project_id": "<uuid>",
"name": "Monthly base",
"period": "monthly",
"amount_cents": 5000,
"currency": "USD",
"criteria_type": "recurring",
"criteria_params": {},
"proration_mode": "skip"
}' \
https://www.trackagoat.com/api/v2/payout-structures
# Instagram-only recurring $50/month (platform scope)
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{
"project_id": "<uuid>",
"name": "Instagram base",
"period": "monthly",
"amount_cents": 5000,
"currency": "USD",
"criteria_type": "recurring",
"criteria_params": {},
"proration_mode": "skip",
"platform_scope": "platform",
"platform": "instagram"
}' \
https://www.trackagoat.com/api/v2/payout-structures
# Goals-based monthly with partial completion
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{
"project_id": "<uuid>",
"name": "Monthly goals bonus",
"period": "monthly",
"amount_cents": 50000,
"currency": "USD",
"criteria_type": "goals",
"criteria_params": {
"mode": "all",
"partial_allowed": true,
"partial_multiplier": 1.0,
"min_completion_pct": 50
},
"proration_mode": "skip"
}' \
https://www.trackagoat.com/api/v2/payout-structures
# CPM-based weekly
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{
"project_id": "<uuid>",
"name": "Weekly CPM",
"period": "weekly",
"amount_cents": 0,
"currency": "USD",
"criteria_type": "cpm",
"criteria_params": {
"source": "views_gained",
"view_lifetime_days": null,
"min_impressions": 10000,
"max_payout_cents": 50000,
"tiers": [
{"up_to_impressions": 1000000, "cents_per_thousand": 50},
{"up_to_impressions": null, "cents_per_thousand": 75}
]
},
"proration_mode": "skip"
}' \
https://www.trackagoat.com/api/v2/payout-structures
# View threshold: $20 at 1M views, $100 at 5M views
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{
"project_id": "<uuid>",
"name": "Viral video milestones",
"period": null,
"amount_cents": null,
"currency": "USD",
"criteria_type": "view_threshold",
"criteria_params": {
"thresholds": [
{"view_count": 1000000, "amount_cents": 2000, "currency": "USD"},
{"view_count": 5000000, "amount_cents": 10000, "currency": "USD"}
]
},
"proration_mode": "skip",
"backfill_on_assignment": false
}' \
https://www.trackagoat.com/api/v2/payout-structuresBody
| Field | Type | Required | Description |
|---|---|---|---|
project_id | uuid | ✓ | Target project |
name | string | ✓ | Structure name |
period | daily | weekly | monthly | null | ✓ | Payout frequency. Pass null for view_threshold structures. |
amount_cents | number | null | ✓ | Amount in cents. Pass null for view_threshold and conversion structures (their rates live in criteria_params). Ignored for cpm: the tiers carry the money, so pass 0. |
currency | USD | ✓ | Currently only USD |
criteria_type | recurring | goals | cpm | view_threshold | conversion | ✓ | Earning rule type |
criteria_params | object | ✓ | Rule-specific params (see below) |
proration_mode | skip | prorate | full | ✓ | Mid-period assignment behavior for recurring and goals. Ignored for cpm (earnings already reflect only the views posted in the period); view_threshold and conversion always use skip. |
backfill_on_assignment | boolean | view_threshold and conversion only. If true, also counts what the creator had already earned before assignment (milestones already crossed, or conversions already recorded). | |
period_anchor | 1-7 | null | conversion with a weekly cadence only. ISO day of week (1=Mon) the week starts on, so "every Tuesday" is expressible. null keeps the default Monday alignment. | |
platform_scope | all | platform | account | Default all. Which of an assigned creator's platform accounts the rule earns against (see below). | |
platform | tiktok | instagram | youtube | null | Required when platform_scope is platform; omit/null otherwise. | |
account_id | uuid | null | Required when platform_scope is account; omit/null otherwise. | |
metadata | object | Agent-writable data | |
readme | string | null | Markdown notes |
criteria_params shapes
// conversion: pay for recorded outcomes
criteria_params: {
cadence: "every_conversion" | "daily" | "weekly" | "monthly",
rates: [
{ conversion_event_id: uuid, rate_type: "flat", amount_cents: number },
{ conversion_event_id: uuid, rate_type: "percent", percent_bps: number }, // 1000 = 10%
],
}
// `period` must equal `cadence`, or be null when cadence is "every_conversion".
// `amount_cents` is always null: the rates carry the money.
// Max 25 rates; each conversion event may appear at most once.
// recurring: no params
criteria_params: {}
// goals: pay if creator met their goals
criteria_params: {
mode: "all" | "any" | "n_of_m",
n?: number, // required when mode = "n_of_m"
// Partial completion (only valid when mode = "all")
partial_allowed?: boolean, // default false: enable fractional payouts on missed goals
partial_multiplier?: number, // default 1.0: scales the partial amount (0 to 1)
min_completion_pct?: number, // default 0: 0 to 100; below this avg completion%, no payout fires
}
// cpm: pay per 1,000 views. Each period pays for the views each video gained
// since it was last paid, so videos keep earning after their first period.
// The tiers carry the money: top-level `amount_cents` (pass 0) and
// `proration_mode` are both ignored for this type.
criteria_params: {
source?: "views_gained", // default; the retired "posts_in_period" is accepted and stored as "views_gained"
view_lifetime_days?: number | null, // days after posting a video keeps earning (1 to 3650); null/omitted = forever
min_impressions: number, // views gained in a period before it pays; smaller periods carry over
max_payout_cents: number | null, // cap (null = unlimited)
tiers: [ // sorted ascending; last tier must have null up_to_impressions
{ up_to_impressions: number | null, cents_per_thousand: number }
]
}
// view_threshold: one-time bonus when a video crosses a view count
// period, amount_cents, and proration_mode must be null/skip for this type
criteria_params: {
thresholds: [ // 1 to 10 milestones; view_count values must be unique
{
view_count: number, // minimum lifetime views on the video to trigger
amount_cents: number, // bonus amount in cents
currency: "USD"
}
]
}Partial completion formula (goals, mode = "all"):
When partial_allowed is true and the creator does not fully meet all goals:
- Each goal's completion fraction =
min(actual / target, 1.0). - Average those fractions →
completion_pct. - If
completion_pct < min_completion_pct / 100→ no accrual. - Otherwise:
payout = amount × completion_pct × partial_multiplier.
When all goals are fully met, the full amount is paid regardless of partial_allowed.
Returns 201 with { structure_id, version_id }.
GET/api/v2/payout-structures/#
Fetch structure detail including current_version, full versions[] history, and assignment_count.
PATCH/api/v2/payout-structures/#
Update a structure. Use kind to distinguish cosmetic edits from rule changes.
# Cosmetic: update name and metadata without creating a new version
curl -X PATCH \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{"kind": "cosmetic", "name": "Monthly base v2", "metadata": {"owner": "growth-team"}}' \
https://www.trackagoat.com/api/v2/payout-structures/<uuid>
# Rule edit: creates a new immutable version; existing accruals keep their version
curl -X PATCH \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{
"kind": "rule",
"period": "monthly",
"amount_cents": 7500,
"currency": "USD",
"criteria_type": "recurring",
"criteria_params": {},
"proration_mode": "skip"
}' \
https://www.trackagoat.com/api/v2/payout-structures/<uuid>A rule PATCH accepts the same platform_scope / platform / account_id fields as create (same validation rules), letting a new version re-scope the payout to a platform or account. Omitting them keeps the current version's scope.
A rule PATCH replaces the whole rule, so send every field. To change only how long videos earn on a CPM structure, read the structure first and send its current criteria_params with view_lifetime_days changed (the MCP tool set_cpm_view_lifetime does this for you):
# Stop paying for a video's views 30 days after it's posted (null = forever)
curl -X PATCH \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{
"kind": "rule",
"period": "weekly",
"amount_cents": 0,
"currency": "USD",
"criteria_type": "cpm",
"criteria_params": {
"source": "views_gained",
"view_lifetime_days": 30,
"min_impressions": 10000,
"max_payout_cents": 50000,
"tiers": [
{"up_to_impressions": 1000000, "cents_per_thousand": 50},
{"up_to_impressions": null, "cents_per_thousand": 75}
]
},
"proration_mode": "skip"
}' \
https://www.trackagoat.com/api/v2/payout-structures/<uuid>The new version applies from the period in progress; periods that already ended keep the version they were calculated with.
Rule PATCH returns { version_id, version_number }. Cosmetic PATCH returns the updated structure row.
A rule PATCH cannot change period (or a conversion cadence) or criteria_type once the structure has any accruals or has ever had a creator assigned: it returns 409 structure_shape_locked. Periods are generated from the structure's current shape, so changing it on a live structure would create overlapping accruals for periods that were already paid under the old shape. Create a new structure instead, assign creators to it, and archive the old one. Amounts, criteria parameters and proration can still be edited, and apply to future periods only.
DELETE/api/v2/payout-structures/#
Archive (default) or hard-delete a structure.
# Archive: stops new accruals, preserves historical ones
curl -X DELETE -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/payout-structures/<uuid>"
# Hard delete: blocked with 409 has_accruals if accruals exist
curl -X DELETE -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/payout-structures/<uuid>?mode=hard"Assignments#
Assign or unassign creators to a payout structure.
GET/api/v2/payout-structures//assignments#
List currently assigned creators.
curl -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/payout-structures/<uuid>/assignments"| Parameter | Type | Description |
|---|---|---|
include_inactive | 1 | true | Include historically unassigned creators |
limit | number | Default 50, max 100 |
cursor | string | Pagination cursor |
POST/api/v2/payout-structures//assignments#
Bulk-assign creators. Returns counts of newly assigned, already-assigned, and not-in-project IDs.
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{"creator_ids": ["<uuid1>", "<uuid2>"]}' \
https://www.trackagoat.com/api/v2/payout-structures/<uuid>/assignments| Field | Type | Required | Description |
|---|---|---|---|
creator_ids | uuid[] | ✓ | Creators to assign |
effective_at | ISO 8601 datetime | When earning begins. Defaults to now. Pass a future period boundary to start on a later period. Cannot be earlier than the start of the period currently in progress (400); earning cannot be backdated into periods that have already closed. | |
first_period_mode | prorate | full | How the period containing effective_at is treated. Omitted inherits the structure's proration_mode. Ignored when effective_at is already a period boundary, since the period is whole either way. |
first_period_mode means different things by criteria type. For recurring and goals it selects how the amount is prorated. For cpm there is no amount to prorate, so it moves the start of the window instead: prorate counts only videos posted, and views gained, at or after effective_at; full counts every video posted since the period started. That start holds for every later period.
# Start on next Monday, taking that period whole
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{
"creator_ids": ["<uuid1>"],
"effective_at": "2026-08-24T05:00:00Z",
"first_period_mode": "full"
}' \
https://www.trackagoat.com/api/v2/payout-structures/<uuid>/assignmentsReturns { assigned, already_assigned, not_in_project, results }.
DELETE/api/v2/payout-structures//assignments#
Bulk-unassign creators. Existing earned accruals remain payable.
curl -X DELETE \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{"creator_ids": ["<uuid1>"]}' \
https://www.trackagoat.com/api/v2/payout-structures/<uuid>/assignmentsPayout Accruals#
Accruals are computed by an hourly background job: they cannot be created via the API.
GET/api/v2/payout-accruals#
List accruals with optional filters.
curl -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/payout-accruals?project_id=<uuid>&status=earned"Query parameters
| Parameter | Type | Description |
|---|---|---|
project_id | uuid | Filter to a project |
creator_id | uuid | Filter to a creator |
structure_id | uuid | Filter to a structure |
status | earned | paid | rejected | voided | Filter by status |
from | YYYY-MM-DD | Period start on or after this date |
to | YYYY-MM-DD | Period end on or before this date |
limit | number | Default 50, max 100 |
cursor | string | Pagination cursor (period_end_at ISO) |
GET/api/v2/payout-accruals/#
Fetch a single accrual including inputs_snapshot.
For a CPM accrual, inputs_snapshot.video_gains lists each video that earned in the period as { video_id, views_start, views_end, gained }: the view count it was already paid up to, the count this accrual paid up to, and the difference. The next period pays from views_end. Rejecting or voiding a CPM accrual does not hand its views to a later period. Accruals calculated before 2026-10-09 have source: "posts_in_period" and no video_gains.
PATCH/api/v2/payout-accruals/#
Manually reject or void an earned accrual.
curl -X PATCH \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{"status": "rejected"}' \
https://www.trackagoat.com/api/v2/payout-accruals/<uuid>Body: { status: "rejected" | "voided" }
Returns 409 cannot_set_status_on_paid_accrual if the accrual is already paid: void the parent payment first.
Payout Payments#
GET/api/v2/payout-payments#
List payments with optional filters.
curl -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/payout-payments?project_id=<uuid>&status=paid"Query parameters
| Parameter | Type | Description |
|---|---|---|
project_id | uuid | Filter to a project |
creator_id | uuid | Filter to a creator |
status | paid | rejected | voided | Filter by status |
from | YYYY-MM-DD | Paid at or after this date |
to | YYYY-MM-DD | Paid at or before this date |
limit | number | Default 50, max 100 |
cursor | string | Pagination cursor (paid_at ISO) |
Response rows include payout_payment_lines[] (linked accruals) and payout_adjustments[] (over/underpayment lines).
POST/api/v2/payout-payments#
Record a payment. Requires an Idempotency-Key header.
# Settle all outstanding accruals for a creator
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"project_id": "<uuid>",
"creator_id": "<uuid>",
"method_id": "<uuid>",
"total_amount_cents": 15000,
"currency": "USD",
"auto_apply_outstanding": true,
"note": "April batch payment"
}' \
https://www.trackagoat.com/api/v2/payout-payments
# Ad-hoc payment with no linked accruals
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"project_id": "<uuid>",
"creator_id": "<uuid>",
"total_amount_cents": 5000,
"note": "Bonus for viral video"
}' \
https://www.trackagoat.com/api/v2/payout-paymentsBody
| Field | Type | Required | Description |
|---|---|---|---|
project_id | uuid | ✓ | Target project |
creator_id | uuid | null | Creator receiving payment (null for true ad-hoc). Required when accrual_ids or auto_apply_outstanding is set | |
method_id | uuid | null | Payout method used | |
total_amount_cents | number | ✓ | Total paid in cents (≥ 0) |
currency | string | ISO 4217, default USD | |
tracking_url | string | null | External receipt link | |
note | string | null | Free-form note | |
paid_at | ISO 8601 | When the payment was sent. Defaults to now; may be backdated to record a historical payment, but cannot be in the future | |
accrual_ids | uuid[] | Explicit accruals to settle (takes precedence over auto_apply_outstanding). Every accrual must belong to creator_id, otherwise 409 accrual_creator_mismatch | |
supplemental_ids | uuid[] | Owed supplemental amounts to settle with this payment. They count toward the amount the payment applies, like accruals. Each must belong to creator_id (409 supplemental_creator_mismatch) and be owed in this project and currency (409 supplemental_not_settleable) | |
auto_apply_outstanding | boolean | Settles all of the creator's earned accruals and owed supplemental amounts in this project and currency (when accrual_ids and supplemental_ids are empty). Requires creator_id. Returns 409 nothing_outstanding and records nothing when there are none | |
adjustment_reason | string | null | Required when total differs from accrual sum | |
metadata | object | Agent-writable data | |
readme | string | null | Markdown notes | |
payment_id | uuid | Your own id for the payment. Required with attachment_path, because the receipt is uploaded under it before the payment exists. Returns 409 payment_id_in_use if another payment has it | |
attachment_path | string | A receipt's path from Payment receipts. Must be in this payment_id's folder, otherwise 400 invalid_attachment_path |
Returns 201 with the payment row, or 200 with the original payment when the Idempotency-Key was already used.
GET/api/v2/payout-payments/#
Fetch a payment including lines, adjustments and the supplemental amounts it settled (payout_supplemental_amounts).
Send a payout through a connected provider#
POST /api/v2/payout-payments/send pays a creator through the organization's connected provider (Dots) and records the payment. POST /api/v2/payout-payments only records a payment made somewhere else; this endpoint moves money.
It works only when all of these hold, otherwise nothing is sent:
- the key has the
writescope and acts as an org admin (403 insufficient_role); - an org admin turned on Agent payments in Org settings → Payment providers (
403 agent_payments_disabled); - the amount fits the org's agent limits (
403 agent_cap_exceeded, withmeta.cap=per_paymentordailyandmeta.limit_cents); - the creator is linked to a payee (
409 payee_not_linked; check withGET /creators/{id}/payee-links); - an
Idempotency-Keyheader is sent. Reusing it returns the original payment and never pays twice.
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"project_id": "<uuid>",
"creator_id": "<uuid>",
"auto_apply_outstanding": true,
"provider_options": { "memo": "September CPM payout" }
}' \
https://www.trackagoat.com/api/v2/payout-payments/sendBody
| Field | Type | Required | Description |
|---|---|---|---|
project_id | uuid | ✓ | Project the accruals belong to |
creator_id | uuid | ✓ | Creator to pay |
provider | string | dots (default) | |
accrual_ids | uuid[] | Earned accruals to pay | |
supplemental_ids | uuid[] | Owed supplemental amounts to pay with them | |
auto_apply_outstanding | boolean | Pay everything the creator is owed in this project and currency: earned accruals and owed supplemental amounts. 409 nothing_outstanding if there is nothing | |
total_amount_cents | integer | Amount to send (≥ 1). Defaults to the total of what is being paid | |
adjustment_reason | string | Required when total_amount_cents differs from the accruals' total | |
currency | string | USD (Dots pays USD only) | |
note | string | Internal note on the trackagoat payment | |
provider_options | object | Provider options, below |
Dots provider_options
| Field | Type | Description |
|---|---|---|
memo | string (≤ 500) | Note shown to the creator with the payout |
message | string (≤ 300) | Text of the SMS Dots sends with a claim link (creators linked by phone) |
description | string (≤ 500) | Description saved on the payout in Dots |
tax_exempt | boolean | Leave this payout out of the creator's 1099 |
Unknown fields are rejected with 400 invalid_provider_options.
Response. 201 with the payment row. Provider fields:
| Field | Description |
|---|---|
provider | dots |
provider_status | completed, awaiting_claim (creator was texted a claim link), processing, unconfirmed (Dots didn't answer in time; trackagoat keeps checking: don't send again with a new key), failed or canceled |
provider_payment_id | Dots payout link id |
provider_error_code, provider_error_message | Why a payout failed, or a note such as "Waiting for funds in your Dots balance" |
provider_options | The options sent |
initiated_via | ui, api or mcp |
When Dots refuses the payout, the response is 422 provider_send_failed with the payment in data: it is already voided, so its accruals are owed again, and provider_error_code says why (for example transaction-insufficient-funds). Retrying needs a new Idempotency-Key.
failed and canceled payments are always voided. Read the status later with GET /api/v2/payout-payments/{id}. A payment that is awaiting_claim or still in flight can't be voided (409 provider_cancel_required / 409 provider_in_flight); cancel an unclaimed payout from the creator's Payouts tab.
GET/api/v2/creators//payee-links#
Read scope and an org admin (403 insufficient_role otherwise), like the creator's payout account in the app. Whether a creator can be paid through each connected provider, and who they are paid as (phone masked). meta carries the org's agent-payment switch and limits.
{
"data": [
{
"provider": "dots",
"provider_name": "Dots",
"environment": "production",
"connection_status": "active",
"linked": true,
"payee": { "provider_payee_id": "a1b2…", "name": "Sarah Brown", "phone": "+1 ••• 4567", "source": "claimed", "linked_at": "2026-10-09T18:22:04Z" }
}
],
"meta": { "agent_payments_enabled": true, "agent_payment_max_cents": 50000, "agent_payment_daily_max_cents": 200000 }
}PATCH/api/v2/payout-payments/#
Void a payment or update metadata/readme.
# Void: reverts linked accruals to "earned" and settled supplemental amounts to "owed"
curl -X PATCH \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{"status": "voided"}' \
https://www.trackagoat.com/api/v2/payout-payments/<uuid>Body: { status?: "voided", metadata?: object, readme?: string | null } (at least one field required). A receipt can only be attached when the payment is recorded, as in the app.
Payouts tracker#
GET/api/v2/payouts/tracker#
Per-creator balances for a project, as on the app's Payouts page. Query: project_id (required), creator_ids (comma-separated), structure_id, campaign_id, from, to (YYYY-MM-DD, org time zone), outstanding_only (default false), include_removed (default false), limit (1 to 200, default 50), cursor (meta.nextCursor). Largest outstanding first.
Each row has total_earned_cents, total_paid_cents, adjustment_cents (signed: the part of paid that settled no accrual), adjustment_breakdown ([{ reason, amount_cents, count }], largest first), outstanding_cents, last_paid_at, is_removed and counts. meta.totals has earned_cents, paid_cents, adjustment_cents, adjustment_breakdown, outstanding_cents, creator_count_outstanding and the removed-creator summary (removed_creator_count_outstanding, removed_outstanding_cents). Without from/to, every row and the totals satisfy earned − paid + adjustments = outstanding. MCP: get_payouts_tracker.
Supplemental amounts#
A supplemental amount is an owed correction on top of a paid accrual, for example posts found after the period was paid. The accrual and its payment are never changed. The amount is owed, counts in the creator's outstanding balance, and is settled by recording a payment (supplemental_ids or auto_apply_outstanding). MCP: list_payout_supplementals, create_payout_supplemental, void_payout_supplemental.
GET/api/v2/payout-supplementals#
Query: project_id, creator_id, structure_id, accrual_id, status (owed | paid | void), cursor, limit (1 to 100, default 50). Newest first.
POST/api/v2/payout-supplementals#
Requires the write scope. Send an Idempotency-Key header: a retry with the same key returns the original row.
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1d0c1e-…" \
-d '{"project_id": "<uuid>", "accrual_id": "<paid accrual uuid>", "amount_cents": 1333, "reason": "Posts found after the week was paid"}' \
https://www.trackagoat.com/api/v2/payout-supplementals| Field | Type | Required | Description |
|---|---|---|---|
project_id | uuid | ✓ | Project |
accrual_id | uuid | The paid accrual this corrects; its creator, structure and period are used. An unpaid accrual returns 409 accrual_not_paid | |
creator_id, structure_id, period_start_at, period_end_at | Instead of accrual_id: all four are required | ||
amount_cents | integer | ✓ | Amount owed, ≥ 1 |
currency | string | Default USD; must match the accrual | |
reason | string | ✓ | 1 to 500 characters; shown as "Supplemental: <reason>" |
Returns 201 with the row (status: "owed"). A removed creator returns 404 not_found: restore the creator first.
GET/api/v2/payout-supplementals/#
Fetch one supplemental amount.
PATCH/api/v2/payout-supplementals/#
Void an owed amount: { "status": "void", "reason"?: string }. A paid one returns 409 supplemental_paid: void the payment that settled it instead (it becomes owed again).
Payment receipts#
A receipt is a file attached to a payment: PNG, JPEG, WebP, PDF, CSV, Word or Excel, up to 5 MB, stored privately. Generate a payment_id UUID first, upload the receipt under it, then record the payment with the same payment_id and attachment_path.
Inline, up to 3 MB. POST /api/v2/payouts/payment-receipts:
PAYMENT_ID=$(uuidgen | tr 'A-Z' 'a-z')
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d "{\"payment_id\": \"$PAYMENT_ID\", \"project_id\": \"<project>\", \"filename\": \"receipt.pdf\", \"content_type\": \"application/pdf\", \"content_base64\": \"$(base64 < receipt.pdf | tr -d '\n')\"}" \
https://www.trackagoat.com/api/v2/payouts/payment-receiptsReturns 201 with { path, content_type, size_bytes }. The file's contents must match content_type (content_type_mismatch); larger files get 413 file_too_large.
Signed URL, up to 5 MB. POST /api/v2/payouts/payment-receipts/upload-url with { payment_id, project_id, filename, content_type } returns signed_url, path, max_bytes and expires_in_seconds (2 hours). PUT the raw file to signed_url with the same Content-Type:
curl -X PUT -H "Content-Type: application/pdf" --data-binary @receipt.pdf "<signed_url>"Then record the payment with "payment_id": "$PAYMENT_ID" and "attachment_path": "<path>".
Error codes#
| Code | HTTP | Description |
|---|---|---|
invalid_api_key | 401 | Missing, expired, or invalid Bearer token |
insufficient_scope | 403 | Write attempted with a key that lacks the write scope |
rate_limit_exceeded | 429 | Daily API quota or per-minute (60 req/min) rate limit hit |
limit_reached | 402 | Org plan limit for methods or structures |
invalid_body | 400 | Zod validation failed: see meta.errors |
invalid_query | 400 | Bad query parameter |
default_method | 409 | Cannot delete a seeded default method |
conflict_name | 409 | Method name already exists in org |
structure_archived | 409 | Cannot assign creators to an archived structure |
has_accruals | 409 | Cannot hard-delete a structure with existing accruals |
cannot_set_paid_directly | 400 | Use POST /payout-payments instead |
cannot_set_status_on_paid_accrual | 409 | Void the parent payment first |
currency_mismatch | 400 | Payment currency doesn't match accrual currency |
accrual_not_settleable | 409 | Accrual is wrong project, status, or currency |
accrual_creator_mismatch | 409 | An accrual belongs to a different creator than the payment's creator_id |
nothing_outstanding | 409 | auto_apply_outstanding found no earned accruals; nothing was recorded |
idempotency_key_required | 400 | POST /payout-payments was sent without an Idempotency-Key header |
idempotency_conflict | 409 | Idempotency-Key already used for a payment in another project |
structure_shape_locked | 409 | Rule edit changes period or criteria_type on a structure that has accruals or assignments: create a new structure instead |
adjustment_reason_required | 400 | Total differs from accrual sum: provide adjustment_reason |
already_voided | 400 | Payment is already voided |
accrual_in_other_payment | 409 | Accrual has been re-paid: void that payment first |
payment_id_in_use | 409 | payment_id already belongs to another payment |
invalid_attachment_path | 400 | attachment_path isn't a receipt uploaded for this payment_id |
invalid_mime_type | 400 | Upload type not allowed (see meta.allowed) |
content_type_mismatch | 400 | Uploaded file's contents don't match content_type |
invalid_base64 | 400 | content_base64 isn't valid base64 |
file_too_large | 413 | Inline upload over 3 MB (receipts) or 1 MB (icons) |
agent_payments_disabled | 403 | An org admin hasn't turned on agent payments |
agent_cap_exceeded | 403 | The payout is over the org's per-payout or 24-hour agent limit |
insufficient_role | 403 | The key acts as a member who isn't an org admin: payout setup, recording or sending payments, method icons and payout accounts need one |
provider_not_connected | 409 | No payment provider is connected |
provider_connection_error | 409 | The provider rejected the stored keys; an admin must update them |
payee_not_linked | 409 | The creator isn't linked to a payee |
invalid_provider_options | 400 | provider_options has an unknown or too-long field |
provider_send_failed | 422 | The provider refused the payout; the payment was voided |
provider_cancel_required | 409 | The payout is awaiting claim: cancel it instead of voiding |
provider_in_flight | 409 | The provider hasn't settled the payout yet |
not_found | 404 | Resource not found or not in org |