Payouts
Pay an invoice — fiat payouts funded with crypto.
Jump to an operation:
- GET /payouts
- POST /payouts
- GET /payouts/eligibility
- GET /payouts/{id}
- POST /payouts/{id}/beneficiary
- GET /payouts/{id}/funding_instructions
- POST /payouts/{id}/funding_instructions
- GET /payouts/{id}/wallet_funding_quote
- GET /payouts/{id}/receipt
- GET /payouts/{id}/attempts
- GET /payouts/{id}/events
- POST /payouts/{id}/cancel
- POST /payouts/{id}/mark_sent
- POST /payouts/{id}/replace
List payouts — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.read· Test mode: unavailable
List the caller's payouts, newest first. status_group mirrors the dashboard's own bucketing (needs_you = draft ∪ awaiting_funds ∪ paid_with_shortfall ∪ needs_attention:true; in_progress = funds_received ∪ processing ∪ paid; done = settled ∪ failed ∪ returned ∪ expired ∪ cancelled; RESOURCE-MODEL §2.2 v2 amendments). Do not promise a query by date, status or recipient beyond this grouping.
query Parameters
limitDefault 25. Out of range is never rejected: a value outside 1..100, a fraction, or a non-number is not a 400 — 0, a negative number, a non-integer or anything non-numeric falls back to the default (25); anything above 100 is capped to 100. A documented clamp, not a silent one; minimum/maximum are deliberately absent from this schema so a generated client (the MCP tool included) does not reject a value the API itself accepts.
cursorOpaque cursor from a previous response's next_cursor.
status_groupThis resource's own bucket set (needs_you, in_progress, done), shared with payroll runs and orders only.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
List payouts — available › Responses
A page of payouts.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
Create a draft payout — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.write· Test mode: unavailable
Creates a draft payout for an external invoice. Nothing is charged and no money moves — that happens later, at POST /v1/payouts/{id}/funding_instructions. Two different things are stamped onto the payout at this moment as capability_snapshot (RESOURCE-MODEL §2.2 v2 amendments, K4c): the capability-contract version, and the corridor's sender identity (sender_display/legal_entity_name) — neither is re-resolved later even if the corridor's terms or the customer's account naming change (see PayoutCapabilitySnapshot's own description for why). eta_seconds/minimum are the OPPOSITE: presentation-only facts resolved fresh from the LIVE catalog on every read, never frozen. Refused with 409 capability_unavailable when corridor_id is not currently executable — do not call this before GET /v1/capabilities?product=payouts (or list_capabilities) reports the corridor available, and do not retry on that error. payer_type is optional: Swaps derives it (business only when the account and the payer's Bridge customer are both business) and refuses a different value with 422 payer_type_mismatch. source_chain is optional too: a draft may be created before the payer picks a chain (source_chain_chosen: false); the chain is then sent when funding.
Headers
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Create a draft payout — available › Request Body
A Money value that must be strictly positive — used on every request field that creates or moves value.
corridor_idOne of GET /v1/capabilities?product=payouts corridors[].id. Refused with a 409 capability_unavailable if the corridor is not currently executable.
source_chainOptional. The USDC chain the payer funds from, when already chosen; it must be one of the corridor's source_chains (otherwise 409 capability_unavailable, nothing created). Leave it out until the payer picks: the draft then has no chain (source_chain_chosen: false) and the chain is given when funding.
payer_typeOptional. Swaps derives the payer type from the account the request acts for (Swaps-Account, or the key's account) and the payer's provider customer: business only when both are business. Leave it out. An equal value is accepted; a different one is refused with 422 payer_type_mismatch (details.expected, details.received) and nothing is created.
noterecipient_emailRequired if notify_recipient is true.
notify_recipientEmail recipient_email a payment confirmation once, when the payment lands.
Create a draft payout — available › Responses
The new draft payout.
id^po_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atstatusThe 11 payout states (RESOURCE-MODEL §2.2). paid is not terminal — the ladder's final rung stays active until settled. settled is the recipient paid the invoice exactly, or above it by at most 0.05 % of the invoice plus 10 minor units (25 for MXN) — the funding buffer's over-delivery; settled_amount is what was actually paid. paid_with_shortfall is terminal: it produces no receipt, no success notification, and never becomes settled. A1-2: open — an unrecognized member is an opaque string, never a deserialization failure.
The invoice amount the recipient is owed, in fiat.
corridor_idThe corridor this payout settles through, as listed by GET /v1/capabilities?product=payouts. Known values today (RESOURCE-MODEL §3 vocabulary): usd_ach, usd_wire, eur_sepa, gbp_faster_payments, brl_pix, mxn_spei, cop_co_bank_transfer (lifecycle blocked — never actually payable).
fiat_railThe settlement rail underlying corridor_id.
source_chainThe chain the payer funds from. Read it together with source_chain_chosen: while that is false, no chain has been chosen yet and this is only the default (base), never a choice. The stored payout.* event payload (GET /v1/events, the SSE stream, webhooks, GET /v1/activity) carries the stored chain instead: null for a draft with no chain, and no source_chain_chosen.
source_chain_chosenTrue once a chain is fixed for this payout: sent as source_chain at create, or fixed at funding (POST /v1/payouts/{id}/funding_instructions, locked once the provider transfer exists). False on a draft created without source_chain; source_chain then shows the default.
source_assetThe stablecoin the payer funds with. Only USDC is live today.
payer_typeSet by Swaps at creation, never from the request: business only when the account the payout was created under and the payer's provider customer were both business. The individual per-payout limit is checked at funding against the account's type at that moment and the fresh provider customer, not against this stored value. A distinct axis from beneficiary.beneficiary_owner_type.
fee_bpsThe Swaps fee, in basis points, on the gross source amount. Today always 100 (1%). A stored value that disagrees with the current fee contract is refused at fund (RESOURCE-MODEL §2.2 invariants).
notify_recipientWhether recipient_email is emailed a payment confirmation once the payout lands.
v1 reality (K4c, §52 C4-D12): eta_seconds/minimum are resolved from the LIVE corridor catalog at read time, keyed by the payout's own stored capability_id; version is the capability-contract version stamped at creation, not re-resolved — both are presentation-only facts (no SLA, no legal claim) that are safe to re-derive live. sender_display/legal_entity_name are the OPPOSITE: FROZEN at payout creation (or replacement — a payouts.replace draft gets its own fresh resolution too, never a copy of the payout it replaces) and read back from the row, never re-derived from the live catalog. A payout created on or after this snapshot's migration always carries one — sender_display is set even when the resolver could not identify a specific sender (the literal value unknown, never omitted in that case). A payout created BEFORE the migration carries no snapshot at all: sender_display is omitted entirely and legal_entity_name is null, exactly as this endpoint behaved before the freeze existed — nothing proves what that payout was actually sent as, so nothing is invented for it. The freeze exists precisely because the catalog's answer for a corridor can change for reasons that have nothing to do with one specific payout (e.g. Bridge moved eur_sepa from bridge to customer on 2026-09-02, D-111): a live-resolved value here would let a payout created before that date wrongly claim "customer" on a statement it never actually carried that name on. Resolution reads the provider's account record AS CACHED AT THE MOMENT OF FREEZE — not a live probe — so a rename on Bridge's side after creation never changes an already-frozen payout's answer; a client rendering bridge should pair it with a tip that the statement name changes once the account is renamed. A client that wants the corridor's CURRENT terms (for a payout not yet created) calls GET /v1/capabilities?product=payouts instead. Consumer contract: prefer this snapshot first and fall back to a live capability read ONLY when it is absent (a legacy pre-freeze payout) — never the reverse.
needs_attentionRouter-derived from the internal reconciliation_status signal, which stays internal (RESOURCE-MODEL D-8, resolved). True renders as "Checking settlement" over whatever status currently reads, most commonly over paid.
updated_atThe confirmed amount actually settled to the recipient — the invoice, or up to 0.05 % of the invoice plus 10 minor units (25 for MXN) above it when the funding buffer over-delivers. Null until settled.
The confirmed amount the provider actually paid out. Set alongside invoice_shortfall_amount on paid_with_shortfall; null otherwise.
invoice_amount minus provider_paid_amount on a paid_with_shortfall payout. Null on every other status.
noteFree-text note the payer attached at creation.
recipient_emailThe recipient's email, when the payer chose to notify them (RESOURCE-MODEL §2.2 v2 amendments — added create + read).
The masked beneficiary, once added. Null on a draft with no beneficiary yet.
Set on failed and returned; also set to missing_return_policy while Bridge holds the deposit with no address to return it to (processing, needs_attention).
funded_atWhen the funding provider transfer was created. Null before funding.
paid_atWhen the provider confirmed payout to the recipient's bank.
settled_atWhen the payout reached its terminal settled state.
Read the holder's payout eligibility — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.read· Test mode: unavailable
The holder's KYC status and, per corridor, whether a payout can be created and funded right now, with the blocker named when it cannot. This is availability, not authorization: the server re-checks at create and again at the funding money boundary (RESOURCE-MODEL §0.10). Treat in_review and gathering_no_path as different answers — never tell someone to submit a form for the second.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read the holder's payout eligibility — available › Responses
The holder's payout eligibility.
The same fee/corridor contract as GET /v1/capabilities?product=payouts — not decomposed further here; read that resource for the full shape.
Get a payout — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.read· Test mode: unavailable
Return one payout the caller owns. A payout paid with a shortfall is terminal, has no receipt by design, and never becomes settled on its own — read provider_paid_amount/invoice_shortfall_amount rather than waiting for a receipt. Cross-account ids answer 404 not_found, never 403.
path Parameters
id^po_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Get a payout — available › Responses
The payout.
id^po_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atstatusThe 11 payout states (RESOURCE-MODEL §2.2). paid is not terminal — the ladder's final rung stays active until settled. settled is the recipient paid the invoice exactly, or above it by at most 0.05 % of the invoice plus 10 minor units (25 for MXN) — the funding buffer's over-delivery; settled_amount is what was actually paid. paid_with_shortfall is terminal: it produces no receipt, no success notification, and never becomes settled. A1-2: open — an unrecognized member is an opaque string, never a deserialization failure.
The invoice amount the recipient is owed, in fiat.
corridor_idThe corridor this payout settles through, as listed by GET /v1/capabilities?product=payouts. Known values today (RESOURCE-MODEL §3 vocabulary): usd_ach, usd_wire, eur_sepa, gbp_faster_payments, brl_pix, mxn_spei, cop_co_bank_transfer (lifecycle blocked — never actually payable).
fiat_railThe settlement rail underlying corridor_id.
source_chainThe chain the payer funds from. Read it together with source_chain_chosen: while that is false, no chain has been chosen yet and this is only the default (base), never a choice. The stored payout.* event payload (GET /v1/events, the SSE stream, webhooks, GET /v1/activity) carries the stored chain instead: null for a draft with no chain, and no source_chain_chosen.
source_chain_chosenTrue once a chain is fixed for this payout: sent as source_chain at create, or fixed at funding (POST /v1/payouts/{id}/funding_instructions, locked once the provider transfer exists). False on a draft created without source_chain; source_chain then shows the default.
source_assetThe stablecoin the payer funds with. Only USDC is live today.
payer_typeSet by Swaps at creation, never from the request: business only when the account the payout was created under and the payer's provider customer were both business. The individual per-payout limit is checked at funding against the account's type at that moment and the fresh provider customer, not against this stored value. A distinct axis from beneficiary.beneficiary_owner_type.
fee_bpsThe Swaps fee, in basis points, on the gross source amount. Today always 100 (1%). A stored value that disagrees with the current fee contract is refused at fund (RESOURCE-MODEL §2.2 invariants).
notify_recipientWhether recipient_email is emailed a payment confirmation once the payout lands.
v1 reality (K4c, §52 C4-D12): eta_seconds/minimum are resolved from the LIVE corridor catalog at read time, keyed by the payout's own stored capability_id; version is the capability-contract version stamped at creation, not re-resolved — both are presentation-only facts (no SLA, no legal claim) that are safe to re-derive live. sender_display/legal_entity_name are the OPPOSITE: FROZEN at payout creation (or replacement — a payouts.replace draft gets its own fresh resolution too, never a copy of the payout it replaces) and read back from the row, never re-derived from the live catalog. A payout created on or after this snapshot's migration always carries one — sender_display is set even when the resolver could not identify a specific sender (the literal value unknown, never omitted in that case). A payout created BEFORE the migration carries no snapshot at all: sender_display is omitted entirely and legal_entity_name is null, exactly as this endpoint behaved before the freeze existed — nothing proves what that payout was actually sent as, so nothing is invented for it. The freeze exists precisely because the catalog's answer for a corridor can change for reasons that have nothing to do with one specific payout (e.g. Bridge moved eur_sepa from bridge to customer on 2026-09-02, D-111): a live-resolved value here would let a payout created before that date wrongly claim "customer" on a statement it never actually carried that name on. Resolution reads the provider's account record AS CACHED AT THE MOMENT OF FREEZE — not a live probe — so a rename on Bridge's side after creation never changes an already-frozen payout's answer; a client rendering bridge should pair it with a tip that the statement name changes once the account is renamed. A client that wants the corridor's CURRENT terms (for a payout not yet created) calls GET /v1/capabilities?product=payouts instead. Consumer contract: prefer this snapshot first and fall back to a live capability read ONLY when it is absent (a legacy pre-freeze payout) — never the reverse.
needs_attentionRouter-derived from the internal reconciliation_status signal, which stays internal (RESOURCE-MODEL D-8, resolved). True renders as "Checking settlement" over whatever status currently reads, most commonly over paid.
updated_atThe confirmed amount actually settled to the recipient — the invoice, or up to 0.05 % of the invoice plus 10 minor units (25 for MXN) above it when the funding buffer over-delivers. Null until settled.
The confirmed amount the provider actually paid out. Set alongside invoice_shortfall_amount on paid_with_shortfall; null otherwise.
invoice_amount minus provider_paid_amount on a paid_with_shortfall payout. Null on every other status.
noteFree-text note the payer attached at creation.
recipient_emailThe recipient's email, when the payer chose to notify them (RESOURCE-MODEL §2.2 v2 amendments — added create + read).
The masked beneficiary, once added. Null on a draft with no beneficiary yet.
Set on failed and returned; also set to missing_return_policy while Bridge holds the deposit with no address to return it to (processing, needs_attention).
funded_atWhen the funding provider transfer was created. Null before funding.
paid_atWhen the provider confirmed payout to the recipient's bank.
settled_atWhen the payout reached its terminal settled state.
Add the beneficiary to a draft payout — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.write· Test mode: unavailable · Money boundary: per-transaction human confirmation required
Adds the destination bank account to a draft payout — once, ever; there is no update and no second call (RESOURCE-MODEL §2.2). Raw bank details cross this boundary once and are then held only by the provider, so this operation is excluded from MCP entirely — no generated tool can take an IBAN or account number as an argument, because most MCP clients log tool arguments verbatim (D-6). Structurally REST-only: business key or agent over HTTPS, never a tool call.
path Parameters
id^po_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Add the beneficiary to a draft payout — available › Request Body
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object · rail="sepa" · requires: account_owner_type, account_holder, address +2 more | |
| type = object · requires: account_owner_type, account_holder, address +3 more | |
| type = object · rail="faster_payments" · requires: account_owner_type, account_holder, address +2 more | |
| type = object · rail="pix" · requires: account_owner_type, account_holder, address +2 more | |
| type = object · rail="spei" · requires: account_owner_type, account_holder, address +1 more |
railaccount_owner_typeWho owns the destination bank account.
account_holderFull name on the destination bank account.
The beneficiary's address (docs/api/consumers/R12-pay-invoice.md §2 PI-G11). state_region is collected only for US rails, and is OPTIONAL there as well as everywhere else — the required list below omits it; a US beneficiary with no state supplied is not rejected for that alone. (Corrected 2026-09-07: the schema, the route's own optionalAddress and the approved pi-recipient-usd screen already agreed on this; this description previously said "required" and was the one outlier — confirm with Bridge whether that was ever actually enforced before tightening either side.)
ibanbicAdd the beneficiary to a draft payout — available › Responses
The payout, now carrying its masked beneficiary.
id^po_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atstatusThe 11 payout states (RESOURCE-MODEL §2.2). paid is not terminal — the ladder's final rung stays active until settled. settled is the recipient paid the invoice exactly, or above it by at most 0.05 % of the invoice plus 10 minor units (25 for MXN) — the funding buffer's over-delivery; settled_amount is what was actually paid. paid_with_shortfall is terminal: it produces no receipt, no success notification, and never becomes settled. A1-2: open — an unrecognized member is an opaque string, never a deserialization failure.
The invoice amount the recipient is owed, in fiat.
corridor_idThe corridor this payout settles through, as listed by GET /v1/capabilities?product=payouts. Known values today (RESOURCE-MODEL §3 vocabulary): usd_ach, usd_wire, eur_sepa, gbp_faster_payments, brl_pix, mxn_spei, cop_co_bank_transfer (lifecycle blocked — never actually payable).
fiat_railThe settlement rail underlying corridor_id.
source_chainThe chain the payer funds from. Read it together with source_chain_chosen: while that is false, no chain has been chosen yet and this is only the default (base), never a choice. The stored payout.* event payload (GET /v1/events, the SSE stream, webhooks, GET /v1/activity) carries the stored chain instead: null for a draft with no chain, and no source_chain_chosen.
source_chain_chosenTrue once a chain is fixed for this payout: sent as source_chain at create, or fixed at funding (POST /v1/payouts/{id}/funding_instructions, locked once the provider transfer exists). False on a draft created without source_chain; source_chain then shows the default.
source_assetThe stablecoin the payer funds with. Only USDC is live today.
payer_typeSet by Swaps at creation, never from the request: business only when the account the payout was created under and the payer's provider customer were both business. The individual per-payout limit is checked at funding against the account's type at that moment and the fresh provider customer, not against this stored value. A distinct axis from beneficiary.beneficiary_owner_type.
fee_bpsThe Swaps fee, in basis points, on the gross source amount. Today always 100 (1%). A stored value that disagrees with the current fee contract is refused at fund (RESOURCE-MODEL §2.2 invariants).
notify_recipientWhether recipient_email is emailed a payment confirmation once the payout lands.
v1 reality (K4c, §52 C4-D12): eta_seconds/minimum are resolved from the LIVE corridor catalog at read time, keyed by the payout's own stored capability_id; version is the capability-contract version stamped at creation, not re-resolved — both are presentation-only facts (no SLA, no legal claim) that are safe to re-derive live. sender_display/legal_entity_name are the OPPOSITE: FROZEN at payout creation (or replacement — a payouts.replace draft gets its own fresh resolution too, never a copy of the payout it replaces) and read back from the row, never re-derived from the live catalog. A payout created on or after this snapshot's migration always carries one — sender_display is set even when the resolver could not identify a specific sender (the literal value unknown, never omitted in that case). A payout created BEFORE the migration carries no snapshot at all: sender_display is omitted entirely and legal_entity_name is null, exactly as this endpoint behaved before the freeze existed — nothing proves what that payout was actually sent as, so nothing is invented for it. The freeze exists precisely because the catalog's answer for a corridor can change for reasons that have nothing to do with one specific payout (e.g. Bridge moved eur_sepa from bridge to customer on 2026-09-02, D-111): a live-resolved value here would let a payout created before that date wrongly claim "customer" on a statement it never actually carried that name on. Resolution reads the provider's account record AS CACHED AT THE MOMENT OF FREEZE — not a live probe — so a rename on Bridge's side after creation never changes an already-frozen payout's answer; a client rendering bridge should pair it with a tip that the statement name changes once the account is renamed. A client that wants the corridor's CURRENT terms (for a payout not yet created) calls GET /v1/capabilities?product=payouts instead. Consumer contract: prefer this snapshot first and fall back to a live capability read ONLY when it is absent (a legacy pre-freeze payout) — never the reverse.
needs_attentionRouter-derived from the internal reconciliation_status signal, which stays internal (RESOURCE-MODEL D-8, resolved). True renders as "Checking settlement" over whatever status currently reads, most commonly over paid.
updated_atThe confirmed amount actually settled to the recipient — the invoice, or up to 0.05 % of the invoice plus 10 minor units (25 for MXN) above it when the funding buffer over-delivers. Null until settled.
The confirmed amount the provider actually paid out. Set alongside invoice_shortfall_amount on paid_with_shortfall; null otherwise.
invoice_amount minus provider_paid_amount on a paid_with_shortfall payout. Null on every other status.
noteFree-text note the payer attached at creation.
recipient_emailThe recipient's email, when the payer chose to notify them (RESOURCE-MODEL §2.2 v2 amendments — added create + read).
The masked beneficiary, once added. Null on a draft with no beneficiary yet.
Set on failed and returned; also set to missing_return_policy while Bridge holds the deposit with no address to return it to (processing, needs_attention).
funded_atWhen the funding provider transfer was created. Null before funding.
paid_atWhen the provider confirmed payout to the recipient's bank.
settled_atWhen the payout reached its terminal settled state.
Read the current funding instructions — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.read· Test mode: unavailable · Money boundary: per-transaction human confirmation required
Read the funding instructions already created for this payout, without side effects. 404 not_found is reserved for a payout that does not exist or is not yours — identical body either way, no oracle. A payout that exists and is yours but was never funded (draft, or any other status POST was never called from) answers 409 payout_funding_instructions_not_ready instead, naming the status. To create the provider transfer, or to refresh the estimate, call POST on this same path.
path Parameters
id^po_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read the current funding instructions — available › Responses
The current funding instructions.
deposit_addressCase-preserved verbatim — never lower-cased, never re-cased. Reproduce it exactly as returned.
The source amount to send, in source_asset: the provider's quote rounded UP to the cent plus a buffer of at most 0.05 % (buffer), so the recipient receives the full invoice. A receipt above the invoice within that buffer still settles. Always an estimate, never an exact total — the rate can move before the funds arrive.
The part of amount that is the buffer (at most 0.05 % of the quote rounded up to the cent), published so a screen can state it without computing it. Null on a funding attempt created before the buffer existed.
The estimated Swaps fee on this transfer. Computed server-side as gross = amount × 10000 / (10000 − bps); clients never recompute.
The estimated gross source amount before the Swaps fee. Computed server-side as gross = amount × 10000 / (10000 − bps); clients never recompute.
amount_is_estimateAlways true — the source amount is never an exact total (RESOURCE-MODEL §2.2 invariants).
chaindeposit_messageA memo/tag to attach to the transfer, when the chain requires one; null otherwise.
transfer_referenceA reference the merchant can quote when tracing the fiat leg.
refund_addressThe address on chain where Bridge returns the USDC if this transfer can't be completed, recorded only after Bridge accepted it as the transfer's return address (EIP-55 checksummed). null when none was set: Swaps then has no refund address on file, and a deposit Bridge cannot deliver may wait in missing_return_policy (the payout reads processing with needs_attention).
sandboxPresent, and true, only on a test-mode (livemode: false) response: deposit_address is a provider-sandbox address on chain, not a real one. Never send real funds to it. Absent on every live response.
funding_sourcePresent only when this attempt is funded from the payer's Swaps wallet («Wallet balance»), on every read and fund of the attempt whatever the request body; absent means the payer sends USDC to deposit_address from any wallet («Another wallet»). While present, do not send from another wallet.
With funding_source: swaps_wallet: the unsigned send from the payer's own Tempo wallet to exactly deposit_address on chain. Its amount is what leaves the wallet (every cost of the leg included), amount_out what Relay guarantees to deliver. Sign send_instructions.steps[] with the wallet passkey only in the session payouts.fund handed it to (one session per send, never re-handed), then record the hash from that session through POST /v1/wallet/send_intents/{id}/source_tx. Returned only to a dashboard session; a business key sees funding_source alone.
Create or refresh the funding instructions — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.write· Test mode: unavailable · Money boundary: per-transaction human confirmation required
The money boundary. On the first call, resolves a deposit address and creates the provider transfer; every later call on the same payout is a safe, idempotent refresh that returns the same address with a re-priced estimate — never a new address. Show this to a human and get per-transaction confirmation before anything is sent; never chain it from a quote or call it unattended.
Fund ordering (RESOURCE-MODEL §2.2 invariants — fixed, never reordered): recover an already-succeeded provider operation, if one exists, before doing anything else → re- resolve the corridor from scratch (a capability read taken at create time never authorizes a transfer by itself) → verify the payer against a fresh provider customer read → price the transfer in USD, failing closed if pricing is unavailable → check the per-payer cap → check the Travel-Rule ceiling. Refused with 409 capability_unavailable if the corridor is blocked at any of these checks — do not retry. The cap reads the payer type live (the account and the fresh provider customer), never the stored payer_type; a refresh of a payout stored business re-checks it and answers 403 above it.
The 1% fee law: the Swaps fee is exactly 1% of the gross source amount, computed by dividing — gross = invoice_amount × 10000 / (10000 − fee_bps) — never invoice_amount × 1.01. A stored fee_bps that disagrees with the live fee contract is refused here, at fund, even if it was accepted at create. deposit_address is returned case-preserved, verbatim; amount is always an estimate of the source amount, never an exact total — do not represent it as final. It is the provider quote rounded up to the cent plus a buffer of at most 0.05 % (buffer), so the recipient receives the full invoice; a receipt above the invoice by at most 0.05 % of it plus 10 minor units (25 for MXN) settles (settled_amount = what was paid). paid_with_shortfall is a terminal payout status: it produces no receipt and no success notification, and never becomes settled.
Returns 503 temporarily_unavailable (with Retry-After), not provider_error, when the source-amount quote or the deposit address cannot be resolved right now — this is a transient kill-switch condition, not a permanent block on the corridor.
Optional body (additive; an absent body is the implicit fund above, unchanged). source_chain picks the USDC chain the payer funds from — accepted only while no provider transfer exists yet and only among the corridor's executable source chains; it is persisted in the same transaction as the transfer claim, and any later different value is 409 payout_source_chain_locked. A payout created without source_chain needs it here (or funding_source: swaps_wallet): without one the call is 400 payout_source_chain_missing (param: source_chain) and nothing is changed; Swaps never picks a chain for another wallet. funding_source: swaps_wallet («Wallet balance», behind the payouts_wallet_funding kill switch) runs the same fund ordering, then prepares ONE unsigned, non-custodial Tempo→Relay send from the payer's own Swaps wallet to exactly this deposit address on the chain the server picks (the first of base, arbitrum, ethereum that is both executable for the corridor and open for the wallet): Relay EXACT_OUTPUT for exactly amount (the same quote rounded up to the cent plus the buffer), no Swaps fee on the leg. The response then carries funding_source: swaps_wallet and wallet_send_intent, whose send_instructions.steps[] the holder signs with the wallet passkey and records through POST /v1/wallet/send_intents/{id}/source_tx. Swaps never signs. The send is handed to ONE session: the call claims it for the calling dashboard session (the verified sign-in's session; its status becomes awaiting_signature), so one signature is one transfer. A repeat call from the same session while that send is unsigned and unexpired returns it; another session gets 409 wallet_funding_claimed and no send, with details.retry_after — when a fresh send can be prepared (30 minutes after it expires, if it is never broadcast). A claim is never handed to another session, also after it expires; sign only a send this call handed to your session. Use a fresh random Idempotency-Key per session: a replay (same key and body) returns the stored answer, send included, to any session of the account. Once its source transaction is recorded, or Relay has seen it, the attempt answers 409 wallet_funding_in_flight forever — as it does for 30 minutes after an unsigned send expires (it may still be broadcast) and when the payout is already marked sent with no recorded wallet send. A send Relay refunded to the wallet is superseded by the next call. Nothing is prepared when the wallet balance does not cover the send (409 wallet_balance_short, details.short_by). swaps_wallet is dashboard-only: the wallet's passkey holder signs in a dashboard session, so a business key or an agent is refused with 409 wallet_funding_unavailable (wallet_not_provisioned), and so is a sign-in that carries no session to hand the send to (session_unbound); PAYOUTS_ENABLED off refuses it too (flag_off), because a wallet send is new money. Whatever the body, an attempt funded from the wallet answers with funding_source: swaps_wallet (the unsigned wallet_send_intent only to the dashboard session), also for 30 minutes after its send expires. Without a body or with funding_source: external_wallet, a linked wallet send that was signed or seen by Relay (and not refunded) is 409 wallet_funding_in_flight, and an unsigned one that can still be signed (until 30 minutes after it expires) is 409 wallet_funding_pending with details.retry_after — never a second funding path while the first can still move. While a cancel of the payout is in progress, funding_source: swaps_wallet is 409 payout_not_fundable and no send is handed out. The payout's settlement is still decided only by what the provider pays out.
Refund address («Another wallet» only; PI-REFUND-ADDR-1-T). refund_address is where Bridge returns the USDC if this transfer can't be completed — an address on the funding chain that the payer controls. Swaps vets it before any provider call: EVM only and never the zero address, a mixed-case address must carry a valid EIP-55 checksum (422 payout_refund_address_invalid); not the Swaps fee wallet, nor a deposit address Swaps issued for payouts, payment links, wallet funding, Relay deposits, wallet off-ramps or Bridge transfers (422 payout_refund_address_not_allowed); never a Swaps Wallet address, which exists on Tempo only (422 destination_rail_keyless); evidence that cannot be read refuses (503 destination_risk_unverifiable, nothing changed). With funding_source: swaps_wallet it is 400 payout_refund_address_unsupported. On the first fund it is sent with the transfer as Bridge's return_instructions.address; a retry before the transfer exists must send the same refund_address (or none, if the first call sent none: 409 payout_refund_address_locked otherwise), and if Bridge refuses the address there the transfer is not created and the payout can no longer be funded (422 payout_refund_address_invalid; create a new payout). Once the transfer exists, a new address is sent to Bridge under one claimed provider operation after a fresh read of the transfer, and only while Bridge reports it awaiting_funds (409 payout_refund_address_locked otherwise, nothing stored). The same address again changes nothing. It is recorded, and returned as refund_address, only once Bridge's own response carries it. Without one, Swaps has no refund address on file: a deposit Bridge cannot deliver may then wait in missing_return_policy, where the payout reads processing with needs_attention and failure_code: missing_return_policy.
path Parameters
id^po_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Create or refresh the funding instructions — available › Request Body optional
funding_sourceexternal_wallet (default): send USDC from any wallet. swaps_wallet: fund from Wallet balance.
source_chainThe USDC chain to fund from; only before a provider transfer exists (409 payout_source_chain_locked afterwards). Required for a payout created without source_chain (400 payout_source_chain_missing otherwise, nothing changed), unless funding_source is swaps_wallet.
refund_addressWhere Bridge returns the USDC if this transfer can't be completed. An address on source_chain that you control. Not accepted with funding_source: swaps_wallet. EVM only (0x and 40 hex characters), never the zero address; a mixed-case address must carry a valid EIP-55 checksum, and the address is stored and sent in its EIP-55 form. Omit it to leave the current one unchanged; once set it can be changed only while the transfer awaits funds, never removed.
Create or refresh the funding instructions — available › Responses
The funding instructions — created on the first call, refreshed on every later one.
deposit_addressCase-preserved verbatim — never lower-cased, never re-cased. Reproduce it exactly as returned.
The source amount to send, in source_asset: the provider's quote rounded UP to the cent plus a buffer of at most 0.05 % (buffer), so the recipient receives the full invoice. A receipt above the invoice within that buffer still settles. Always an estimate, never an exact total — the rate can move before the funds arrive.
The part of amount that is the buffer (at most 0.05 % of the quote rounded up to the cent), published so a screen can state it without computing it. Null on a funding attempt created before the buffer existed.
The estimated Swaps fee on this transfer. Computed server-side as gross = amount × 10000 / (10000 − bps); clients never recompute.
The estimated gross source amount before the Swaps fee. Computed server-side as gross = amount × 10000 / (10000 − bps); clients never recompute.
amount_is_estimateAlways true — the source amount is never an exact total (RESOURCE-MODEL §2.2 invariants).
chaindeposit_messageA memo/tag to attach to the transfer, when the chain requires one; null otherwise.
transfer_referenceA reference the merchant can quote when tracing the fiat leg.
refund_addressThe address on chain where Bridge returns the USDC if this transfer can't be completed, recorded only after Bridge accepted it as the transfer's return address (EIP-55 checksummed). null when none was set: Swaps then has no refund address on file, and a deposit Bridge cannot deliver may wait in missing_return_policy (the payout reads processing with needs_attention).
sandboxPresent, and true, only on a test-mode (livemode: false) response: deposit_address is a provider-sandbox address on chain, not a real one. Never send real funds to it. Absent on every live response.
funding_sourcePresent only when this attempt is funded from the payer's Swaps wallet («Wallet balance»), on every read and fund of the attempt whatever the request body; absent means the payer sends USDC to deposit_address from any wallet («Another wallet»). While present, do not send from another wallet.
With funding_source: swaps_wallet: the unsigned send from the payer's own Tempo wallet to exactly deposit_address on chain. Its amount is what leaves the wallet (every cost of the leg included), amount_out what Relay guarantees to deliver. Sign send_instructions.steps[] with the wallet passkey only in the session payouts.fund handed it to (one session per send, never re-handed), then record the hash from that session through POST /v1/wallet/send_intents/{id}/source_tx. Returned only to a dashboard session; a business key sees funding_source alone.
Quote funding this payout from the Swaps wallet — available
Status: Available · Callers: dashboard session · Scope:
payouts.read· Test mode: unavailable
Side-effect-free read behind the «Pay with» step: whether «Wallet balance» can fund this payout right now, on which chain, what must arrive at the deposit address (the funding estimate rounded up to the cent plus at most 5 bps), what leaves the wallet including the Relay leg's own cost, the wallet's spendable USDC.e and whether it covers the send. No transfer is created and nothing is written; before the payout is funded the figures are an estimate that payouts.fund re-quotes. available: false carries a reason (flag_off — also while PAYOUTS_ENABLED is off, wallet_not_provisioned — also every non-dashboard caller, wallet_paused, route_unavailable, test_mode, marked_sent — the payout is already marked sent with no recorded wallet send) and null figures. Dashboard-only: the wallet balance is never shown to a business key. Show «Another wallet» whatever this answers.
path Parameters
id^po_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Quote funding this payout from the Swaps wallet — available › Responses
The wallet-funding quote, or why Wallet balance is unavailable.
funding_sourceavailablereasonWhy Wallet balance is unavailable; null when available is true. marked_sent: the payout is already marked sent with no recorded wallet send.
source_chainThe chain the server would fund from (first of base, arbitrum, ethereum open to both sides).
What must arrive at the deposit address: the funding estimate rounded up to the cent plus a buffer of at most 5 bps.
What leaves the wallet, the Relay leg's own cost included (no Swaps fee on the leg).
wallet_send_estimate minus required_delivery — the cross-network leg, published, never hidden.
The wallet's spendable USDC.e right now.
coverswallet_balance ≥ wallet_send_estimate; false whenever available is false.
How much more USDC.e the wallet needs; null when it covers.
expires_atWhen to re-read this quote (the prepared send's own expiry once one exists).
Get the settlement receipt — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.read· Test mode: unavailable
Exists only once a payout has reached settled. A payout paid_with_shortfall is terminal and by design never produces one — 404 not_found is the correct, expected answer there, not an error to retry past.
path Parameters
id^po_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Get the settlement receipt — available › Responses
The settlement receipt.
payout_id^po_ · requiredThe confirmed amount the recipient actually received — never an estimate on a receipt.
The confirmed amount actually sent from the source chain.
The Swaps fee actually charged on this transfer, read from provider-confirmed settlement state — never an estimate, on a receipt or anywhere else. null unless observed, and today it is always null: the only fee figures a payout carries are computePayoutFee's pre-funding projections, written at funding commit, and that function's own contract is that it produces an estimate, not realized revenue — the settlement webhook remains the SSOT. Publishing a projection here would state as observed something nothing observed. Those projections are published under estimates below instead. This field becomes non-null the day a settlement-confirmed fee is recorded in normalized state, and not before (SEC-D / P1-10).
The gross source amount actually charged, before the Swaps fee — read from provider-confirmed settlement state, never an estimate. null unless observed, under exactly the same reasoning as fee above: the pre-funding projection (gross = amount × 10000 / (10000 − bps)) is a computation, not an observation, and is published as estimates.gross_estimate.
provider_referencesettled_atThe masked beneficiary projection returned on payouts and receipt. Raw bank details cross the boundary once, at POST /v1/payouts/{id}/beneficiary, and are held only by the provider from then on — this is the only shape ever read back (RESOURCE-MODEL §2.2 invariants). Note the output field is beneficiary_owner_type, not the request's account_owner_type — the two are named differently on purpose.
The estimates shown before settlement, kept for the record — never authoritative. Present whenever the payout carries the fee projection written at funding commit; source_estimate is omitted when no source projection was stored for this payout.
noteList a payout's funding attempts — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.read· Test mode: unavailable
List the source-chain funding attempts made against this payout, newest first.
path Parameters
id^po_ · requiredquery Parameters
limitDefault 25. Out of range is never rejected: a value outside 1..100, a fraction, or a non-number is not a 400 — 0, a negative number, a non-integer or anything non-numeric falls back to the default (25); anything above 100 is capped to 100. A documented clamp, not a silent one; minimum/maximum are deliberately absent from this schema so a generated client (the MCP tool included) does not reject a value the API itself accepts.
cursorOpaque cursor from a previous response's next_cursor.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
List a payout's funding attempts — available › Responses
A page of funding attempts.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
List a payout's events — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.read· Test mode: sandbox
List this payout's immutable event timeline, newest first. This is the only timestamp source for the dashboard's 4-step tracker ladder — there are no separate ladder-step columns anywhere else in the contract (RESOURCE-MODEL §2.2 v2 amendments, "required"). Test mode: sandbox (K11-6, §4.2 drift, was declared full) — a test payout's events are written by DEV handlers into DEV's own api_events outbox; a prod-local read would return an empty page forever.
path Parameters
id^po_ · requiredquery Parameters
limitDefault 25. Out of range is never rejected: a value outside 1..100, a fraction, or a non-number is not a 400 — 0, a negative number, a non-integer or anything non-numeric falls back to the default (25); anything above 100 is capped to 100. A documented clamp, not a silent one; minimum/maximum are deliberately absent from this schema so a generated client (the MCP tool included) does not reject a value the API itself accepts.
cursorOpaque cursor from a previous response's next_cursor.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
List a payout's events — available › Responses
A page of payout events.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
Cancel a payout — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.write· Test mode: unavailable · Money boundary: per-transaction human confirmation required
Cancel a payout that has not yet received funds. Re-reads the live provider transfer first and refuses if money may already be moving — never describe this as a way to claw back a payment in flight; there is no tool for that, only support. Calling it twice is safe.
A wallet send prepared for this payout («Wallet balance», payouts.fund with funding_source: swaps_wallet) is signed by the holder outside Swaps, so while it can still be signed — until it expires, and for 30 minutes after that — cancel is refused with 409 wallet_funding_pending and details.retry_after: wait until then, there is no earlier way. Once nothing can be signed, the payout's unsigned wallet sends are closed first, and only then is the provider transfer cancelled; if they cannot be closed nothing else is touched (503 wallet_funding_cancel_failed).
path Parameters
id^po_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Cancel a payout — available › Responses
The cancelled payout.
id^po_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atstatusThe 11 payout states (RESOURCE-MODEL §2.2). paid is not terminal — the ladder's final rung stays active until settled. settled is the recipient paid the invoice exactly, or above it by at most 0.05 % of the invoice plus 10 minor units (25 for MXN) — the funding buffer's over-delivery; settled_amount is what was actually paid. paid_with_shortfall is terminal: it produces no receipt, no success notification, and never becomes settled. A1-2: open — an unrecognized member is an opaque string, never a deserialization failure.
The invoice amount the recipient is owed, in fiat.
corridor_idThe corridor this payout settles through, as listed by GET /v1/capabilities?product=payouts. Known values today (RESOURCE-MODEL §3 vocabulary): usd_ach, usd_wire, eur_sepa, gbp_faster_payments, brl_pix, mxn_spei, cop_co_bank_transfer (lifecycle blocked — never actually payable).
fiat_railThe settlement rail underlying corridor_id.
source_chainThe chain the payer funds from. Read it together with source_chain_chosen: while that is false, no chain has been chosen yet and this is only the default (base), never a choice. The stored payout.* event payload (GET /v1/events, the SSE stream, webhooks, GET /v1/activity) carries the stored chain instead: null for a draft with no chain, and no source_chain_chosen.
source_chain_chosenTrue once a chain is fixed for this payout: sent as source_chain at create, or fixed at funding (POST /v1/payouts/{id}/funding_instructions, locked once the provider transfer exists). False on a draft created without source_chain; source_chain then shows the default.
source_assetThe stablecoin the payer funds with. Only USDC is live today.
payer_typeSet by Swaps at creation, never from the request: business only when the account the payout was created under and the payer's provider customer were both business. The individual per-payout limit is checked at funding against the account's type at that moment and the fresh provider customer, not against this stored value. A distinct axis from beneficiary.beneficiary_owner_type.
fee_bpsThe Swaps fee, in basis points, on the gross source amount. Today always 100 (1%). A stored value that disagrees with the current fee contract is refused at fund (RESOURCE-MODEL §2.2 invariants).
notify_recipientWhether recipient_email is emailed a payment confirmation once the payout lands.
v1 reality (K4c, §52 C4-D12): eta_seconds/minimum are resolved from the LIVE corridor catalog at read time, keyed by the payout's own stored capability_id; version is the capability-contract version stamped at creation, not re-resolved — both are presentation-only facts (no SLA, no legal claim) that are safe to re-derive live. sender_display/legal_entity_name are the OPPOSITE: FROZEN at payout creation (or replacement — a payouts.replace draft gets its own fresh resolution too, never a copy of the payout it replaces) and read back from the row, never re-derived from the live catalog. A payout created on or after this snapshot's migration always carries one — sender_display is set even when the resolver could not identify a specific sender (the literal value unknown, never omitted in that case). A payout created BEFORE the migration carries no snapshot at all: sender_display is omitted entirely and legal_entity_name is null, exactly as this endpoint behaved before the freeze existed — nothing proves what that payout was actually sent as, so nothing is invented for it. The freeze exists precisely because the catalog's answer for a corridor can change for reasons that have nothing to do with one specific payout (e.g. Bridge moved eur_sepa from bridge to customer on 2026-09-02, D-111): a live-resolved value here would let a payout created before that date wrongly claim "customer" on a statement it never actually carried that name on. Resolution reads the provider's account record AS CACHED AT THE MOMENT OF FREEZE — not a live probe — so a rename on Bridge's side after creation never changes an already-frozen payout's answer; a client rendering bridge should pair it with a tip that the statement name changes once the account is renamed. A client that wants the corridor's CURRENT terms (for a payout not yet created) calls GET /v1/capabilities?product=payouts instead. Consumer contract: prefer this snapshot first and fall back to a live capability read ONLY when it is absent (a legacy pre-freeze payout) — never the reverse.
needs_attentionRouter-derived from the internal reconciliation_status signal, which stays internal (RESOURCE-MODEL D-8, resolved). True renders as "Checking settlement" over whatever status currently reads, most commonly over paid.
updated_atThe confirmed amount actually settled to the recipient — the invoice, or up to 0.05 % of the invoice plus 10 minor units (25 for MXN) above it when the funding buffer over-delivers. Null until settled.
The confirmed amount the provider actually paid out. Set alongside invoice_shortfall_amount on paid_with_shortfall; null otherwise.
invoice_amount minus provider_paid_amount on a paid_with_shortfall payout. Null on every other status.
noteFree-text note the payer attached at creation.
recipient_emailThe recipient's email, when the payer chose to notify them (RESOURCE-MODEL §2.2 v2 amendments — added create + read).
The masked beneficiary, once added. Null on a draft with no beneficiary yet.
Set on failed and returned; also set to missing_return_policy while Bridge holds the deposit with no address to return it to (processing, needs_attention).
funded_atWhen the funding provider transfer was created. Null before funding.
paid_atWhen the provider confirmed payout to the recipient's bank.
settled_atWhen the payout reached its terminal settled state.
Record the payer's self-reported "I've sent it" — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.write· Test mode: unavailable
Records the payer's own claim that the funding transfer was sent. This is a self-report, authoritative: false (RESOURCE-MODEL §3) — it never advances status on its own and is never a substitute for the funds-received signal the provider webhook produces. REST-only; not curated as an MCP tool, since an agent should never manufacture this claim on a person's behalf.
path Parameters
id^po_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Record the payer's self-reported "I've sent it" — available › Responses
The payout, with the self-report recorded.
id^po_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atstatusThe 11 payout states (RESOURCE-MODEL §2.2). paid is not terminal — the ladder's final rung stays active until settled. settled is the recipient paid the invoice exactly, or above it by at most 0.05 % of the invoice plus 10 minor units (25 for MXN) — the funding buffer's over-delivery; settled_amount is what was actually paid. paid_with_shortfall is terminal: it produces no receipt, no success notification, and never becomes settled. A1-2: open — an unrecognized member is an opaque string, never a deserialization failure.
The invoice amount the recipient is owed, in fiat.
corridor_idThe corridor this payout settles through, as listed by GET /v1/capabilities?product=payouts. Known values today (RESOURCE-MODEL §3 vocabulary): usd_ach, usd_wire, eur_sepa, gbp_faster_payments, brl_pix, mxn_spei, cop_co_bank_transfer (lifecycle blocked — never actually payable).
fiat_railThe settlement rail underlying corridor_id.
source_chainThe chain the payer funds from. Read it together with source_chain_chosen: while that is false, no chain has been chosen yet and this is only the default (base), never a choice. The stored payout.* event payload (GET /v1/events, the SSE stream, webhooks, GET /v1/activity) carries the stored chain instead: null for a draft with no chain, and no source_chain_chosen.
source_chain_chosenTrue once a chain is fixed for this payout: sent as source_chain at create, or fixed at funding (POST /v1/payouts/{id}/funding_instructions, locked once the provider transfer exists). False on a draft created without source_chain; source_chain then shows the default.
source_assetThe stablecoin the payer funds with. Only USDC is live today.
payer_typeSet by Swaps at creation, never from the request: business only when the account the payout was created under and the payer's provider customer were both business. The individual per-payout limit is checked at funding against the account's type at that moment and the fresh provider customer, not against this stored value. A distinct axis from beneficiary.beneficiary_owner_type.
fee_bpsThe Swaps fee, in basis points, on the gross source amount. Today always 100 (1%). A stored value that disagrees with the current fee contract is refused at fund (RESOURCE-MODEL §2.2 invariants).
notify_recipientWhether recipient_email is emailed a payment confirmation once the payout lands.
v1 reality (K4c, §52 C4-D12): eta_seconds/minimum are resolved from the LIVE corridor catalog at read time, keyed by the payout's own stored capability_id; version is the capability-contract version stamped at creation, not re-resolved — both are presentation-only facts (no SLA, no legal claim) that are safe to re-derive live. sender_display/legal_entity_name are the OPPOSITE: FROZEN at payout creation (or replacement — a payouts.replace draft gets its own fresh resolution too, never a copy of the payout it replaces) and read back from the row, never re-derived from the live catalog. A payout created on or after this snapshot's migration always carries one — sender_display is set even when the resolver could not identify a specific sender (the literal value unknown, never omitted in that case). A payout created BEFORE the migration carries no snapshot at all: sender_display is omitted entirely and legal_entity_name is null, exactly as this endpoint behaved before the freeze existed — nothing proves what that payout was actually sent as, so nothing is invented for it. The freeze exists precisely because the catalog's answer for a corridor can change for reasons that have nothing to do with one specific payout (e.g. Bridge moved eur_sepa from bridge to customer on 2026-09-02, D-111): a live-resolved value here would let a payout created before that date wrongly claim "customer" on a statement it never actually carried that name on. Resolution reads the provider's account record AS CACHED AT THE MOMENT OF FREEZE — not a live probe — so a rename on Bridge's side after creation never changes an already-frozen payout's answer; a client rendering bridge should pair it with a tip that the statement name changes once the account is renamed. A client that wants the corridor's CURRENT terms (for a payout not yet created) calls GET /v1/capabilities?product=payouts instead. Consumer contract: prefer this snapshot first and fall back to a live capability read ONLY when it is absent (a legacy pre-freeze payout) — never the reverse.
needs_attentionRouter-derived from the internal reconciliation_status signal, which stays internal (RESOURCE-MODEL D-8, resolved). True renders as "Checking settlement" over whatever status currently reads, most commonly over paid.
updated_atThe confirmed amount actually settled to the recipient — the invoice, or up to 0.05 % of the invoice plus 10 minor units (25 for MXN) above it when the funding buffer over-delivers. Null until settled.
The confirmed amount the provider actually paid out. Set alongside invoice_shortfall_amount on paid_with_shortfall; null otherwise.
invoice_amount minus provider_paid_amount on a paid_with_shortfall payout. Null on every other status.
noteFree-text note the payer attached at creation.
recipient_emailThe recipient's email, when the payer chose to notify them (RESOURCE-MODEL §2.2 v2 amendments — added create + read).
The masked beneficiary, once added. Null on a draft with no beneficiary yet.
Set on failed and returned; also set to missing_return_policy while Bridge holds the deposit with no address to return it to (processing, needs_attention).
funded_atWhen the funding provider transfer was created. Null before funding.
paid_atWhen the provider confirmed payout to the recipient's bank.
settled_atWhen the payout reached its terminal settled state.
Create a replacement payout — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payouts.write· Test mode: unavailable
Creates a new draft payout carrying this one's corridor and invoice details forward, for a payout that failed, expired or was cancelled before funding — there is no update on payouts; the flow is always cancel (or a terminal failure) plus a fresh create (RESOURCE-MODEL §2.2). Emits payout.replaced on the old payout and payout.created_as_replacement on the new one, linking both ids. REST-only, no MCP tool: creating a replacement is a deliberate, reviewed action, never one an agent should chain automatically off a failure.
path Parameters
id^po_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Create a replacement payout — available › Responses
The new replacement payout, in draft.
id^po_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atstatusThe 11 payout states (RESOURCE-MODEL §2.2). paid is not terminal — the ladder's final rung stays active until settled. settled is the recipient paid the invoice exactly, or above it by at most 0.05 % of the invoice plus 10 minor units (25 for MXN) — the funding buffer's over-delivery; settled_amount is what was actually paid. paid_with_shortfall is terminal: it produces no receipt, no success notification, and never becomes settled. A1-2: open — an unrecognized member is an opaque string, never a deserialization failure.
The invoice amount the recipient is owed, in fiat.
corridor_idThe corridor this payout settles through, as listed by GET /v1/capabilities?product=payouts. Known values today (RESOURCE-MODEL §3 vocabulary): usd_ach, usd_wire, eur_sepa, gbp_faster_payments, brl_pix, mxn_spei, cop_co_bank_transfer (lifecycle blocked — never actually payable).
fiat_railThe settlement rail underlying corridor_id.
source_chainThe chain the payer funds from. Read it together with source_chain_chosen: while that is false, no chain has been chosen yet and this is only the default (base), never a choice. The stored payout.* event payload (GET /v1/events, the SSE stream, webhooks, GET /v1/activity) carries the stored chain instead: null for a draft with no chain, and no source_chain_chosen.
source_chain_chosenTrue once a chain is fixed for this payout: sent as source_chain at create, or fixed at funding (POST /v1/payouts/{id}/funding_instructions, locked once the provider transfer exists). False on a draft created without source_chain; source_chain then shows the default.
source_assetThe stablecoin the payer funds with. Only USDC is live today.
payer_typeSet by Swaps at creation, never from the request: business only when the account the payout was created under and the payer's provider customer were both business. The individual per-payout limit is checked at funding against the account's type at that moment and the fresh provider customer, not against this stored value. A distinct axis from beneficiary.beneficiary_owner_type.
fee_bpsThe Swaps fee, in basis points, on the gross source amount. Today always 100 (1%). A stored value that disagrees with the current fee contract is refused at fund (RESOURCE-MODEL §2.2 invariants).
notify_recipientWhether recipient_email is emailed a payment confirmation once the payout lands.
v1 reality (K4c, §52 C4-D12): eta_seconds/minimum are resolved from the LIVE corridor catalog at read time, keyed by the payout's own stored capability_id; version is the capability-contract version stamped at creation, not re-resolved — both are presentation-only facts (no SLA, no legal claim) that are safe to re-derive live. sender_display/legal_entity_name are the OPPOSITE: FROZEN at payout creation (or replacement — a payouts.replace draft gets its own fresh resolution too, never a copy of the payout it replaces) and read back from the row, never re-derived from the live catalog. A payout created on or after this snapshot's migration always carries one — sender_display is set even when the resolver could not identify a specific sender (the literal value unknown, never omitted in that case). A payout created BEFORE the migration carries no snapshot at all: sender_display is omitted entirely and legal_entity_name is null, exactly as this endpoint behaved before the freeze existed — nothing proves what that payout was actually sent as, so nothing is invented for it. The freeze exists precisely because the catalog's answer for a corridor can change for reasons that have nothing to do with one specific payout (e.g. Bridge moved eur_sepa from bridge to customer on 2026-09-02, D-111): a live-resolved value here would let a payout created before that date wrongly claim "customer" on a statement it never actually carried that name on. Resolution reads the provider's account record AS CACHED AT THE MOMENT OF FREEZE — not a live probe — so a rename on Bridge's side after creation never changes an already-frozen payout's answer; a client rendering bridge should pair it with a tip that the statement name changes once the account is renamed. A client that wants the corridor's CURRENT terms (for a payout not yet created) calls GET /v1/capabilities?product=payouts instead. Consumer contract: prefer this snapshot first and fall back to a live capability read ONLY when it is absent (a legacy pre-freeze payout) — never the reverse.
needs_attentionRouter-derived from the internal reconciliation_status signal, which stays internal (RESOURCE-MODEL D-8, resolved). True renders as "Checking settlement" over whatever status currently reads, most commonly over paid.
updated_atThe confirmed amount actually settled to the recipient — the invoice, or up to 0.05 % of the invoice plus 10 minor units (25 for MXN) above it when the funding buffer over-delivers. Null until settled.
The confirmed amount the provider actually paid out. Set alongside invoice_shortfall_amount on paid_with_shortfall; null otherwise.
invoice_amount minus provider_paid_amount on a paid_with_shortfall payout. Null on every other status.
noteFree-text note the payer attached at creation.
recipient_emailThe recipient's email, when the payer chose to notify them (RESOURCE-MODEL §2.2 v2 amendments — added create + read).
The masked beneficiary, once added. Null on a draft with no beneficiary yet.
Set on failed and returned; also set to missing_return_policy while Bridge holds the deposit with no address to return it to (processing, needs_attention).
funded_atWhen the funding provider transfer was created. Null before funding.
paid_atWhen the provider confirmed payout to the recipient's bank.
settled_atWhen the payout reached its terminal settled state.