Schemas
Money
amount^-?[0-9]+$ · requiredMinor-unit integer string. A negative value appears only on signed ledger rows; every creating request uses MoneyPositive and the router rejects zero or negative amounts.
currencyISO 4217 code or a stablecoin symbol (USDC, USDC.e, pathUSD).
decimalsMoneyPositive
amount(?=.*(?:^-?[0-9]+$))… · requiredMinor-unit integer string. A negative value appears only on signed ledger rows; every creating request uses MoneyPositive and the router rejects zero or negative amounts.
currencyISO 4217 code or a stablecoin symbol (USDC, USDC.e, pathUSD).
decimalsMoneyNonNegative
amount(?=.*(?:^-?[0-9]+$))… · requiredMinor-unit integer string. A negative value appears only on signed ledger rows; every creating request uses MoneyPositive and the router rejects zero or negative amounts.
currencyISO 4217 code or a stablecoin symbol (USDC, USDC.e, pathUSD).
decimalsDepositInstructions
addresschainA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
assetAsset symbol to send on chain (e.g. USDC, USDC.e).
chain_idNumeric chain id, when chain alone does not pin a single concrete network — e.g.
tempo covers both Moderato (testnet, 42431) and Mainnet (4217), and asset's
contract address is not guaranteed distinct across them (a signer must not infer the
network from the symbol alone). Absent when the rail has no such ambiguity.
token_addressOn-chain contract address asset resolves to on chain, verbatim case, when this
attempt has one frozen (e.g. the TIP-20 token address a Tempo Pay splitter accepts).
The authoritative send target for an in-app signer — never re-derive it from asset
against a local registry, which can drift from what the attempt actually froze.
Absent when the rail's asset has no single on-chain contract identity.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
amount_is_estimatequote_expires_atA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
gross_upmemoOn-chain memo / reference when the rail requires one; never on Tempo pay-ins.
expires_atproviderThe third party that moves the funds on this leg, when one does (relay on a
crypto_relay payment). Absent when the payer pays the destination directly.
BankDepositInstructions
currencyrailbank_namebeneficiary_nameholder_kindD-111's classification. Binding on every BankDepositInstructions consumer, present and future: classify the beneficiary name the provider actually returned on THIS record — never derived from the currency or the rail, and never invented when the provider returned no name at all (null, explicit, never merely omitted). own_name: an ordinary beneficiary name (the holder's own, or — for a payment-links payer session — whatever name the provider actually returned). provider_name: the beneficiary is the provider's own settlement entity (e.g. Bridge's "Bridge Building S.A." prefix — KNOWN FALSE POSITIVE: a genuine beneficiary whose own name happens to start with "bridge", e.g. "Bridgewater Ltd", also reads as provider_name; tightening the match is a follow-up, not fixed here). developer_name: a memo-matched, flexible-amount collection account issued in the developer's own name ("Swaps") — payment-links bank-rail pay-ins are EXPECTED to route through exactly this shape, though that expectation has not yet been confirmed against a live provider response for that specific endpoint.
account_numberrouting_numbersort_codeUK Faster Payments — 6-digit domestic sort code, alongside account_number.
clabeMexico SPEI — the 18-digit interbank account key (replaces account_number/routing_number for this rail).
pix_keyBrazil PIX — the key the payer sends to; often the only field this rail populates.
bank_addressibanbicreferenceThe transfer memo/reference text. When matching is reference, this is what the provider matches the deposit BY — the payer must type it into their bank transfer exactly, or the deposit lands unmatched on a pooled account. When matching is account_number, this is recommended (it speeds up reconciliation) but never required — the deposit is already matched by the account it landed on. Absent matching altogether (a record from before this field existed), treat reference as required, matching this schema's pre-existing behavior.
matchingHow the provider attributes THIS deposit to the payer, independent of holder_kind. reference: a memo-matched transfer on a pooled account — the provider matches by the reference text alone, which the payer must type exactly, or the deposit is unmatched. account_number: the account itself (account_number/iban/clabe/sort_code, whichever this rail populates) is unique to this customer, so the provider matches by account rather than memo; reference, when present, is recommended for faster reconciliation but is never required to complete the payment. Omitted when the provider payload carries no field this schema could use to decide (e.g. pix, where a Pix key alone is already a complete, self-matching destination).
ErrorType
The recovery class an agent must distinguish (API-CANON §5).
ListMeta
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.
ObjectBase
idPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atupdated_atAccount
id^acct_ · requiredThe account's id. Never looked up directly by a business key or a session — reached only as "the selected account" (GET /account) or via GET /accounts (mine).
display_nameemailmember_sincecustomer_typeindividual or business. Follows the account owner's customer (customers.get, GET /customers/{id}): it is the type Swaps has recorded for that customer, not a live read from our verification partner. Swaps sets it whenever a customer of the owner is recorded or changes type, in either direction (business when any owner has a business customer). Until the owner has a customer, the type the account was opened with. Not writable through PATCH /account, and customers.create does not write it. A test-mode account keeps the type it was created with. Not itself a KYB projection (RESOURCE-MODEL §2.4).
countryISO 3166-1 alpha-2. null until a source column exists (проверить — public.users has none today).
languagenull until a source column exists (проверить — public.users has none today). Always null for an sk_test_ key (test mode).
access_stateReason for a restricted/suspended/blocked state stays internal — never on the wire.
livemodesupport_contactL4-9 — an EXPLICIT opt-in support contact for the payer surfaces (PaymentSession.merchant_contact on /pay/<token>). NEVER derived from email above (the login e-mail), KYC, Bridge or settlement data — null until the holder sets it here. Also null for an sk_test_ key: withheld in test mode, not unset.
BL-25 (D-PF-9a) — the persona's market for the embedded Buy & sell widget, which previously had no signal to resolve country/currency from and fell back to the public-site default regardless of who was signed in. A read-only projection over the same static country→currency registry the public-site widget itself defaults from (_shared/services/config.ts's countries.json, ISO 4217 local currency per ISO 3166-1 alpha-2 country) — never a live/provider-routed pick, so this carries no money path. Not itself writable. Resolved, in order of trust, from country above (declared once at POST /accounts, validated against this same registry there), then the holder's Bridge KYC address country (not published as its own field), then the public-site no-signal default. Always present — source names which case applied, and a consumer MUST treat source: 'default' as "no persona signal at all" rather than seed anything from it (it is the exact value an anonymous, signed-out visitor gets).
For an sk_test_ key (test mode) always {marketing: false, product_tips: true}, not the owner's values; a test-mode dashboard session sees its own.
Cross-device UI preferences persisted server-side (R18 TA-G9) — not UI-private, since prod already syncs them. For an sk_test_ key (test mode) always {}, not the owner's values; a test-mode dashboard session sees its own.
Present only when ?expand=readiness (or ?expand=readiness,setup_guide) is requested on GET /account AND that expansion succeeded (API-PERF-1 M5, fix round 1) — the exact GET /account/readiness payload, merged in. Absent, never null, when not requested; also absent, with a matching entry in expand_errors, when requested but the expansion itself failed (an unresolvable owner or a Bridge outage) — a failed expansion never fails the account read.
Present only when ?expand=setup_guide (or ?expand=readiness,setup_guide) is requested on GET /account AND that expansion succeeded (API-PERF-1 M5, fix round 1) — the exact GET /account/setup_guide payload, merged in. Absent, never null, when not requested; also absent, with a matching entry in expand_errors, when requested but the expansion itself failed.
Present only when at least one requested ?expand= field could not be computed (API-PERF-1 fix round 1). The account read itself, and any OTHER requested expansion that DID succeed, are still published — this names exactly which field is missing and why, rather than failing the whole response the way the standalone GET /account/readiness/GET /account/setup_guide routes do on the same failure. Absent entirely when ?expand= is unset, or when every requested field succeeded.
Who is making this request, on this account: the credential type and, for a session, the caller's membership role. Read it before offering an owner/admin-only action (e.g. changing display_name with PATCH /account, API keys, the wallet families) instead of learning it from a 403 role_denied. It comes from the same resolved credential the role gate checks, never a second lookup. Present on GET /account and PATCH /account, and on the one GET /accounts item this request's credential resolved to (the selected account). Absent, not null, on any other account (the other GET /accounts items, POST /accounts): this request never resolved a role there — select that account with Swaps-Account and read GET /account. Published in test and live mode alike.
AccountMemberRole
A caller's membership role on an account (account_members.role). owner and admin may change the account's own shape and mint durable credentials; member reads (its scopes are the *.read half). No flow assigns admin today. Response-only and open: a role added within /v1 (e.g. a team role) is additive — treat a value you do not recognize as neither owner nor admin.
AccountUpdateRequest
display_nameThe account's name. 1–80 characters; leading and trailing whitespace is trimmed, and a value that is empty after trimming is refused with 400 invalid_request, param: display_name. Only an account owner or admin may change it (a member gets 403). Payment links, the payer page, receipts and invoice emails show the profile name of the person the link is recorded under (for a link created through /v1, the account's first owner), not this account name. On a live account, saving this name also sets it for each owner who belongs to no other live account, so their payers see it at once. An owner who belongs to more than one live account keeps their current name on all of them until payer names are resolved per account.
languagesupport_contactL4-9 — the opt-in support contact the payer surfaces may show (§0.12 PATCH null-clears convention): omitted leaves it unchanged, an object sets it, explicit null clears it back to unset. At least one of email/url must be present on a set (never a bare {}).
AccountCreateRequest
display_namecustomer_typecountryISO 3166-1 alpha-2. Optional — unset until the holder declares one. Fix round 1 (P2): validated against the same countries.json registry market (on Account) reads from — case/whitespace-normalized on write, an unrecognised value is a 400 invalid_request (param: 'country'), never silently stored and never able to disagree with the resolved market.country for the same account.
AccountList
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.
AccountReadiness
kindtitlebodyfactsShort supporting facts rendered under the body copy (e.g. a pending-count sentence).
next_actiondegradedPresent, and true, only when the Bridge customer status this card was built from is the last persisted snapshot because the live read failed (Customer.degraded). The card then states no verified identity. Absent on a live read.
SetupGuide
donetotalnext_stepdegradedPresent, and true, only when the Bridge customer status this guide was built from is the last persisted snapshot because the live read failed (Customer.degraded). verify_identity and confirm_eligible then report done: false even where the snapshot says verified, and next_step never names a step withheld for that reason: it is the step a live read of the same status names. Absent on a live read.
ActivityRow
productPointer to the backing /v1 resource — read it there for the full projection.
titlestatusThe underlying object's own status string, verbatim. Not a shared cross-product vocabulary — collisions between products' status words are named in R18 §5 and are not resolved by this field.
status_groupThis resource's own six-bucket set (draft, pending, processing, completed, failed,
returned) — not a vocabulary shared with payment_links (open, needs_attention, paid, ended)
or with payouts/payroll_runs/orders (needs_you, in_progress, done). Read each of those
resources' own status_group for their set.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
directionoccurred_atNullable — absent when no fresh conversion exists. Never inflate the figure by omitting a stale one.
counterpartyNull when this row has no single owning provider (e.g. a payroll run).
ActivityList
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.
ActivityProductStatusBucket
countActivityProductStatusTotals
One (product, status_group) cell of ActivitySummary.by_product_status (K8c, §52 C4-D14) — an honest count plus a minor-unit sum, never client math. amount is null when the cell has no rows, when its rows carry more than one currency OR the same currency at more than one decimal scale (summing across either would need an FX rate or a rescale this endpoint does not fabricate), or when a row's amount could not be parsed: count still reflects every row in the cell, but a sum that quietly excluded the unparseable row's contribution, or combined mismatched scales, would be a wrong number, not an honest one.
One (product, status_group) cell of ActivitySummary.by_product_status (K8c, §52 C4-D14) — an honest count plus a minor-unit sum, never client math. amount is null when the cell has no rows, when its rows carry more than one currency OR the same currency at more than one decimal scale (summing across either would need an FX rate or a rescale this endpoint does not fabricate), or when a row's amount could not be parsed: count still reflects every row in the cell, but a sum that quietly excluded the unparseable row's contribution, or combined mismatched scales, would be a wrong number, not an honest one.
One (product, status_group) cell of ActivitySummary.by_product_status (K8c, §52 C4-D14) — an honest count plus a minor-unit sum, never client math. amount is null when the cell has no rows, when its rows carry more than one currency OR the same currency at more than one decimal scale (summing across either would need an FX rate or a rescale this endpoint does not fabricate), or when a row's amount could not be parsed: count still reflects every row in the cell, but a sum that quietly excluded the unparseable row's contribution, or combined mismatched scales, would be a wrong number, not an honest one.
One (product, status_group) cell of ActivitySummary.by_product_status (K8c, §52 C4-D14) — an honest count plus a minor-unit sum, never client math. amount is null when the cell has no rows, when its rows carry more than one currency OR the same currency at more than one decimal scale (summing across either would need an FX rate or a rescale this endpoint does not fabricate), or when a row's amount could not be parsed: count still reflects every row in the cell, but a sum that quietly excluded the unparseable row's contribution, or combined mismatched scales, would be a wrong number, not an honest one.
One (product, status_group) cell of ActivitySummary.by_product_status (K8c, §52 C4-D14) — an honest count plus a minor-unit sum, never client math. amount is null when the cell has no rows, when its rows carry more than one currency OR the same currency at more than one decimal scale (summing across either would need an FX rate or a rescale this endpoint does not fabricate), or when a row's amount could not be parsed: count still reflects every row in the cell, but a sum that quietly excluded the unparseable row's contribution, or combined mismatched scales, would be a wrong number, not an honest one.
One (product, status_group) cell of ActivitySummary.by_product_status (K8c, §52 C4-D14) — an honest count plus a minor-unit sum, never client math. amount is null when the cell has no rows, when its rows carry more than one currency OR the same currency at more than one decimal scale (summing across either would need an FX rate or a rescale this endpoint does not fabricate), or when a row's amount could not be parsed: count still reflects every row in the cell, but a sum that quietly excluded the unparseable row's contribution, or combined mismatched scales, would be a wrong number, not an honest one.
ActivitySummary
totalPer-product × lifecycle-status totals for the product hub sub-lines and Today tiles (K8c, §52 C4-D14) — the same six products and six status groups as by_product/by_status_group, crossed. Computed over the same capped read as the rest of this resource.
EventType
The catalogue of dotted event names an Event.type / a webhook endpoint's event_types[] entry may carry
(RESOURCE-MODEL §3 "publish" rows plus v2 additions; dead types are excluded). This is the CLOSED,
REQUEST-side form: webhook_endpoints.create/.update's event_types and events.list's type filter
use it, and an unrecognized or retired name there is 400 invalid_request. EventTypeOpen (below) is the
same list for RESPONSE fields, where an unrecognized member is an opaque string a client must not fail on —
the two are one vocabulary read from two sides, not a contradiction: a request is validated against what
this deployment knows, while a response may come from a deployment newer than the client. The list grows
within /v1 without a version cut.
x-swaps-event-status marks every member live (written to the api_events outbox today) or catalogued
(not written to the outbox: never delivered to a webhook endpoint and never listed by /v1/events or
/v1/activity until its outbox allowlist ships; it is accepted in a subscription, and a per-resource event
list such as payouts.events.list or payroll_runs.events.list may still show it from the product's own
ledger). The marking is generated from EVENT_PAYLOAD_ALLOWLIST
(packages/contracts-api/events.ts) by scripts/openapi/normalize.mjs: the outbox writer drops a type
whose allowlist is empty, so the allowlist is what decides whether a type can ever reach the outbox. No historical
backfill for any type. Producers of the live families: payment_link.*/payment.* — the payment-link
service layer (merchant mutations, the Bridge webhook reducer, the expiry/reminder cron) and the Tempo watcher
(payment.underpaid/.overpaid; payment.unmatched ONLY for a deposit to a closed, unpaid payment's
address — a second deposit to an already-paid payment, or a deposit matching no payment, is recorded for
support and not published on /v1; payment.unmatched is also emitted once, by the payment-link service
layer, when a merchant cancels an underpaid crypto_tempo/crypto_relay payment); payout.* —
appendPayoutEvent (api-v1 or dashboard/admin actions); payroll_run.* — payroll/lib.ts's
appendEvent; subscription.* — the crypto subscriptions service; order.* — emitOrderOutboxEvent;
customer.* — emitCustomerOutboxEvent; test.ping — webhook_endpoints.send_test_event, delivered
only to the endpoint under test; webhook_endpoint.disabled — api-webhooks-worker when an endpoint
auto-disables.
EventTypeOpen
The exact same catalogue as EventType, as the RESPONSE-side form (Event.type, WebhookEvent.type, WebhookEndpoint.event_types): an unrecognized member is an opaque string, never a deserialization failure, because a delivery may carry a type added after the client was generated. A REQUEST that names a type still validates against the closed EventType and answers 400 invalid_request for an unknown name. Kept as its own schema so the open-enum marker never reaches a request field. x-swaps-event-status is the same generated live/catalogued marking as on EventType.
Event
id^evt_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_attypeThe exact same catalogue as EventType, as the RESPONSE-side form (Event.type, WebhookEvent.type, WebhookEndpoint.event_types): an unrecognized member is an opaque string, never a deserialization failure, because a delivery may carry a type added after the client was generated. A REQUEST that names a type still validates against the closed EventType and answers 400 invalid_request for an unknown name. Kept as its own schema so the open-enum marker never reaches a request field. x-swaps-event-status is the same generated live/catalogued marking as on EventType.
api_versionThe Swaps-Version this event was minted under.
updated_atrequestNull for events with no originating API request (a webhook reducer, a cron sweep, a watcher).
EventData
The affected resource's own allowlisted projection at the time of the event — never the raw internal row. Deliberately THINNER than a GET of the same resource: fields that need gateway-only computation (a payment link's payable_rails/payable_rail_kinds, its line items) are never recomputed here, so they are absent from this projection even where GET would carry them — never guessed or backfilled from a stale value.
detailA short, human-readable summary of the event, built server-side from the producer's own record of what happened (e.g. 3 rows · USD, Jamie Rivera · bank account) — never a substitute for data.object, and never more than what the resource's own published projections already disclose, and never an internal actor's identity (an account's approving user, an operator) unless that identity is itself a published field elsewhere the same caller can already read. Populated per event type by the producer that emits it (K7b ships it for payroll_run.*/payroll_item.*); absent, not an empty string, where no producer has one yet.
WebhookEvent
id^evt_ · requiredtypeThe exact same catalogue as EventType, as the RESPONSE-side form (Event.type, WebhookEvent.type, WebhookEndpoint.event_types): an unrecognized member is an opaque string, never a deserialization failure, because a delivery may carry a type added after the client was generated. A REQUEST that names a type still validates against the closed EventType and answers 400 invalid_request for an unknown name. Kept as its own schema so the open-enum marker never reaches a request field. x-swaps-event-status is the same generated live/catalogued marking as on EventType.
created_atlivemodeapi_versionThe receiving endpoint's pinned Swaps-Version.
EventList
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.
WebhookEndpoint
id^whe_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_aturlPublic https only. Rejected at create/update time if it is a swaps.app host or a Swaps project's own supabase.co host, or
resolves to a private, link-local, loopback or otherwise reserved destination (SSRF hardening); the delivery worker re-resolves and re-checks
the SAME way immediately before every send attempt and never follows a redirect — a 3xx response
is recorded as a failed delivery attempt, not a success. This narrows, but does not eliminate, a
DNS-rebinding window: the guard's own resolution and the subsequent fetch() are two separate DNS
lookups a hostile or compromised resolver could answer differently (docs/api/WEBHOOKS.md §7).
event_typesEvent types this endpoint receives. Empty means "all" (every type this account can ever emit,
present and future). An unknown or retired name is 400 invalid_request, at create and update alike
(validated against the closed EventType at that time — this response echo is open only because
it is an A1-2 response field, not because an unrecognized value can actually appear here). A type
marked catalogued in EventType's x-swaps-event-status is accepted but delivers nothing until its outbox allowlist ships.
enabledupdated_atdisabled_reasonSet only when enabled is false: manual (the owner disabled it via update) or
auto_disabled_repeated_failures (K9's own auto-disable after too many consecutive
fully-exhausted deliveries — see webhook_deliveries.status). Null whenever enabled is true.
descriptionsecret_prefixThe signing secret's own non-secret prefix (e.g. whsec_ab12) — always present once an endpoint has
a secret, safe to display anywhere the full secret is not (it never re-derives the full value).
secretThe full signing secret, in cleartext. Present ONLY in the response to create and
rotate_secret — stored hashed thereafter, never returned again and never re-derivable.
WebhookEndpointCreateRequest
urlPublic https only. A private, link-local, loopback or otherwise reserved destination is rejected,
and so is any swaps.app host or a Swaps project's own supabase.co host (the platform never delivers to itself).
event_typesSubset of the closed event registry this endpoint receives. Omitted or empty means "all". An unknown
or retired name is 400 invalid_request. A type marked catalogued in EventType's
x-swaps-event-status is accepted but delivers nothing until its outbox allowlist ships.
descriptionWebhookEndpointUpdateRequest
urlPublic https only. A private, link-local, loopback or otherwise reserved destination is rejected,
and so is any swaps.app host or a Swaps project's own supabase.co host (the platform never delivers to itself).
event_typesAn unknown or retired name is 400 invalid_request.
enabledSetting false disables the endpoint (disabled_reason becomes manual) and halts future
deliveries — past deliveries stay in the log. Setting true re-enables it and clears
disabled_reason, including one an auto-disable had set.
descriptionWebhookEndpointList
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.
WebhookDelivery
id^whd_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atendpoint_id^whe_ · requiredevent_id^evt_ · requiredattemptHow many delivery attempts this row has made so far (the worker's own retry schedule; see docs/api/WEBHOOKS.md).
statuspending: never yet attempted, due now. succeeded: a 2xx response — terminal. failed: the most
recent attempt failed and a further retry is already scheduled at next_attempt_at (automatic — no
action needed). exhausted: every scheduled attempt failed and no further retry will happen — replay
it explicitly with webhook_deliveries.replay.
updated_atnext_attempt_atresponse_statusThe HTTP status the endpoint returned, or null if the attempt never got a response (timeout, DNS/SSRF refusal, connection error).
response_msRound-trip time in milliseconds for the most recent attempt. The response BODY is never stored.
errorA short machine-readable failure reason for the most recent attempt (e.g. timeout, connection_refused, non_2xx_response, url_not_allowed). Never the endpoint's response body.
delivered_atSet once, when status first becomes succeeded.
WebhookDeliveryList
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.
WebhookWaitlistRequest
emailCardWaitlistRequest
emailAddressBookEntry
id^adr_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atlabelrailupdated_atnetworkrail: crypto only.
addressrail: crypto only. Case-preserved verbatim — never lowercased.
iban_maskedrail: iban only.
bicrail: iban or swift.
routing_number_maskedrail: ach only.
account_number_maskedrail: ach, faster_payments or swift.
sort_code_maskedrail: faster_payments only.
pix_key_typerail: pix only.
pix_key_maskedrail: pix only.
clabe_maskedrail: spei only.
bank_addressrail: swift only.
rail: crypto only — the latest screening of address on network by this account, from screenings.create or address_book.recheck. network is matched the way screenings.create reads chain (a chain id or name, case-insensitive), so an entry saved as Ethereum carries a screening recorded under chain 1. Null only when no such screening exists: no /v1 screening of it yet (checks made in the v1 dashboard are not carried over, so null does not mean never checked), or screening does not cover the entry — a network such as Tempo, or an address that cannot exist on its network — and its address_book.recheck answers 409 network_not_supported, never an Ethereum verdict.
last_checked_atscreened_at of screening; null exactly when screening is null — a check made in the v1 dashboard is not carried over.
self for the holder's own destination, third-party once a beneficiary attestation exists.
Null until a beneficiary attestation exists. Set to individual/legal_entity/vasp_customer by address_book.beneficiary.update; set to self_verified/self_unverified by the dashboard's proof-of-control flow (not itself exposed on /v1).
Read-only. Null until the destination is verified through the dashboard's proof-of-control flow — that verification step is not exposed on /v1 (out of scope for this operation). micro_deposit is published because a live v1 writer already uses it, even though today's DB CHECK constraint has not been widened to accept it (a separate v1 bug) — publishing it now costs nothing, no consumers exist yet.
proof_of_control_verified_atRead-only. Non-null rows are what the address-book hub counts as "verified".
proof_of_control_artifact_urlRead-only. A Supabase Storage object key in a private bucket — not a directly fetchable public URL.
travel_rule_requiredTrue once a transfer through this destination triggered EU TFR / MiCA / FATF R.16 evaluation.
travel_rule_last_attested_atNull until a transfer through this destination has been Travel-Rule-attested. Stamped only by the transfer-time attestation (attestTransfer, mid-transfer) — never written by any /v1 operation, including address_book.beneficiary.update.
counterparty_nameThe saved beneficiary's name. Null until a beneficiary attestation exists. Set by address_book.beneficiary.update. No pattern/length constraint on this READ shape, deliberately — so a row already stored before a request-side constraint existed can never fail the whole list/get response, the same failure mode finding #5 flagged for proof_of_control_method. The constraint lives on the request schema (TravelRuleAttestationCreateRequest) instead.
counterparty_countryISO 3166-1 alpha-2, uppercased on write. Null until a beneficiary attestation exists.
counterparty_vasp_nameThe counterparty VASP's name. Only set when beneficiary_subtype is vasp_customer.
counterparty_vasp_jurisdictionISO 3166-1 alpha-2, uppercased on write. Only set when beneficiary_subtype is vasp_customer.
AddressBookEntryCreateRequest
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object · rail="crypto" · requires: label, network, address | |
| type = object · rail="iban" · requires: label, account_holder, iban +1 more | |
| type = object · rail="ach" · requires: label, account_holder, account_number +2 more | |
| type = object · rail="faster_payments" · requires: label, account_holder, sort_code +1 more | |
| type = object · rail="pix" · requires: label, account_holder, pix_key +1 more | |
| type = object · rail="spei" · requires: label, account_holder, clabe | |
| type = object · rail="swift" · requires: label, account_holder, swift_bic +2 more |
labelrailnetworkSaved as sent; any network can be saved. Screening covers only the networks ScreeningCreateRequest.chain lists — an entry on any other reads screening: null and its address_book.recheck answers 409 network_not_supported.
addressCase-preserved verbatim — never lowercase a BTC, Tron or Solana address.
AddressBookEntryCreateRequestCrypto
labelrailnetworkSaved as sent; any network can be saved. Screening covers only the networks ScreeningCreateRequest.chain lists — an entry on any other reads screening: null and its address_book.recheck answers 409 network_not_supported.
addressCase-preserved verbatim — never lowercase a BTC, Tron or Solana address.
AddressBookEntryCreateRequestIban
labelrailibanbicaccount_holderLegal name of the account owner, as the bank holds it (a person's full name or a company's registered name). A payment link settles to this destination under this name. Blank after trimming is refused. Stored trimmed and not returned on a read.
AddressBookEntryCreateRequestAch
labelrailaccount_numberrouting_numberaccount_typeaccount_holderLegal name of the account owner, as the bank holds it (a person's full name or a company's registered name). A payment link settles to this destination under this name. Blank after trimming is refused. Stored trimmed and not returned on a read.
AddressBookEntryCreateRequestFasterPayments
labelrailsort_codeaccount_numberaccount_holderLegal name of the account owner, as the bank holds it (a person's full name or a company's registered name). A payment link settles to this destination under this name. Blank after trimming is refused. Stored trimmed and not returned on a read.
AddressBookEntryCreateRequestPix
labelrailpix_keypix_key_typeaccount_holderLegal name of the account owner, as the bank holds it (a person's full name or a company's registered name). A payment link settles to this destination under this name. Blank after trimming is refused. Stored trimmed and not returned on a read.
AddressBookEntryCreateRequestSpei
labelrailclabeaccount_holderLegal name of the account owner, as the bank holds it (a person's full name or a company's registered name). A payment link settles to this destination under this name. Blank after trimming is refused. Stored trimmed and not returned on a read.
AddressBookEntryCreateRequestSwift
labelrailswift_bicaccount_numberbank_addressaccount_holderLegal name of the account owner, as the bank holds it (a person's full name or a company's registered name). SWIFT destinations are not a payment-link settlement destination today: activate answers 422 settlement_rail_unsupported; use iban, ach, faster_payments, pix or spei. Blank after trimming is refused. Stored trimmed and not returned on a read.
AddressBookEntryUpdateRequest
labelTravelRuleAttestationCreateRequest
beneficiary_subtypecounterparty_namecounterparty_country^[A-Za-z]{2}$ISO 3166-1 alpha-2, either case (uppercased on write).
counterparty_vasp_nameRequired when beneficiary_subtype is vasp_customer; refused blank, exactly like the dashboard form.
counterparty_vasp_jurisdiction^[A-Za-z]{2}$ISO 3166-1 alpha-2, either case (uppercased on write). Only meaningful when beneficiary_subtype is vasp_customer.
AddressBookList
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.
Screening
id^scr_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_ataddressCase-preserved verbatim.
chainrisk_scorerisk_levelverdictDerived, coarser than risk_level. Still a signal, never a block decision.
actionA suggestion the caller may act on — never an authorization by itself.
cachedscreened_atupdated_atflagsFree-form, source-shaped detail — never re-typed per source.
confidencecoverageThe sources actually checked, so a client can see what was not.
provider_versionScreeningCreateRequest
addressCase-preserved verbatim — never lowercase a BTC, Tron or Solana address.
chainThe network to screen on, by chain id or name, case-insensitive: Ethereum (1, eth), BNB Chain (56, bsc), Polygon (137), Arbitrum (42161), Optimism (10), Base (8453), Avalanche (43114), bitcoin, tron, solana, xrp (XRP Ledger), ton, cardano, cosmos. Any other value — blank, tempo, litecoin — whatever the address, and an address that cannot exist on the named network (an EVM address on bitcoin, a Bitcoin address on tron) are refused 409 network_not_supported; nothing is ever screened as Ethereum by default. An address is judged by the named network's own address format; on solana that is any base58 string of 32–44 characters.
ScreeningList
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.
ScreeningReport
id^rpt_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atcachedTrue when this report was served from the 30-day cache rather than spending a fresh credit.
updated_atScreeningReportList
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.
TraceRequest
hashThe origin transaction hash to trace forward from (the incident transfer, e.g. the transaction that sent funds to a scammer) — a trace always starts from a specific transaction, never a bare address, since an address alone has no single "flow" to follow.
chainTrace
id^trc_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atupdated_atChainTransaction
chainhashA short risk brief on each side of the transaction.
Credits
free_remainingpaid_balancetotal_checksCreditEvent
typeamountSigned credit delta — negative for a consumption, positive for a purchase, refund or grant.
balance_aftercreated_ataddressThe screened address this event relates to, when one exists.
chainCreditEventList
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.
events_totalLifetime, exact row count of every credit_events entry for this account, across all type values — never capped by limit/cursor pagination and never limited to the page currently returned in data. Omitted (not null, not 0) on the rare failure of the underlying count query — a caller must treat a missing field as "unknown right now", not as zero.
Displayed side by side with GET /v1/credits's own total_checks as "N events · M reports" — never summed. The two counters are not on the same basis and are not reconcilable against each other: events_total is an append-only row count (it includes the refund row itself, alongside the consume_free/consume_paid row it refunds), while total_checks is a net counter that is incremented on consume and DECREMENTED, clamped at zero, on refund. Summing them double-counts every refunded check, and the two drift further apart with every refund an account accumulates.
CreditCheckout
urlA Stripe Checkout session url — redirect the holder there to complete the purchase.
ApiKey
id^key_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atnamekey_prefixsk_live_ or sk_test_, matching livemode.
last_fourdefault_swaps_versionThe Swaps-Version this key pins requests to when the caller sends no Swaps-Version header
(API-CANON §3, RESOURCE-MODEL §0.6). A request that does send the header overrides this default
for that call only; this field is unaffected.
scopesA key created before the screenings.* rename may still list the legacy screening.read/screening.write.
statusupdated_atallowed_ipsexpires_atlast_used_atrevoked_atApiKeyCreateRequest
namescopes<resource>.<read|write>. The legacy screening.read/screening.write are deprecated aliases, accepted until the next Swaps-Version date and stored as screenings.read/screenings.write.
livemodedefault_swaps_versionOptional; defaults to the current Swaps-Version when omitted.
allowed_ipsexpires_atApiKeyCreated
id^key_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atnamekey_prefixsk_live_ or sk_test_, matching livemode.
last_fourdefault_swaps_versionThe Swaps-Version this key pins requests to when the caller sends no Swaps-Version header
(API-CANON §3, RESOURCE-MODEL §0.6). A request that does send the header overrides this default
for that call only; this field is unaffected.
scopesA key created before the screenings.* rename may still list the legacy screening.read/screening.write.
statussecretThe full sk_live_… / sk_test_… value, in cleartext. Present only in the FIRST live response to
create/roll — stored hashed thereafter, never returned again and never re-derivable. A replayed
Idempotency-Key against the same request gets the same body back with this field redacted to null,
never the cleartext a second time (AuthenticatedRoute.secretFields, the same mechanism K9
introduced for webhook_endpoints.create/.rotate_secret).
updated_atallowed_ipsexpires_atlast_used_atrevoked_atApiKeyList
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.
ApiKeyUsage
requests_todayok_todayerrors_todaylast_request_atRequestLog
id^req_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atrequest_idoperationThe operationId this call resolved to, e.g. screenings.create.
status_codeoklatency_msipupdated_aterror_codeRequestLogList
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.
PaymentLinkItem
product_id^prd_namedescriptionquantityA Money value that must be zero or positive (never negative) — for a real figure that may legitimately be zero, e.g. a free/included line item's unit price. Money itself permits a leading - for signed ledger rows; this variant never does.
unitOptional unit of measure. Design-forward (RESOURCE-MODEL v2 amendment, R11 PL-G4) — not a confirmed backend column.
sort_orderPaymentLink
id^pl_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atstatusrefunded reads "Refunded" to the merchant and "Returned" to the payer on the same underlying value (R11 §3) — one enum member, two audience labels.
titlememoLabelled "Memo" on Payment Links merchant screens; the crypto-processing surfaces label this same field "Description" (R19 CP-G15) — one field, two display labels, never two schema fields.
invoice_numberreference_idA short, non-technical, server-minted id in CP-<n> form (e.g. CP-1042), unique per merchant. Set by the server at creation, never accepted as input. Distinct from invoice_number above, which is an optional, merchant-typed reference that may repeat or be null, and from id (the pl_-prefixed identifier), whose raw form is not meant to be shown to a payer or merchant as a reference number. Present on every payment link, not only crypto-processing ones.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
client_id^cli_ · requireddirectionSystem-set at activation, never an input (RESOURCE-MODEL §2.1 invariants). Enum values are not published in RESOURCE-MODEL; left open rather than invented.
expected_payer_typeComputed grouping of payable_rails into the three UI kinds (RESOURCE-MODEL §2.1 v2 amendment; R11 PL-G5).
expires_atpayer_emailEditable while the link is draft, active or viewed (RESOURCE-MODEL §2.1). R11 PL-G3 flags a design screen that disables editing on viewed — not ratified as a rule change, so the contract keeps viewed editable.
settlement_kindSystem-set at activation, never an input (RESOURCE-MODEL §2.1 invariants; RESOURCE-MODEL §2.1 v2 amendment). Null on a draft that has not activated yet.
BL-39 (C4-D27) — the token identity frozen from settlement_snapshot at ACTIVATION, never a live chain read and never derived from a rate. null on a draft (no snapshot yet) or a non-Tempo settlement — Bridge bank/crypto destinations have no single payer-facing "token" this field describes. Carries no amount of its own.
BL-50 — the saved /v1/address_book entry create/update set as where this link's funds land. null until chosen. Read from the SAME reference activate freezes into settlement_snapshot (never cleared by activation), so it keeps reading the live destination after the link goes live too — distinct from settlement_asset above, which is the token identity frozen at that one moment.
short_code^[0-9bcdfghjkmnpqrst… · requiredC5-SHORT-LINK — the link's short public code (e.g. 7fq3k2c): 7 characters of lowercase Crockford base32 without the vowels a/e (so also no i, l, o, u). Minted by the server from a cryptographic random source when the link is created, for every link (draft or live, live or test mode), never accepted as input, never derived from ids, amounts, e-mails or names, and never changed afterwards. Unique across the project's links. It resolves to the payer session through GET /v1/payment_sessions/by_code/{short_code}, so treat it like url: share it with the payer only.
urlshort_urlC5-SHORT-LINK — <payer host>/p/<short_code> (e.g. https://swaps.app/p/7fq3k2c), on the same per-project payer host as url and under the same rule: null exactly when url is null. Not yet a working payer link: it resolves only once the /p/:code page is live on that host, and only while the /v1 API is enabled; share url until then.
viewed_atRESOURCE-MODEL §2.1 v2 amendment, R11 PL-G1.
paid_atsettled_atupdated_atallowed_railscrypto_relay (R19 X2) is a payer rail behind the crypto_relay_rail flag (off in production), never a stored restriction. A settlement_kind: crypto_only link offers crypto_tempo, plus crypto_relay where its gate admits it (the same on payable_rails and in the payer session): that flag and an admitted route (founder_accepted per founder §52.34, or proven; see K13-CRYPTO-RELAY-ROUTES.md). A1-2: open on THIS response field only — an unrecognized rail here is an opaque string, never a deserialization failure. The same vocabulary on PaymentLinkCreateRequest/PaymentLinkUpdateRequest stays closed: a merchant choosing an unknown rail is a real request error, not a fact to tolerate.
payable_railsThe rails the payer is offered now (one exception below), recomputed on every read; not a snapshot (RESOURCE-MODEL §2.1 v2 amendment). A later read can differ from an earlier one when a rail flag, a Relay route or the Relay cap changes. crypto_relay is listed exactly when the payer session of this link offers it (the same gate: the crypto_relay_rail flag for this merchant, the per-invoice cap, an admitted route for the link's Tempo settlement network, a USD link). If that gate cannot be read, crypto_relay is left out and the read still succeeds, where the payer session answers 503 temporarily_unavailable. Exception: an individual (direction: p2p) merchant whose Bridge fiat pay-in is not active yet is listed with its bank rails, although the payer session withholds them (C4-D23; tracked in #3977). Open-enum like allowed_rails above (queue item #3560) — the same forward-compatibility reasoning applies to a computed field.
accepted_rail_kindsA crypto_only settlement suppresses every fiat rail, so this reads [crypto] (RESOURCE-MODEL §2.1 v2 amendment; R19 §2.1).
partial_paymentSet only while status is processing and a payment arrived short of amount (§8.4). received reports the single MOST RECENT short payment recorded for this link's CURRENT attempt — one deposit's figure, not a running total of everything the link has received — and is null when that deposit's settled amount could not be confirmed (shown to the merchant as "needs review"). null while processing with nothing recorded, and again once the link leaves processing for a later full payment or a cancellation. A no-funds failure on an attempt moves the link to viewed and clears this field to null; it does NOT reopen the link to processing. The flag CAN reappear, but only when a LATER attempt creates a new transfer and puts the link back into processing — if that new attempt also under-pays, its own event is what reappears here, never an earlier attempt's superseded figure. The key itself may be absent when the underlying signal could not be read — that is not the same as null.
PaymentLinkList
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.
Present only with ?expand=summary (LIST-SUMMARY-1). Computed server-side over every link matching the request's status_group, q, client_id and settlement_kind (never cursor/limit), in one grouped query. by_status_group uses the status_group buckets exactly; they overlap (viewed/processing are both open and needs_attention), so they do not add up to total.
PaymentLinkListSummary
totalThe amount of every matching paid or settled link, summed exactly per currency (minor units, same Money rules as PaymentLink.amount), sorted by currency. Empty when none. The link's requested amount, not a settlement ledger.
SettlementDestination
address_book_id^adr_ · requiredSettlementDestinationView
address_book_id^adr_ · requiredPL-TEMPO-BRIDGE-1-T (§52.33, independent money review — the FX-leg P1); mechanism fixed by PL-TEMPO-BRIDGE-2-T (Bridge external accounts are fiat-only — sandbox proof: 400 invalid_parameters on account_type: 'crypto'). Present ONLY once activation has materialized a Bridge COLLECTION account for this destination (a non-USD link's Tempo wallet settling through Bridge instead of the non-custodial crypto_tempo splitter) — absent for every other destination (a plain Tempo splitter address, a Bridge bank rail, or a draft with no snapshot yet). status: 'created' is the only value this can ever carry: a Bridge REFUSAL of the collection account (settlement_destination_provider_refused) fails the activate call itself before anything is persisted, so a link can never be read back with any other status here. Carries no exchange rate or fee — that data exists only once a real transfer settles; this object reports the destination Bridge holds, never a quote. last4 is the settlement address's own last 4 characters (the collection template's to_address), never an account id — there is no external account.
PaymentLinkCreateRequest
A Money value that must be strictly positive — used on every request field that creates or moves value.
titleOptional — a blank title is treated as absent. When absent, it is derived server-side: invoice_number (as Invoice <invoice_number>) if set, else the first line item's name (or, when unnamed, the name of the product it resolves to), with +N more appended for additional items. Only when none of those can supply a value does create answer 400 invalid_request naming title. A derived title longer than 200 characters is truncated with an ellipsis (L4-10).
memoinvoice_numberclient_id^cli_expected_payer_typeallowed_railsRails to offer the payer on this link. Use accepted_rail_kinds to draft a crypto-only invoice — setting rails here alone does not select settlement_kind.
accepted_rail_kindsSet to [crypto] for a link settling to your OWN Swaps Wallet (R19 CP-G2) — there is no separate settlement_kind input (RESOURCE-MODEL §2.1 invariants: settlement_kind is system-set, never an input). On a USD request this routes the later activate call onto the Bridge-less crypto_tempo splitter branch, gated by payment_links_crypto_only. On any OTHER currency (§52.33, PL-TEMPO-BRIDGE-4-T, fixer round findings #5/#11) it instead routes activate onto the SAME Bridge collection path (payment_rail: tempo + the account's own wallet address) an address-book Tempo destination uses, gated by payment_links_tempo_via_bridge ALONE — the SAME flag capabilities.get's settlement_currencies[currency].tempo_wallet reads.
BL-50 — the saved destination activate will freeze into settlement_snapshot. Naming this alongside accepted_rail_kinds: [crypto] (a Swaps-Wallet intent) on the SAME create request is refused 400 settlement_conflicts_with_destination — reachable only while this account is admitted for that currency (payment_links_crypto_only for USD, payment_links_tempo_via_bridge otherwise); when it is not, accepted_rail_kinds: [crypto] is itself refused first (409 capability_unavailable on USD, 422 tempo_via_bridge_not_enabled otherwise), so the 400 never fires. update does not refuse the combination: setting a destination there RETIRES an earlier accepted_rail_kinds: [crypto] intent instead — see PaymentLinkUpdateRequest.settlement_destination.
expires_atpayer_emailPaymentLinkUpdateRequest
titleA blank value is refused 400 invalid_request naming title — clearing it is not supported; omit the key to leave the stored title unchanged (L4-10).
memoinvoice_numberA Money value that must be strictly positive — used on every request field that creates or moves value.
client_id^cli_expected_payer_typeallowed_railsAccepted as a partial write mid-draft (R11 PL-G6). Three states, and they are distinct: omit the field to leave the restriction as it is, send null to REMOVE it (the link then offers every rail available for its currency), send an array to replace it. An empty array is not a way to clear it — it is a 400, because a link restricted to no rail cannot be paid. A restriction that leaves the link with no payable rail at all is refused 400 allowed_rails_invalid_for_currency (and 409 at activation), carrying the rails that would work in error.details.offerable_rails. It narrows what the link can be paid on and never widens it: a rail the merchant is not endorsed for stays unavailable whether or not it is named here.
BL-50 — three-state, same convention as allowed_rails above: omit to leave the stored destination as it is, send null to CLEAR it, send {address_book_id} to set or replace it. An id you do not own answers 404 not_found, identical to an unknown one (RESOURCE-MODEL §0.7). Setting a destination here RETIRES an earlier accepted_rail_kinds: [crypto] (Swaps-Wallet) intent recorded at create — the response stops reporting accepted_rail_kinds and the link settles to this destination instead; the newer choice always wins. 400 settlement_conflicts_with_destination is refused only by create, for naming both in the SAME request — update never throws it.
expires_atpayer_emailPaymentLinkActivateRequest
attestation_acceptedMust be true; any other value is refused.
PaymentLinkReceipt
payment_link_id^pl_ · requiredpayment_id^pay_ · requiredThe payment (attempt) that completed the link — GET /v1/payments/{id} returns it.
titleinvoice_numberWhat the link asked for, in its own currency — the ask, not an observation.
payment_railThe rail the completing payment used (a Payment.payment_rail value).
The observed on-chain deposit for a crypto_tempo payment, in the token that arrived (same value as Payment.amount_received). null on every other rail.
provider_referenceThe provider's reference for a provider-routed payment — the transfer id, or the deposit id for a payment collected on a collection account (virtual account); null for crypto_tempo.
The 1% Swaps fee, payer-borne — same value as Payment.fee for payment_id (exact server arithmetic on crypto_tempo; null on rails where the payment projection publishes none). It is why a crypto_tempo amount_received exceeds amount.
The Swaps fee deducted from what arrived on a Bridge-routed rail — same value as Payment.deducted_fee for payment_id (the provider receipt's figure); null on crypto_tempo/crypto_relay, when the payer paid in another currency than the invoice, and while no consistent receipt is recorded.
What the merchant receives — same value as Payment.net_amount for payment_id: the invoice amount on crypto_tempo/crypto_relay once settled; on a Bridge-routed rail the provider receipt's figure (what arrived minus deducted_fee, in the invoice currency, before conversion), null without one; null before settlement.
settlement_kindSame value as PaymentLink.settlement_kind.
Where the funds land, masked as a reference — same value as PaymentLink.settlement_destination; never a raw address or bank detail.
onchain_tx_hashpaid_atsettled_atnull while the link is paid and not yet settled.
noteThe link's own memo.
What the merchant received, provider-confirmed. OMITTED today, never null and never the invoice amount: the received figure is recorded on the link's paid event (checked against the invoice within a 1% tolerance) but is not yet published as a provider-confirmed amount. net_amount is the server arithmetic, not this confirmation.
ReminderSchedule
statusoffsets_daysCaller-owned.
sentServer-owned (RESOURCE-MODEL §2.1). A caller-supplied value here is ignored, not merged.
next_reminder_atComputed convenience field (RESOURCE-MODEL §2.1 v2 amendment; R11 PL-G17).
ReminderScheduleUpdateRequest
offsets_daysClient
id^cli_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atdisplay_nameupdated_atemailcompanycountryFree text today. R11 PL-G13 is an open ruling on whether this narrows to a closed enum — left as free text pending that ruling.
rolenotesarchived_atClientCreateRequest
display_nameemailcompanycountrynotesClientUpdateRequest
display_nameemailnull clears the client's email. An empty string fails the format check (400).
companynull clears the client's company name.
countrynull clears the client's country.
notesnull clears the client's notes.
ClientList
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.
Product
id^prd_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atnamenull for a product saved with no fixed price (the merchant fills the amount in per-invoice at create time) — pl_products.unit_price is a nullable column; K3 correction, the v1 draft required this and 500'd on a real row.
updated_atdescriptioncurrencyISO 4217 code or stablecoin symbol, carried alongside unit_price (RESOURCE-MODEL §2.1 lists both unit_price{} and currency verbatim as sibling fields).
archived_atProductCreateRequest
nameA Money value that must be strictly positive — used on every request field that creates or moves value.
currencydescriptionProductUpdateRequest
nameA Money value that must be strictly positive — used on every request field that creates or moves value.
currencydescriptionnull clears the product's description. An empty string is trimmed and treated as null (unchanged pre-K3c behaviour, no format validator on this field).
ProductList
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.
SettlementAsset
symbolCanonical display ticker for the frozen network — USDC.e on Tempo mainnet, pathUSD on Moderato.
decimalsnetworkThe settlement rail, not the concrete Tempo network — Moderato vs Mainnet is DepositInstructions.chain_id, never re-derived from this label.
PaymentStatus
Cross-product pay-in lifecycle (RESOURCE-MODEL §2.1 v2 amendment, 12 states). processing is still planned (Bridge-rail only, R19 X4). underpaid / overpaid / unmatched are AVAILABLE (CP-T3, §52.37 item 4 — RESOURCE-MODEL CP-G5's producer): the Tempo watcher (crypto_tempo/crypto_relay) writes them directly onto payment_link_attempts.status — a deposit short of the frozen total is underpaid (stays live: a later top-up can still complete it, settling paid with the SUM of every leg received, never just the last one); a match ABOVE the total settles the merchant's exact invoice as before and is overpaid, surplus published (paid to the Swaps fee wallet together with the fee, never auto-refunded — see Payment.surplus); a deposit to a closed, unpaid payment's address (expired or abandoned) makes that payment unmatched, amount_received = what was observed on it; a merchant cancel of an underpaid payment makes it unmatched too, never abandoned — the money stays held and its running total stays amount_received; Payment.unmatched_reason names which of these two made it (deposit_after_close / partial_before_cancel); a further deposit to an unmatched or already-paid payment's address never changes its status and is recorded for support, not published on /v1; a leg in a different accepted token than an underpaid payment's running total is never summed into it and is recorded for support; a deposit matching no payment at all is recorded for support. Events: payment.underpaid / payment.overpaid are live on TWO independent producers — the Bridge pay-in reducer (below) AND, as of CP-T3, the Tempo watcher, each firing at most once per payment on its own rail. The Bridge pay-in reducer emits one per payment when Bridge reports the received amount in the link's own currency and it falls outside ±1 % of the link amount; data.object carries both figures as amount_received and amount_expected (Money, link currency for the Bridge producer; the observed Tempo stablecoin for the watcher's own producer — crypto_tempo has no FX leg to convert either figure into). No verdict is published for a pay-in in another currency or on an FX-estimated figure. On the Bridge rails ONLY, payment.paid (and payment.settled) also fire for a short payment and come first, and data.object.status keeps reading paid/settled while the link stays processing: a payment.underpaid on the same pay_ id overrides them — do not fulfil on payment.paid alone. On crypto_tempo/crypto_relay this never happens: an underpaid deposit never reaches funds_received at all until it is topped up. payment.marked_sent is live — the payer's non-authoritative "I have sent it" on /pay, status still awaiting. Still catalogued, with the missing observation: payment.detected — Bridge delivers no pre-arrival state on the rails Payment links use (funds_scheduled is ACH-only, collection accounts are GBP/EUR) and the Tempo watcher settles on first sight; payment.processing — Bridge's payment_submitted follows funds_received (already paid) and in_review is only recorded on the link timeline, so no pre-paid processing step is observed. payment.unmatched is live (CP-T3, crypto_tempo/crypto_relay) once per payment, ONLY for a deposit to a closed, unpaid payment's address or a merchant cancel of an underpaid payment, its data.object.unmatched_reason naming which; a further deposit to an unmatched or paid payment and a deposit matching no payment are recorded for support and never published on /v1; a Bridge collection-account deposit with no match is still held in the operator ledger with no Payment to carry it (that half stays unproduced). A1-2 fixer round 1 (finding #2): this is the CLOSED, request-side form — payments.list's status query filter uses it directly, and an unrecognized value there is a real 400. PaymentStatusOpen (below) is the exact same list, response-side only (Payment.status, PaymentAttemptView.status, PaymentLink.latest_attempt_status); round 1 marked this schema directly instead, which leaked the open behaviour onto the query filter too (CONVENTIONS.md: mark the specific field, never the vocabulary in general).
PaymentStatusOpen
The exact same 12-state lifecycle as PaymentStatus above, kept as its own schema so the open-enum marker never reaches the payments.list status query filter (A1-2 fixer round 1, finding #2). Response-only — an unrecognized member here is an opaque string, never a deserialization failure. See PaymentStatus above for the state descriptions.
PaymentUnmatchedReason
Why a payment is unmatched (CP-T4-T, crypto_tempo/crypto_relay): one code per way a payment becomes unmatched. partial_before_cancel — the payer had already sent part of the amount (the payment was underpaid) when the merchant cancelled the link; that partial amount is amount_received. deposit_after_close — money was first observed at the payment's settlement address after the payment had closed without being paid (for example it expired, or the link was cancelled before any money was observed); what arrived is amount_received. It may have been sent shortly before the close: the watcher polls, and a crypto_relay payment can still be bridging — this is the observation time, not the chain time. Either way the funds are held: nothing is released or refunded automatically, support handles them (CP-G6). Response-only and open: a value added within /v1 is additive — treat one you do not recognise like null (neutral copy).
PaymentSessionAmountVerdict
PAY-VERDICT-2-T — the amount verdict a payer session publishes (PaymentSession.amount_verdict). exact: the total due arrived, no more and no less. overpaid: more than the total arrived; the merchant still received exactly the invoice and the surplus is paid to the Swaps fee wallet together with the fee (see Payment.surplus). underpaid: less than the total arrived and the request is not complete. unmatched: money arrived on a payment that had already closed without being paid (see Payment.unmatched_reason) and is held. Response-only and open: a value added within /v1 is additive — treat one you do not recognise like null.
PaymentScreening
statuslevelflagsPayment
id^pay_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atpayment_link_id^pl_ · requiredsubscription_id^sub_ · requiredSet when this pay-in settles a subscription invoice rather than a standalone link (RESOURCE-MODEL §2.1 v2 amendment — /v1/payments filters on payment_link_id, subscription_id, settlement).
statusThe exact same 12-state lifecycle as PaymentStatus above, kept as its own schema so the open-enum marker never reaches the payments.list status query filter (A1-2 fixer round 1, finding #2). Response-only — an unrecognized member here is an opaque string, never a deserialization failure. See PaymentStatus above for the state descriptions.
source_currencysource_chainThe one chain field (CMP-8) — the v2 crypto-extension's source_network duplicate is removed; this carries the payment_sessions.network vocabulary for every crypto rail.
source_assetsource_addressMasked (RESOURCE-MODEL §2.1 v2 amendment).
deposit_addressThe splitter address, case-preserved verbatim — never lowercased (AGENTS.md address-case rule).
Where to send crypto for this attempt (MON-7, CMP-5) — present only for a crypto rail; null on a bank rail (bank_deposit_instructions below is set instead). An underpaid crypto_tempo payment with a recorded running total names only the token that total counts, or is null when that token cannot be told apart.
The Bridge-issued bank transfer target for this attempt (K3b) — present only for one of the seven bank rails (ach, wire, fednow, sepa, faster_payments, pix, spei); null on a crypto rail (deposit_instructions above is set instead). Exactly one of the two is ever non-null. See BankDepositInstructions.holder_kind for who the beneficiary actually is on this attempt.
payer_typepayer_marked_sent_atA payer self-report, never authoritative on its own.
return_reasonRaw code from the returns dictionary (R11 §3); the client renders the display label.
settlement_tx_hashonchain_tx_hashCrypto rails only (RESOURCE-MODEL §2.1 v2 amendment; R11 PL-G7).
BL-39 (C4-D27) — this attempt's parent link's token identity, frozen from settlement_snapshot at ACTIVATION (never a live chain read, never derived from a rate, never recomputed per attempt). null for anything that is not a Tempo crypto-only settlement.
The ask — what this attempt requires the payer to send, fixed at creation and unaffected by what has actually arrived (MON-8; RESOURCE-MODEL §2.1 v2 amendment; R11 PL-G2).
The observation — what has actually been detected on-chain or at the rail so far. Null before status=detected; once populated it never goes back to null (MON-8). On unmatched (crypto_tempo/crypto_relay): everything observed up to the moment the payment became unmatched; later deposits to its address are recorded for support only.
The Swaps fee on crypto_tempo/crypto_relay, payer-borne on top of the invoice, at the settlement token's scale (the deposit instructions' figure; amount_expected includes it); null on every other rail, Bridge-routed rails included — the fee those rails deduct from what arrives is deducted_fee.
The Swaps fee deducted from what arrived on a Bridge-routed rail (bank rails, crypto_bridge) — the provider receipt's developer fee, recorded when the payment settled, in the invoice currency, rounded up to the currency's scale; the merchant bears it (net_amount is what arrived minus it). null before the payment is settled (and again if it is later returned); on crypto_tempo/crypto_relay (their fee is fee, paid on top); when the payer paid in another currency than the invoice (a receipt is never converted — the USDC source of crypto_bridge included); and while no consistent receipt is recorded (none sent, terms that do not add up, a provider exchange or gas fee on top). The configured rate is Capabilities.deducted_fee.
What the merchant receives, once the payment is settled; null before. On crypto_tempo/crypto_relay the invoice amount (the fee is paid on top). On a Bridge-routed rail only the provider receipt's figure — what arrived minus deducted_fee, in the invoice currency, before any conversion to the settlement asset, rounded down to the currency's scale — and null whenever deducted_fee is null for a receipt reason; never copied from the invoice there.
Present only when status=overpaid (RESOURCE-MODEL §2.1 v2 amendment; live, CP-T3/CP-R4 — the Tempo watcher's own producer, crypto_tempo/crypto_relay only). amount_received minus the frozen total the deposit instructions quoted, denominated in the observed stablecoin. Paid to the Swaps fee wallet together with the fee when the payment settles; returned to the sending address, minus the network fee, only on request through support (manual, from the fee wallet; in force after the legal sign-off on CP-G6, founder §52.60 54A); never auto-refunded. The merchant's own release is untouched: they still receive exactly their invoice regardless of this figure.
Present only when status=underpaid (live, CP-T3/CP-R4 — crypto_tempo/crypto_relay only): the frozen total minus everything observed toward this attempt SO FAR (every deposit leg summed, not just the latest one). A later top-up that completes the payment clears this back to null and settles paid with the sum. null rather than a zero or negative figure, or one across two token scales.
Present only when status=unmatched (CP-T4-T): which of the two paths made the payment unmatched — see PaymentUnmatchedReason. null on every other status, and on a payment that became unmatched before this field existed (no reason was recorded: show neutral copy, never a guessed reason).
RESOURCE-MODEL §2.1 v2 amendment. not_screened is a legitimate value, not an absent field (R19 CP-G13) — a payment that exists but was never run through the guard action reads not_screened, distinct from the field being missing.
expires_atcrypto_relay: the send-by time of its Relay quote (the quote_expires_at of its deposit instructions). null on every other rail today.
updated_atpayment_railcrypto_relay (R19 X2): the payer sends USDC on another EVM network to a Relay deposit address whose recipient is this attempt's splitter; live behind the crypto_relay_rail flag, off in production.
PaymentList
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.
Present only with ?expand=summary (LIST-SUMMARY-1). Computed over every pay-in matching the request's payment_link_id, subscription_id, settlement and status (never cursor/limit). by_status has one key per PaymentStatus value; a status this version does not know counts in total only.
PaymentListSummary
totalneeds_attentionunderpaid + overpaid + unmatched, server-computed over the same rows the list already loaded (C-2, §52.37 item 4). Live for crypto_tempo/crypto_relay pay-ins (CP-T3 — the Tempo watcher is the producer of all three statuses; unmatched can also come from a merchant cancel of an underpaid payment); a Bridge-rail short/over payment is still visible only through the payment.underpaid/.overpaid EVENTS (its Payment.status stays paid/settled per PaymentStatus's own note), so a nonzero count here is not the complete set of every pay-in a merchant might want to review — subscribe to those events too.
PaymentEventList
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.
PaymentSessionPayment
id^pay_ · requiredobjectstatusThe exact same 12-state lifecycle as PaymentStatus above, kept as its own schema so the open-enum marker never reaches the payments.list status query filter (A1-2 fixer round 1, finding #2). Response-only — an unrecognized member here is an opaque string, never a deserialization failure. See PaymentStatus above for the state descriptions.
created_atpayment_railcrypto_relay (R19 X2): the payer sends USDC on another EVM network to a Relay deposit address whose recipient is this attempt's splitter; live behind the crypto_relay_rail flag, off in production.
source_chainstablecoinThe stablecoin the payer sends. USDC (native Circle USDC on source_chain) on a crypto_relay attempt; null on every other rail today.
source_assetWhere to send crypto for this attempt (MON-7, CMP-5) — present only for a crypto rail; null on a bank rail (bank_deposit_instructions below is set instead). An underpaid crypto_tempo payment with a recorded running total names only the token that total counts (as PaymentSession.underpaid_payment does), or is null when that token cannot be told apart.
The Bridge-issued bank transfer target for this attempt (K3b) — present only for one of the seven bank rails; null on a crypto rail (deposit_instructions above is set instead). Exactly one of the two is ever non-null. See BankDepositInstructions.holder_kind for who the beneficiary actually is.
The ask — what this attempt requires the payer to send (MON-8).
The observation — what has actually been detected so far. Null before status=detected (MON-8).
Same value and gating as Payment.surplus: present only when status=overpaid (crypto_tempo/crypto_relay), amount_received minus the frozen total, in the observed stablecoin. Paid to the Swaps fee wallet together with the fee when the payment settles; returned to the sending address, minus the network fee, only on request through support (manual, from the fee wallet; in force after the legal sign-off on CP-G6, founder §52.60 54A); never auto-refunded.
Same value and gating as Payment.amount_missing: present only when status=underpaid (crypto_tempo/crypto_relay), the frozen total minus every deposit leg observed so far. The same figure as PaymentSession.underpaid_payment.amount_missing for this payment.
Same value and gating as Payment.unmatched_reason: present only when status=unmatched, which of the two paths made it (PaymentUnmatchedReason); null otherwise and on a payment made unmatched before the field existed.
The Swaps fee on crypto_tempo/crypto_relay, payer-borne on top of the invoice, at the settlement token's scale (the deposit instructions' figure; amount_expected includes it); null on every other rail, Bridge-routed rails included.
expires_atcrypto_relay: the send-by time of its Relay quote (the quote_expires_at of its deposit instructions). null on every other rail today.
watcher_stateNarrator sub-state (R19 CP-G11), produced for crypto_relay payments (CP-X2, behind crypto_relay_rail); null on every other rail today. From the Relay deposit intent and the payment: address_issued (deposit address issued, nothing seen), source_seen (the payer's transfer seen on the source network), confirming (Relay is bridging it), matching (Relay reported the fill on Tempo; the splitter watcher is matching it), delivered (the splitter watcher observed the deposit and released it — the only state that goes with paid). releasing has no observable producer yet. Problem states always carry watcher_reason and never mean paid: recovering / recovery_required (a deposit on the wrong network, recovered through Relay or needing support; recovery_required with payment_closed: funds reached or are on their way to the splitter of a payment that is already closed, so support must return them), bridge_failed (Relay refunded the order to the payer's refund address, or failed it; a failed order may return nothing and needs support), expired (the send-by time passed with nothing seen — do not send to this address). A transfer Relay saw before the send-by time stays confirming.
watcher_reasonWhy watcher_state is a problem state; null otherwise. bridge_delivered_less goes with matching when Relay reported delivering less than the amount due: the splitter holds the funds and releases nothing until the full amount is there (the underpaid rule). payment_closed goes with recovery_required: the payment was closed (link expired, cancelled or paid another way) while its funds were in flight.
settlement_tx_hashpayer_marked_sent_atThe payer's own non-authoritative "I've sent it" self-report (POST .../mark_sent) — never settlement, status stays the sole source of truth (RESOURCE-MODEL §3, authoritative:false).
PaymentLinkEventList
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.
Subscription
id^sub_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atkindOnly scheduled_invoices is buildable for v1. authorised_pull names the v2 shape (SwapsSubscription contract) so the collision cannot be claimed by accident — it stays behind the founder-gated third-party audit (PRD §11.6) and has no producer.
statusDesign-proposed (R19 §2.3) — PRD-CP-001 does not itself define a status enum.
nameA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
intervalupdated_atoverdueNB-2 — derived: this subscription has at least one invoice that is open and past its due_at. Never stored, computed fresh from the invoice table at read time (one query per page, never N+1) — the same zero-grace rule SubscriptionInvoice.overdue uses. status itself never flips to reflect a missed payment (D-K13-6); this is the field that says so instead. Present on every /v1 read of this object (list, get, create, pause, resume, cancel) and ABSENT from subscription.* event and webhook payloads, which describe the transition that happened rather than a live invoice poll — read subscriptions.get for the current value.
first_due_atnext_due_atclient_id^cli_The merchant's settlement destination — always the Swaps Wallet for v1 (RESOURCE-MODEL §2.6). Sub-shape not further enumerated there.
referenceSUB-00nn (RESOURCE-MODEL §2.6).
paused_atC-55 (§52.37 item 4) — the instant pause set it; resume clears it back to null. null on an active or cancelled subscription, or one never paused. Present on every /v1 read (list, get, create, pause, resume, cancel) — the same producer/consumer pairing as overdue above.
Stored but inert — no delivery exists on the crypto rail yet (R19 CP-G10).
SubscriptionCreateRequest
nameA Money value that must be strictly positive — used on every request field that creates or moves value.
intervalclient_id^cli_ · requiredattestation_acceptedfirst_due_atSubscriptionList
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.
Present only with ?expand=summary (LIST-SUMMARY-1). Computed over every subscription of this account matching the request's status (never cursor/limit), in one grouped query.
SubscriptionListSummary
totalactivepausedThe earliest next_due_at among the matching active subscriptions (ties broken by id); null when none is active or none has a next due date.
SubscriptionInvoice
id^inv_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atsubscription_id^sub_ · requiredsequence1, 2, 3… (Invoice 1, Invoice 2…).
due_atstatusupdated_atissued_atSet on the due date, never earlier.
payment_link_id^pl_Nullable until issued.
overdueDerived: past due and unpaid. Never stored (RESOURCE-MODEL §2.6) — a clock skew cannot desync it from the invoice's real status.
SubscriptionInvoiceList
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.
PaymentSession
merchant_display_nameA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
statusThe live link status published verbatim, plus refunded (v2 amendment). link_not_payable, link_expired and not_found are errors, never states.
Why held money is held, when amount_verdict is unmatched: the server's own classification. Clients draw it; they never derive order from other fields. PAY-HELD-REASON-1-T: the unmatched_reason of the payments behind that verdict (PaymentUnmatchedReason, the same value GET .../payments/{payment_id} publishes for such a payment): partial_before_cancel when part of the payment arrived before the request was cancelled, deposit_after_close when money was first observed after the payment closed unpaid. null whenever amount_verdict is not unmatched; when a payment behind it became unmatched before reasons were recorded; when the payments behind it carry different reasons; when the reason is partial_before_cancel but other money for this link is held beside the payment (for example a top-up sent after the cancel); and for a stored value this contract does not know (withheld and reported, never published). Show neutral copy for null and for a value you do not recognise. Only the code: never an amount, a payment id or a time. Always present, never omitted.
pending_paymentPAY-WAIT-1-T (founder P1, 2026-09-29) — where to send the money while this link's live payment waits for it, so a payer who reopens the link in another browser, an in-app browser or a private window still sees the payment method they chose until its status changes. Present only while status is processing, the link has not expired, and its latest payment is awaiting, detected or processing with deposit instructions that are still usable (a crypto_relay address only until its send-by time and only while Relay has seen nothing; a memo-matched bank slip only with its reference). null in every other case: no payment, a failed, abandoned, expired or returned payment, a verdict (underpaid, overpaid, unmatched), a deposit Swaps already observed on that payment and is still recording (PAY-VERDICT-2-T), another payment's deposit still being recorded (a deposit Swaps observed on another open payment of this link may complete it, so the payer is never asked for money while it is being recorded; PAY-WAIT-3-T), money already on another payment of this link (underpaid, unmatched, completed, or a crypto_relay payment whose Relay leg saw funds), a paid, closed or expired link. Every payment link is a single-payment request (the first completed payment closes it to new payments; a subscription issues one link per invoice), so these are the receiving instructions of the ONE request the link is for: Swaps' or the provider's receiving account, address and reference plus the link's own amount, exactly what GET .../payments/{payment_id} shows for that payment. An exact, on-time payment with them completes that same request, whoever sends it. crypto_relay: send exactly deposit_instructions.amount on deposit_instructions.chain before expires_at; any Relay refund goes to the refund address given when this payment was started, which is not shown here. Never carries the payment id, the payer's e-mail, name, type, state, IP, source or refund address, or any provider id. The id is not a secret from a link holder, though: the same-rail POST .../payments replay returns it on every rail except crypto_relay, so the link token is the only capability for mark_sent and receipt_email. Always present (null or the object), never omitted. A short crypto_tempo payment the payer can top up reads null here and is described by underpaid_payment.
underpaid_paymentPAY-WAIT-2-T (founder ruling §52.53 2A, 2026-09-29) — how much is still owed and where to send it when this link's live crypto_tempo payment arrived short, so the payer can top up the same payment from any browser. Present only while status is processing, the link has not expired, its latest payment is a crypto_tempo payment in underpaid whose running total Swaps recorded, no deposit to it is still being recorded, no other payment's deposit is being recorded (a deposit Swaps observed on another open payment of this link may complete it; PAY-WAIT-3-T), and no other payment of this link holds money (underpaid, unmatched, completed, or a crypto_relay leg that saw funds). null in every other case, including: crypto_relay (its Relay deposit address is issued per quote, so there is no top-up path; amount_verdict and the payment's own read say it is short), a bank rail or crypto_bridge, a cancelled link (its short payment becomes unmatched), a paid, closed or expired link, and a recorded total from which Swaps cannot prove one positive remainder in one token (withheld and reported, never published as a guess). While this is set pending_payment is null. A top-up that reaches the total settles the payment paid with the sum. Never carries the payment id or any payer data (e-mail, name, type, state, IP, source address).
client_display_nameThe payer's own name as saved on the merchant's client record for this link (§52 C4-D5) — e.g. "Marin & Co." rendered as "Website deposit · Marin & Co." null when the link has no client of record attached. Distinct from merchant_display_name (whose money this is) and from title (the merchant's free-text label for the link).
titlememoRendered as "Description" on the payer surfaces (R19 CP-G15).
invoice_numberexpected_payer_typeExactly the rails POST .../payments will actually accept for this link today (K3b: bank rails are back, now that bank_deposit_instructions exists — the P1-4b gap this note used to describe is closed). crypto_relay is listed only while the crypto_relay_rail flag admits the merchant (off in production), the link settles on Tempo mainnet in USD, the invoice is within the rail cap and at least one source network has an admitted Relay route (founder_accepted per founder §52.34, or proven; see K13-CRYPTO-RELAY-ROUTES.md); its networks names those networks.
PL-UNAVAIL-RAILS — rails this link could offer in principle (its currency's rails, the merchant's enabled rails, narrowed by allowed_rails, plus both crypto rails) that THIS session may not pick, so a payer page can show them muted with an honest reason instead of hiding them. Computed from the same eligibility inputs as rails[] and never overlaps it; POST .../payments refuses every rail listed here with rail_not_allowed. Payer-dependent refusals (the individual-payer caps, which depend on the declared payer_type) are not listed: those rails stay in rails[] and are refused at payment time.
expires_atPAY-VERDICT-2-T (#3922) — ONE amount verdict for this link, from the payment that settled it or holds its money, never from the newest payment: a newer payment on another rail never shadows it (latest_attempt_status is unchanged and still reads the newest payment). Swaps derives it from the amounts it recorded itself; never compute one from amount_received and amount_expected. crypto_tempo / crypto_relay: exact when the watcher observed exactly the total due (the invoice plus the 1% fee) and no other payment of this link holds money; overpaid when it observed more (stored together with the paid state, never a second write); underpaid while the running total is short; unmatched for money that reached a closed, unpaid payment. Bridge rails: underpaid only while Swaps holds a short payment open with its received amount recorded for that payment; a Bridge payment Swaps settled as paid reads null, never exact: the only received figure kept for it is an audit note compared without the 1% tolerance and possibly converted at an FX rate, not a verdict (its payment.overpaid / payment.underpaid events stay the Bridge signal). null whenever no verdict is provable: nothing has arrived; the observed amount was not recorded, or is still being recorded; more than one payment of the link completed; with no completed payment, payments that hold money under different verdicts; exact while any other money for this link is held (on another payment, on another payment's Relay leg, or a further deposit to this one). A settling payment's overpaid / underpaid is published even when another payment also holds money; only exact requires that none does. null never means exact.
networkThe source network of this link's latest crypto_relay payment (CP-X2). Present only once a crypto_relay payment exists, which only the crypto_relay_rail flag (off in production) allows; absent otherwise. solana stays dark: POST .../payments answers 409 network_not_supported for it.
stablecoinThe stablecoin the payer sends on network — USDC for crypto_relay. Present exactly when network is.
The Relay deposit for network (CP-X2; R19 §2.4; MON-7, CMP-5), present exactly when network is. Send amount of asset on chain to address (case-preserved verbatim) — never to the splitter — before quote_expires_at. The quote is a strict exact-output order: amount_is_estimate is false, the splitter receives exactly amount_out (the invoice plus the 1% Swaps fee), bridge_fee is Relay's part of amount, and provider: relay states that a third party moves the funds. Relay refunds a failed or mismatched deposit only to the refund address the payer gave; Swaps holds no key on this path. An empty array means the address is not payable (the payment or link is closed, Relay has already seen a transfer, or the send-by time passed) or the stored quote could not be read; watcher_state says which. It is also empty while another payment of the link holds money or has a deposit being recorded; watcher_state then stays address_issued and pending_payment is null.
watcher_stateNarrator sub-state (R19 CP-G11), produced for crypto_relay payments (CP-X2, behind crypto_relay_rail); null on every other rail today. From the Relay deposit intent and the payment: address_issued (deposit address issued, nothing seen), source_seen (the payer's transfer seen on the source network), confirming (Relay is bridging it), matching (Relay reported the fill on Tempo; the splitter watcher is matching it), delivered (the splitter watcher observed the deposit and released it — the only state that goes with paid). releasing has no observable producer yet. Problem states always carry watcher_reason and never mean paid: recovering / recovery_required (a deposit on the wrong network, recovered through Relay or needing support; recovery_required with payment_closed: funds reached or are on their way to the splitter of a payment that is already closed, so support must return them), bridge_failed (Relay refunded the order to the payer's refund address, or failed it; a failed order may return nothing and needs support), expired (the send-by time passed with nothing seen — do not send to this address). A transfer Relay saw before the send-by time stays confirming.
watcher_reasonWhy watcher_state is a problem state; null otherwise. bridge_delivered_less goes with matching when Relay reported delivering less than the amount due: the splitter holds the funds and releases nothing until the full amount is there (the underpaid rule). payment_closed goes with recovery_required: the payment was closed (link expired, cancelled or paid another way) while its funds were in flight.
merchant_contactL4-9 — display-only, so a terminal screen's "contact the merchant" line can be actionable. Populated ONLY from the merchant's own opt-in Account.support_contact; null when the merchant never set one. NEVER the merchant's login e-mail, a KYC field, a Bridge record or any settlement/id data.
PaymentSessionCodeResolution
token^plk_ · requiredThe plk_ capability token — a secret, never an id: never logged, never echoed.
url^https:// · requiredThe payer page for token on this project's payer host (the same value as PaymentLink.url).
PaymentSessionUnavailableRail
railreason_codeClosed set. merchant_fiat_payin_pending: the merchant is an individual whose bank pay-in is not active yet (the same details.reason POST .../payments returns). settlement_requires_crypto: the link settles to a bank account and there is no bank-to-bank route, so only a crypto pay-in can settle it; it takes precedence over merchant_fiat_payin_pending. merchant_individual_rail_blocked: this rail is never payable to an individual merchant (pix, faster_payments). merchant_rail_not_enabled: the merchant's account is not enabled for this rail. link_currency_unsupported: the rail cannot settle an invoice in this currency (crypto_tempo is USD only). amount_below_rail_minimum: the invoice is under the rail's minimum. rail_disabled: Swaps has switched the rail off for this link (an operator switch, not a merchant choice). settlement_kind_mismatch: the link's settlement destination cannot receive this crypto rail (crypto_tempo needs a Tempo address; crypto_bridge needs a Tempo address or a supported bank off-ramp account, so a link settling to an Ethereum address gets neither; crypto_bridge is also never offered on a link that settles to the merchant's own Swaps Wallet, crypto_only). merchant_not_enrolled: Swaps has not yet opened crypto_bridge for this merchant (a Swaps rollout gate, not a merchant setting, so do not word it as the merchant's choice). crypto_tempo and crypto_bridge are always in exactly one of rails[] and this list; crypto_relay is in exactly one of them only while the crypto_relay_rail flag admits the merchant, and in neither otherwise. For crypto_relay, rail_not_offered means no source network has an admitted Relay route (founder_accepted per founder §52.34, or proven) into this link's settlement network, and amount_above_rail_maximum means the invoice is above the rail's per-payment cap. rail_temporarily_unavailable: eligibility could not be verified right now (for example a missing FX rate), so the rail fails closed. rail_not_offered: no more specific reason applies. New values are added only in a documented contract change.
PaymentSessionSelectRailRequest
railpayer_typeThe payer's own declaration (§5.1) — decides which fiat-rail cap applies. Not the merchant's expected_payer_type restriction.
payer_stateTwo-letter US state code — only consulted for an individual payer on a US ACH/wire/FedNow rail against an individual (p2p) merchant's US-residency gate.
payer_emailOptional receipt email set at select-rail time — a payer may also set or correct it afterward via POST .../receipt_email.
networkRequired when rail is crypto_relay: the network the payer sends USDC from (base, ethereum, polygon, arbitrum, optimism; tempo and solana answer 409 network_not_supported). Accepted and ignored on the other rails today.
stablecoinOptional. crypto_relay accepts only USDC (anything else is 400 invalid_request, param: stablecoin); accepted and ignored on the other rails today.
refund_address^0x[0-9a-fA-F]{40}$Required when rail is crypto_relay: the payer's own address on network, kept case-verbatim. Relay returns a failed, short or excess deposit only here. Never a Swaps address — the Swaps fee wallet, the merchant's settlement address and the zero address are refused (400 invalid_request, param: refund_address).
source_chainRequired when rail is crypto_bridge — the payer's own chain, distinct from network (the merchant's settlement network).
source_addressOptional when rail is crypto_bridge — the payer's sending wallet, kept case-verbatim. Bridge accepts a deposit from any sender when omitted.
source_assetRequired when rail is crypto_bridge — Phase 1 accepts USDC only.
PaymentWatcherState
Narrator sub-state (R19 CP-G11), produced for crypto_relay payments (CP-X2, behind crypto_relay_rail); null on every other rail today. From the Relay deposit intent and the payment: address_issued (deposit address issued, nothing seen), source_seen (the payer's transfer seen on the source network), confirming (Relay is bridging it), matching (Relay reported the fill on Tempo; the splitter watcher is matching it), delivered (the splitter watcher observed the deposit and released it — the only state that goes with paid). releasing has no observable producer yet. Problem states always carry watcher_reason and never mean paid: recovering / recovery_required (a deposit on the wrong network, recovered through Relay or needing support; recovery_required with payment_closed: funds reached or are on their way to the splitter of a payment that is already closed, so support must return them), bridge_failed (Relay refunded the order to the payer's refund address, or failed it; a failed order may return nothing and needs support), expired (the send-by time passed with nothing seen — do not send to this address). A transfer Relay saw before the send-by time stays confirming.
PaymentWatcherReason
Why watcher_state is a problem state; null otherwise. bridge_delivered_less goes with matching when Relay reported delivering less than the amount due: the splitter holds the funds and releases nothing until the full amount is there (the underpaid rule). payment_closed goes with recovery_required: the payment was closed (link expired, cancelled or paid another way) while its funds were in flight.
PaymentSessionReceiptEmailRequest
attempt_id^pay_ · requiredemailPaymentSessionMarkSentRequest
attempt_id^pay_ · requiredPaymentSessionPayWithWalletResponse
request_idexpires_atPayoutStatus
The 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.
PayoutFailureCode
Bridge terminal-state failure code (RESOURCE-MODEL §2.2, v2 amendments — "failure_code as a published enum"). The exhaustive list of Bridge terminal codes is not present in the files this fragment was generated from; only returned_no_account is directly evidenced (docs/api/consumers/R12-pay-invoice.md §1 pi-returned, §4). Left open as a free string rather than a closed, invented enum — see x-swaps-enum-source.
PayoutCapabilitySnapshot
eta_secondsTypical time to arrival for this corridor, in seconds. Presentation-only — it carries no provider SLA and must be labelled accordingly wherever it is shown.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
versionCapability-contract version this snapshot was resolved from.
sender_displayHow the sender appeared on the recipient's bank statement, frozen at the moment this payout was created. Omitted only for a payout created before the creation-time freeze existed (no column value to publish) — present, and possibly unknown, on every payout created since.
legal_entity_nameThe specific legal-entity/account-holder name frozen alongside sender_display, verbatim from the customer's Bridge virtual account record AS CACHED AT THE MOMENT OF FREEZE — populated ONLY for eur_sepa (the one corridor whose customer category genuinely reflects the receiving-account record; usd_wire also defaults to customer in the catalog, but that is an ACTIVE Bridge configuration Swaps applies at funding time, not an account fact, so it never gets this per-account refinement and always reads null here), and only when a Bridge customer id is already resolvable for the payer and every activated account THAT Bridge customer holds for EUR agrees on the name on file. Null in every other case: sender_display itself omitted (a pre-freeze payout), sender_display is bridge/payment_partner/unknown, the corridor is not eur_sepa, no Bridge customer id was resolvable yet, no activated account existed to resolve against, or more than one activated account disagreed on the name — the resolver never guesses between them. packages/config/payoutCapabilities.ts itself still carries no legal-entity-name field (PI-G6's catalog half remains open); this field is a per-PAYOUT fact sourced from the account record, not a per-corridor catalog constant.
PayoutBeneficiary
account_holderThe bank account holder's name, as supplied on the beneficiary request.
account_tailThe last few digits of the destination account or rail identifier, masked.
bank_countryISO country code of the destination bank.
beneficiary_owner_typebank_nameName of the destination bank, as supplied on the beneficiary request. Present only when one was supplied (US bank rails ach and wire); absent, never empty, otherwise.
PayoutBeneficiaryAddress
streetcitypostal_codestate_regionUS rails only; null for every other rail.
PayoutBeneficiaryIban
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.)
ibanbicPayoutBeneficiaryUsBank
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.)
account_numberrouting_numberaccount_typebank_nameName of the destination bank. Optional; some banks need it to be identified.
PayoutBeneficiaryUkBank
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.)
sort_codeaccount_numberPayoutBeneficiaryPix
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.)
pix_keypix_key_typeThe PIX key's own kind (e.g. email, phone, CPF/CNPJ, random) — not further enumerated in RESOURCE-MODEL §2.2.
PayoutBeneficiaryClabe
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.)
clabePayoutBeneficiaryRequest
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.)
ibanbicPayout
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.
PayoutCreateRequest
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.
PayoutList
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.
PayoutFundingInstructions
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.
PayoutFundRequest
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.
PayoutWalletFundingQuote
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).
PayoutReceipt
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.
notePayoutAttempt
id^poa_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atstatussource_chainsource_currencyupdated_atsource_tx_hashrefund_addressThe return address Bridge accepted for this attempt's transfer (EIP-55); null when none was set.
PayoutAttemptList
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.
PayoutEventList
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.
PayoutEligibility
The same fee/corridor contract as GET /v1/capabilities?product=payouts — not decomposed further here; read that resource for the full shape.
PayrollRunStatus
A1-2: open — an unrecognized member is an opaque string, never a deserialization failure.
The run's lifecycle. draft and approved are DB-legal but not reachable through a
/v1 action of this caller's own: runs_create writes reviewed directly and
runs_approve writes funding_pending directly, so neither state is ever produced by
this API — only an internal ops webhook (or a seeded row) can leave one in draft or
approved. Both are published in this enum regardless (G decision 2026-09-14):
GET /v1/payroll_runs reads real rows, and a caller-unreachable status is still a
status a real row can carry — omitting it 500'd the entire list the moment one such row
existed, rather than 200ing every row but that one. funded: an inbound funding event
was observed — it does not mean the run is fully funded; read funding_state,
funding_confirmed_amount and needs_attention. Execution is refused for a funded run
whose funding_state is not funded or overfunded.
PayrollRunItemStatus
The item's payout lifecycle inside its run (R14-payroll §4). No item-level cancelled exists — cancelling a run leaves its items wherever they were.
PayrollBlockerCode
Additive reason codes blocking approval, funding or execution, reconciling the two live
dialects behind BLOCKER_CODE_ALIASES (R14-payroll §4, PR-G20): a cached check (4
codes, drawn on the Today hero and the readiness gate before a live Bridge round trip) and
a fresh check (7 codes, drawn in full on the readiness gate — pr-gate). tos_not_accepted
is the one code both dialects share. New codes are appended, never renumbered or removed.
run_has_no_rows (PR-APPROVE-ROWS) is a run-state rung, not a payer check: the run has
zero rows, so approve and fund refuse it with 409 run_has_no_rows.
A1-2: open — an unrecognized member is an opaque string, never a deserialization failure.
PayrollRun
id^pr_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atstatusA1-2: open — an unrecognized member is an opaque string, never a deserialization failure.
The run's lifecycle. draft and approved are DB-legal but not reachable through a
/v1 action of this caller's own: runs_create writes reviewed directly and
runs_approve writes funding_pending directly, so neither state is ever produced by
this API — only an internal ops webhook (or a seeded row) can leave one in draft or
approved. Both are published in this enum regardless (G decision 2026-09-14):
GET /v1/payroll_runs reads real rows, and a caller-unreachable status is still a
status a real row can carry — omitting it 500'd the entire list the moment one such row
existed, rather than 200ing every row but that one. funded: an inbound funding event
was observed — it does not mean the run is fully funded; read funding_state,
funding_confirmed_amount and needs_attention. Execution is refused for a funded run
whose funding_state is not funded or overfunded.
titlecurrencyISO 4217 code the run pays in.
pay_period_startpayroll_runs.pay_period_start is a NULLABLE DB column — the internal create path (optionalDate) writes null whenever the caller omits it, and most real rows do (prod: 59 of 74 rows, 21 of 25 employers, have a null pay period on at least one side). respondValidated's .strict() parse used to require a string here, so that dominant row shape 500'd the entire list the same way the missing draft/approved enum values did (BL-15) — the key stays present (never omitted), the value is null when the run has no pay period on record.
pay_period_endNullable for the same reason as pay_period_start — see that field.
funding_stateThe funding classifier's verdict, independent of status. unfunded: nothing attributable arrived. partially_funded: money arrived and is short of funding_amount — payroll_runs.execute answers 409 payroll_run_underfunded naming the shortfall. funded: the confirmed amount matches. overfunded: more arrived than was asked for; the run may execute and the surplus is recorded.
needs_attentionDerived from funding_state: true when money was observed but the amount is short, is in surplus, or
was never confirmed at all. Advisory — the refusal that actually protects the money is on execute.
funding_methodHow the employer funds the run: fiat by bank transfer to a Bridge virtual account,
crypto by sending USDC to the employer's Bridge payroll wallet. crypto is offered
per currency by GET /v1/capabilities?product=payroll
(funding_currencies[].crypto_funding); while crypto funding is switched off it
answers 409 capability_unavailable on create, approve and the funding call — it
is never silently substituted with a fiat instruction.
Counts over this run's items, by destination readiness.
updated_atmemoA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
The inbound amount the funding classifier attributed to this run and compared against funding_amount. Denominated in the currency the money actually arrived in: the run's own currency on the bank rail, USDC on the crypto rail, where the requirement itself was converted before it was compared. It is never restated into another currency. Null means no amount was ever confirmed — and a run with a null value here cannot execute.
Present once a funding attempt has been requested; null before that.
approved_atfunded_atA funding event was observed for this run — not proof the run is fully funded.
completed_atPayrollRunCreateRequest
titlecurrencypay_period_startpay_period_endfunding_methodOffer crypto only when GET /v1/capabilities?product=payroll shows crypto_funding.available for this currency. create refuses it with 409 capability_unavailable only while crypto funding is switched off (crypto_funding.reason: crypto_funding_not_enabled). For bridge_funding_currency_unsupported or verification_required the run is accepted and cannot be funded: approve refuses it or its funding instructions read blocked.
memoPayrollRunList
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.
Present only with ?expand=summary (LIST-SUMMARY-1). Computed over every run matching the request's status_group (never cursor/limit). The three counts are the Payroll hub's own sections, NOT the status_group buckets: needs_action = draft, approved, reviewed, funding_pending, funded, partial, failed; in_progress = executing; completed = completed, cancelled. A status this version does not know counts in total only.
PayrollRunListSummary
totalneeds_actionin_progresscompletedPayrollRunItem
id^pri_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atrun_id^pr_ · requiredFrozen at run creation — does not track later edits to the recipient record.
destination_statusstatusThe item's payout lifecycle inside its run (R14-payroll §4). No item-level cancelled exists — cancelling a run leaves its items wherever they were.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
updated_atNull while destination_status is missing.
destination_changed_after_approvalPlanned — no producer exists yet (payroll_run_items carries no such column and
payroll_events mints no such event today); a caller must not read this as a live
signal until a producer ships. Always false until then.
paid_atPayrollRunItemList
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.
PayrollRunItemLinkReissue
objectOne recipient's line inside a run — a masked destination projection, never the raw bank or wallet detail (mirrors the payouts beneficiary projection).
linkThe employer's own copy of the recipient's onboarding link. The token appears ONLY inside url, never as a bare field, and only to this run's own employer (the same trust boundary the operation description states). Null on an Idempotent-Replayed response — the token is shown once, never re-served from the idempotency store (AuthenticatedRoute.secretFields, the same mechanism api_keys' own secret uses).
PayrollRecipient
id^prcp_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atdisplay_namedestination_statusupdated_atemailcountryworker_typedestination_kindNon-null only once the employer has adopted a recipient-supplied destination onto the roster (payroll_recipients.adopt_pending_destination) — for where a specific payment actually lands, read payroll_run_items.destination_snapshot instead; null for every recipient with no adopted destination, including rows created before this field existed, which are never backfilled.
null when nothing is waiting. Independent of destination_status: a recipient with a ready default can also have a newer destination waiting.
PayrollRecipientAdoptPendingDestinationRequest
expected_submitted_atThe pending_destination.submitted_at value you read. If the recipient saved another destination since, the call answers 409 pending_destination_changed and the default is unchanged.
PayrollRecipientDestinationAdoption
objectA payee referenced by one or more runs. Created only as a byproduct of create — there is no direct create, update or archive on /v1; the one write is adopt_pending_destination, which saves the destination in pending_destination as the default. email is genuinely optional: existing rows can be email-less — from data that predates this field's own validation, or from the legacy dashboard-v1 action handler (payroll/actions.ts's own optionalEmail) — even though the public POST /v1/payroll_runs itself always requires recipient.email and accepts no destination input of its own (PayrollRunCreateRequest; a /v1 caller cannot currently create an email-less row this way — Codex review finding, round 12, correcting an earlier description here that implied it could). The sibling PayrollRunItem.recipient_snapshot.email and PayrollTemplate.items_snapshot[].recipient.email already model the identical optionality for the identical field on those resources (Codex review finding, round 7 — email was required here only, out of step with every other resource carrying the same field, and the list route's .strict() validation of every returned row meant one such legitimately-email-less recipient failed the ENTIRE list response with a 500, not just its own row).
PayrollRecipientList
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.
PayrollTemplate
id^prt_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atnamecurrencyThe source run's items at the moment the template was saved.
source_run_id^pr_ · requiredupdated_atPayrollTemplateCreateRequest
source_run_id^pr_ · requirednameDefaults server-side to "
PayrollTemplateList
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.
PayrollFundingInstructions
statusrun_referencePAYROLL- plus the run id, stripped of non-alphanumerics, first 12 characters, upper-cased.
retryablefunding_route_typeThe rail family behind these instructions (bank or crypto), driving which of bank/crypto below is populated. A run whose funding_method is crypto reads crypto_deposit_to_wallet in every state before verified.
Present when funding_route_type is bank. beneficiary_name carries what was previously
beneficiary; rail is the single settlement rail for this instruction (the run's own
run_reference above stays on the parent object, not nested here).
Present when funding_route_type is crypto_deposit_to_wallet and status is
verified: the employer's Bridge payroll wallet — address (case-preserved, send to it
verbatim), chain (the wallet's chain, e.g. solana), asset (USDC) and amount, the run's
funding_amount (fee included) converted to USDC when the instructions were prepared.
Send the whole amount in ONE transfer of asset on chain: deposits on this route are
not summed — a short deposit leaves the run partially_funded, and a top-up does not
fund it (contact support). A later funding call without a method switch returns this
same amount; it is never re-priced under a deposit already sent. This whole object is
null (never a zero-valued object) when the amount cannot be priced; status then reads
failed with failure_reason: fx_unconverted. This route answers capability_unavailable on
create/approve/refresh while crypto funding is switched off — it is never returned
as a silent substitute for the bank shape, and the bank shape is never returned as a
silent substitute for it.
failure_reasonNormalized only — never the raw Bridge or provider error text. A snake_case code (for example bridge_wallet_address_unavailable); provider_error when the provider failed with text that is not published.
PayrollReadiness
readyblockersPayrollAttempt
id^pra_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atrun_item_id^pri_ · requiredstatusupdated_atprovider_idrail_typeA provider-scoped rail identifier, not a Rail value: bank_bridge (bank payout via Bridge), bridge_crypto_wallet (crypto-wallet payout via Bridge) or tempo_wallet (Swaps Tempo wallet). Open — a new provider or transport adds a value. A provider-neutral vocabulary is an A1-10 (naming freeze) follow-up.
provider_referencenormalized_error_codenormalized_error_messageHumanised — never raw engineering vocabulary.
PayrollAttemptList
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.
PayrollEventList
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.
PayrollRecipientSession
run_titleA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
for_namestatusHumanised item state. locked covers every status past ready — the page shows one line, never a per-status detail.
can_update_destinationWhether payroll_recipient_sessions.destination.set will currently accept a submission from this token. status alone stopped predicting this once a ready item's parent run could also lock it: a ready item reads can_update_destination: false once the run has moved past draft/reviewed (approved onward), and a needs_destination item reads false once the run is cancelled/completed/failed — nothing will ever be approved against it. Check this before rendering the destination form rather than inferring it from status.
employer_display_nameThe employer's real business name, not an internal handle.
rotated_tokenOnly present on payroll_recipient_sessions.destination.set, and only for the holder that just used the old token. Changing a destination retires the token that did it and mints a replacement, so a leaked copy of a payslip link cannot redirect the same payout twice. Use this value for every subsequent call; the token in the path you just called is dead.
PayrollRecipientDestinationRequest
destination_kindRequired when destination_kind is bank. Supply exactly the rail's own field set — today only SEPA (IBAN + BIC) is reachable end to end. bank_name and address are also required for every reachable bank rail (the server's own normalizePayrollBankDestinationInput refuses destination_bank_name_required / destination_beneficiary_address_required without them, the same beneficiary-address requirement the authenticated payouts beneficiary shape already carries) — this pair of fields was missing from the wire schema at K7 ship time, which made the ONLY reachable rail (SEPA) always 400 (code-fix prerequisite, found while building the public recipient page, R6).
Required when destination_kind is crypto.
QuoteRequest
sideDirection of the trade. swap is crypto-to-crypto; search exists in the underlying contract but is never a public input (RESOURCE-MODEL §2.4, R15 §3).
from_assetSource asset symbol or ISO 4217 fiat code (bestQuoteRequestSchema.fromAsset).
to_assetDestination asset symbol or ISO 4217 fiat code (bestQuoteRequestSchema.toAsset).
from_amount^[0-9]{1,15}(\.[0-9]…Amount to spend, as a decimal string in from_asset's major units — must be strictly positive; the router rejects zero and validates it against the route's resolved minimum (bestQuoteRequestSchema.fromAmount). Exactly one of from_amount / to_amount is given; the router quotes the other side. Bounded by digit count, not a flat character cap (SEC/BSR-5, buy/sell v-fix review 2026-09-17; widened fixer round 1, 2026-09-19) — this string previously had no length cap at all: from_amount "99999999999999999999" (20 digits) reached a live Bridge backend_native offer and echoed back as final_in: "9999999999999999999900" minor units. A flat maxLength: 18 closed that but was itself too narrow: prod quote_attempts (last 30 days, widget-originated but pricing the SAME shape an /v1 destination-amount caller sends) has real rows up to 20 characters, e.g. 0.03853376279604058 (19) and 0.004231894472103885 (20) — full-precision crypto amounts an ERC-20 caller pricing by destination amount routinely sends. The pattern instead bounds the INTEGER part to 15 digits — one above the widest documented per-currency ceiling (CURRENCY_MAX_AMOUNT_MAJOR_UNITS.COP, packages/config/amountLimits.ts, 14 integer digits at 0 decimals) — and the FRACTIONAL part to 18 digits, comfortably covering every real value above while still rejecting the original 20-digit integer abuse case. This is a contract-layer SHAPE bound only, not a value ceiling: the per-offer/per-route VALUE limit (resolved_limits.max, further clamped by a config-owned risk ceiling) is enforced separately in createBridgeNativeOrder (supabase/functions/api-v1/routes/orders.ts) — a 15-digit integer string can still spell an amount far above a route's own limit without tripping a pattern/length check alone. Separately, for side: 'buy' only, POST /v1/quotes also refuses (422 amount_invalid) a from_amount whose fractional part carries more precision than from_asset's OWN scale (BSR-8, getPrecision — packages/config/amountLimits.ts's CURRENCY_PRECISION, the canonical fiat-decimals table .claude/rules/money.md names for every amount limit in this codebase) — e.g. "20.999" for a 2-decimal fiat currency like EUR — a shape this pattern deliberately still admits (18 fractional digits, for a real 18-decimal crypto amount) but that no fiat rail can move: LIVE evidence (dev) showed a Bridge backend_native offer round that exact value to EUR 21.00 internally while sealing the raw "20.999" verbatim into the deposit instructions, instructing the payer to send an amount SEPA cannot carry. A value with harmless trailing zeros beyond the scale ("20.990") is unaffected, as is a real 3-decimal Gulf dinar ("1.001" KWD/BHD/OMR/JOD — CURRENCY_PRECISION lists these at 3, not the naive 2-decimal guess an earlier cut of this fix used). This scale check applies to from_amount only, never to_amount (see that field's own description for why), and to side: 'buy' only, never sell/swap: sell's from_asset is the crypto being sold and swap is crypto-to-crypto, neither a fiat amount CURRENCY_PRECISION has an opinion on — an unlisted ERC-20 (e.g. DAI, 18 decimals) checked against getPrecision's own 2-decimal fallback would be misjudged and refused for a perfectly real amount.
to_amount^[0-9]{1,15}(\.[0-9]…Amount to receive, as a decimal string in to_asset's major units — must be strictly positive, when the caller is pricing by destination amount instead of source amount (bestQuoteRequestSchema.toAmount). Bounded the same way as from_amount (SEC/BSR-5, widened fixer round 1) — see that field's description for why. Deliberately NOT held to from_amount's BSR-8 per-asset scale check: from_amount (or the offer's own final_in derived from it, when priced in receive-mode) is the one figure this gateway ever seals into a live payer-facing deposit instruction, and to_amount is not — no route this API exposes today seals a raw to_amount into anything a payer must send or a rail must move. A caller pricing BY destination amount routinely sends the full on-chain precision of what it wants to receive (prod quote_attempts: to_amount: "0.004231894472103885", 18 fractional digits, for to_asset: "USDC", a 6-decimal token) — the SmartRouter still prices that correctly into a from-leg the provider itself scales properly, so there is no verbatim-sealing hazard on this side to close.
country^[A-Z]{2}$ISO 3166-1 alpha-2 country of the payer, when known — narrows the payment-method fan-out (bestQuoteRequestSchema.country). A well-formed but sanctioned or unrecognized code is refused with 409 capability_unavailable, not accepted and quoted (BSR-6, SEC-02) — checked against the same catalog GET /v1/capabilities?product=buy_sell validates country against.
payment_methodPreferred payment method id, when known (bestQuoteRequestSchema.paymentMethod). Omit to let the router rank every connected method.
from_networkSource chain, for a crypto leg (bestQuoteRequestSchema.fromNetwork).
to_networkDestination chain, for a crypto leg (bestQuoteRequestSchema.toNetwork). Optional here, but a Bridge-native orders.create (handoff_mode: backend_native) requires the WINNING offer's own execution_context.to_network to be set — a quote priced without it cannot become a Bridge-native order (409 capability_unavailable). Set it explicitly for any asset with more than one public chain.
Provider
idStable registry id (e.g. bridge, paybis, transak) — not a /v1 prefixed object id (RESOURCE-MODEL §0.2 lists no provider prefix).
display_namelogo_urlServed from Swaps' own CDN, never recoloured (D-26, brand-asset invariant) — https://swaps.app/providers/ {id}.svg, the same first-attempt asset path the exchange widget's own ProviderLogo component requests (K6b).
kyc_typeThe provider's KYC posture from PROVIDER_REGISTRY (D-56) — e.g. whether it verifies before or after the first trade. No fixed enum is published upstream; treat as an opaque label.
This provider's minimum for the route, from the registry (D-56).
This provider's maximum for the route, from the registry (D-56).
descriptionShort, public-safe copy for this provider, from PROVIDER_REGISTRY[].description (K6b) — never the internal notes field (operational status, env var names, commercial terms). Omitted, not guessed, for a provider with no vetted public description yet.
CapabilityProvider
idStable registry id (e.g. bridge, paybis, transak) — not a /v1 prefixed object id (RESOURCE-MODEL §0.2 lists no provider prefix).
display_namestatusDerived from THREE signals, combined, never the static one alone: this provider's RAIL_CAPABILITIES_ REGISTRY row lifecycle (public -> available (live), preview -> degraded (real code, not fully live — dark-flagged or cohort-gated), blocked -> disabled) — the SAME registry buildBuySellProviders() already reads — overridden to disabled whenever EITHER of the two sanctioned runtime kill switches the quote route's own eligibility check reads is tripped right now: provider_config.enabled = false (global switch), or every provider_directions row for this provider is enabled = false (per-direction switch — a provider with no provider_directions rows at all is unaffected). A read failure on either check 503s the whole response rather than guessing (Codex review, PR #2989 rounds 2-4: a provider an operator had runtime-disabled via either switch otherwise still reported available here).
logo_urlServed from Swaps' own CDN, never recoloured (D-26, brand-asset invariant) — https://swaps.app/providers/ {id}.svg, the same first-attempt asset path the exchange widget's own ProviderLogo component requests. Present only when that SVG is actually checked in under public/providers/ — a provider without one yet is omitted here rather than advertising a URL that 404s for an external client with no monogram fallback of its own (K6b P2 fix, Codex review PR #2989).
kyc_typeThe provider's KYC posture from PROVIDER_REGISTRY (D-56) — e.g. whether it verifies before or after the first trade. No fixed enum is published upstream; treat as an opaque label.
This provider's minimum for the route, from the registry (D-56).
This provider's maximum for the route, from the registry (D-56).
descriptionShort, public-safe copy for this provider, from PROVIDER_REGISTRY[].description (K6b) — never the internal notes field (operational status, env var names, commercial terms). Omitted, not guessed, for a provider with no vetted public description yet.
QuoteProviderResult
statusquoted = this provider returned a priced offer; quoting is reserved for future streaming and is never returned by synchronous POST /quotes; no_offer = it answered with nothing executable; paused = disabled server-side; error = it failed to answer in time. Never collapse no_offer into error.
Present only when status is quoted — this provider's own priced offer, same shape as the fields Quote carries for the winning route (quoteOfferSchema). payment_method alone stays optional (absent means unknown) — every other field here is a money/pricing fact the server never quotes a provider without (BL-35 follow-up, DEV-2).
reasonPresent only when status is no_offer, paused or error — the dead-end reason code (never a raw provider error string).
QuoteFees
QuoteResolvedLimits
min^[0-9]+(\.[0-9]+)?$ · requiredDecimal amount in currency's major units (resolvedLimitsSchema.min).
max^[0-9]+(\.[0-9]+)?$ · requiredDecimal amount in currency's major units (resolvedLimitsSchema.max).
currencysourceHuman-readable provenance, e.g. "Global fallback · converted from USD" (resolvedLimitsSchema.source).
QuoteExecutionContext
sidefrom_assetto_assetamount_modeWhich side of the requested route amount was supplied.
from_amountRequested source amount in the source asset's major units, when supplied.
to_amountRequested destination amount in the destination asset's major units, when supplied.
countryCountry supplied for route eligibility, when supplied. This is not proof of residency.
payment_methodPayment method supplied as a caller preference, when supplied.
from_networkSource crypto network supplied for the route, when supplied.
to_networkDestination crypto network supplied for the route, when supplied.
QuoteOffer
A provider's identity and, where the caller is pricing or reviewing capabilities, its registry-published limits — reused verbatim on Quote.provider, Order.provider and QuoteProviderResult (D-26, D-56). Capabilities.providers[] (product=buy_sell) uses CapabilityProvider below — a flat schema that duplicates these fields rather than an allOf extension of this one — so its own status vocabulary never collides with QuoteProviderResult.status, and so composing a second closed schema on top of this closed one never produces an unsatisfiable combined schema (K6b — see CapabilityProvider's own description).
offer_idOpaque provider offer identifier; preserve it verbatim.
rateDecimal string, from_asset per to_asset (BSR-9) — the server's own final_in's decimal value ÷ final_out's decimal value for THIS offer, computed BEFORE either is rounded into the published Money fields below (when final_in is absent it equals the request's from_amount), in the SAME two request currencies, computed identically for every offer in this list regardless of provider or side (never a provider's own internal pricing field, whose basis is not uniform). Because rate is derived pre-rounding, rate × final_out.amount (scaled by final_out.decimals) can differ from final_in.amount in the last significant digit(s) when an asset's published Money.decimals is smaller than the server's own internal precision — do not treat that identity as exact. Informational only — NEVER compare offers by rate (rounding/precision differ by asset). Compare by final_out for a from_amount request (every offer targets a different final_out for the same pay amount). For a to_amount request, compare by final_in only among offers whose final_out equals the requested to_amount (lower final_in is better there) — an offer whose final_out differs is not comparable this way; not every provider honors an exact-output target.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
Fee breakdown, field names from quoteFeesSchema (packages/contracts-exchange/schemas.ts). Each leg converts to a minor-unit Money object at the /v1 boundary per RESOURCE-MODEL §0.3; network_currency (the zod schema's optional currency-of-the-network-fee field) is folded into network.currency rather than published twice.
expires_atThis offer's expiry; re-quote after it expires.
The non-PII request facts used to produce this offer. It binds the offer to the caller's exact route intent (assets, amount side, optional country, payment method and networks); it is descriptive only and does not authorize execution. Order creation must revalidate the quote, offer_id and all risk controls.
payment_method^[a-z][a-z0-9_]*$Canonical payment method priced for this offer, when the provider returned one. Never infer it.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
Field names from resolvedLimitsSchema. min/max share one currency rather than each carrying their own Money object, matching the zod source's flat shape verbatim — RESOURCE-MODEL §2.4 lists this field as resolved_limits{min,max,currency,source}, not as two nested Money objects. A1-4 — this schema is ALSO Capabilities.defaults.resolved_limits' shape (a producer outside this item's file ownership); min/max gained a pattern here rather than becoming Money objects, so that other producer's existing plain-decimal output is not broken by a shape change it was never part of fixing.
eta_secondsQuote
id^qt_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectsideThe winning route's provider (D-26).
rateDecimal string, from_asset per to_asset (BSR-9) — the server's own final_in's decimal value ÷ final_out's decimal value for the winning offer, computed BEFORE either is rounded into the published Money fields below (when final_in is absent it equals the request's from_amount), in the SAME two request currencies, computed identically for every offer in this response regardless of provider or side (never a provider's own internal pricing field — Bridge's and Coinbase's own rate math disagree on this basis by side, which is exactly the bug BSR-9 closed: docs/api/CHANGELOG.md). Because rate is derived pre-rounding, rate × final_out.amount (scaled by final_out.decimals) can differ from final_in.amount in the last significant digit(s) when an asset's published Money.decimals is smaller than the server's own internal precision — do not treat that identity as exact. Informational only — NEVER compare offers by rate (rounding/precision differ by asset). Compare by final_out for a from_amount request (every offer targets a different final_out for the same pay amount). For a to_amount request, compare by final_in only among offers whose final_out equals the requested to_amount (lower final_in is better there) — an offer whose final_out differs is not comparable this way; not every provider honors an exact-output target.
What the caller receives (bestQuoteResponseSchema.finalOut).
Fee breakdown, field names from quoteFeesSchema (packages/contracts-exchange/schemas.ts). Each leg converts to a minor-unit Money object at the /v1 boundary per RESOURCE-MODEL §0.3; network_currency (the zod schema's optional currency-of-the-network-fee field) is folded into network.currency rather than published twice.
expires_atQuotes expire — re-quote rather than reusing a stale one.
Field names from quoteOfferSchema.checkoutReadiness — tells the caller how the eventual order will complete.
quote_statusindicative came from a warm cache without running the live fan-out — re-quote live before acting on it once older than the safety window (bestQuoteResponseSchema.quoteStatus).
offer_idOpaque identifier of the winning offer; preserve it verbatim for selection.
payment_method^[a-z][a-z0-9_]*$Canonical method priced for the winning offer. This is requested funding intent, not proof of the eventual received settlement rail. Absent means unknown.
What the caller pays, when it differs from the request amount (bestQuoteResponseSchema.finalIn).
Field names from resolvedLimitsSchema. min/max share one currency rather than each carrying their own Money object, matching the zod source's flat shape verbatim — RESOURCE-MODEL §2.4 lists this field as resolved_limits{min,max,currency,source}, not as two nested Money objects. A1-4 — this schema is ALSO Capabilities.defaults.resolved_limits' shape (a producer outside this item's file ownership); min/max gained a pattern here rather than becoming Money objects, so that other producer's existing plain-decimal output is not broken by a shape change it was never part of fixing.
eta_secondskyc_levelquoteOfferSchema.kycLevel — the identity tier this route requires, when known.
Field names from routeFactsSchema — comparison context for a poor-value or no-KYC-eligible alternative.
indicative_as_ofWhen the indicative price was actually fetched — never "now" (bestQuoteResponseSchema.indicativeAsOf).
Every fully priced offer from the same call, including sibling payment methods and providers. This additive list is the priced method surface; providers[] remains the terminal one-row-per-provider diagnostic summary for backward compatibility. An offer omitted from this list was not safely priced and must not be inferred from capabilities — including when the reason is this gateway being unable to verify that offer's own currency/asset decimal scale (fixer round 1, 2026-09-22), a money reason, not a rate one. If payment_method is absent, the provider did not return a canonical method and the client must not present that entry as a selectable named method.
Terminal outcomes from this call's existing provider offers and diagnostics (D-21, BS-G2). No additional provider requests are made to populate these rows. Reasons are public codes; raw diagnostics and provider payloads are never exposed.
price_change_threshold_bpsThe basis-point move that triggers a re-quote prompt on the client (today a server constant, 50) — D-27.
OrderCreateRequest
quote_id^qt_ · requiredA Quote.id from a prior POST /v1/quotes.
offer_idSelects a specific QuoteProviderResult offer instead of the winning route, when the caller reviewed the fan-out and picked a different provider.
external_account_id^ba_REQUIRED for a Bridge-native sell (execution_context.side: sell on a backend_native offer): the saved bank destination the fiat payout settles to, from GET /v1/wallet/external_accounts. It must belong to this account's own Bridge customer, and its currency and rail must match the quote's to_asset and the offer's payout rail — a mismatch is refused, never reinterpreted. Never accepted on a buy or swap.
wallet_addressCrypto destination for a buy. NOT accepted on a Bridge-native sell (400 invalid_request, param: wallet_address): Bridge matches the incoming deposit from ANY sending wallet, so a declared source address would be a promise the server cannot keep. Send from any wallet you control to the address in the created order's deposit_instructions.
emailF-5: ignored. No path reads this field — a hosted checkout's identity is built from this account's own verified email (public.users), never from the request body, and the internal action this route calls has no email parameter to forward one to. Ignore any value sent here; do not build client logic on it being used. Kept on the wire in case a future provider path needs a caller-suggested address, not because one exists today.
Order
id^ord_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectstatusA1-2: open — a new transactions.status value is a real, growing set (see K6/BL-36 history below); an unrecognized member is an opaque string, never a deserialization failure. transactions.status verbatim (D-22) — transactions/recovery.ts's ACTIVE_STATUSES + TERMINAL_STATUSES + UNCLASSIFIED_STATUSES vocabulary (19 values), widened from the seven the approved R15 §3 screens draw on (K6 fix: the original enum only covered the seven approved terminal/active screens and rejected every other real transactions.status value as a contract violation). BL-36 (2026-09-16) adds initiated (3 prod rows) and abandoned (1293 prod rows) — the DB status_check constraint has allowed both since 20260708201001_transactions_status_add_refunded.sql, but the vocabulary here and in recovery.ts never caught up: orders.get 500ed on either status (respondValidated rejects a status the enum does not list) and a page of orders.list containing one failed the same way whenever that row also carried a quote_id. abandoned joins TERMINAL_STATUSES; initiated is admitted here for contract purposes only and is deliberately left in recovery.ts's UNCLASSIFIED_STATUSES — prod shows every live initiated row already carries an on-chain tx_hash (one is metadata.lifecycle_stage: 'completed'), so it is a stranded post-broadcast swap the DEX reconciler never picked up, not a start-of-lifecycle "no money moved" state; see recovery.ts's comment for the full writer/reconciler trace. Never conflate with failure_kind — the failed screen's "Status code" must read failure_kind, not this field (BS-G9).
sidequote_id^qt_An opaque reference to the quote this order came from, when one exists. OPTIONAL — ABSENT, never a fabricated id, for an order that has no producer for it (a live write-path gap on some provider-native orders, or a swap that never went through a quote at all; see the API changelog for current prod scope). NOT resolvable via GET /v1/quotes/{id} for a historical order: this value can reference quote_attempts.rid, a different table than the one that endpoint reads, while an order created through POST /v1/orders carries a GET /v1/quotes/{id}-resolvable id instead.
rateDecimal string, from_asset per to_asset, the SAME basis as QuoteOffer.rate (BL-28, BS-10; fixer round 2, P1, independent review 2026-09-20 — round 1 published this on Bridge's own native, opposite basis, which collided with sibling PR BSR-9/#3357's provider-agnostic wire contract). BSR-10 gives this a live producer for a Bridge-native buy order: read rate_basis, below, to know which of two figures this is — never assume one from this field's mere presence. The planned read-time source (transactions.quote_id -> quotes.id -> quotes.offers) is a dead join in production — transactions.quote_id has referenced quote_attempts.rid, not quotes.id, since 20260210100000_fix_transactions_quote_fk.sql, and quote_attempts carries no rate/offers column. BL-48 re-examined this for orders.create's Bridge-native path (BL-47), which prices via api_quotes (a genuinely joinable row, unlike quotes) — still ruled out as a read-time source, since api_quotes is quote-cache infrastructure with its own TTL, and reading it back from orders.get/.list days after settlement, for a row a future cleanup job may have already pruned, would make this field's presence flicker on a purge schedule having nothing to do with the order itself. BSR-10 uses a WRITE-time source instead: createBridgeNativeOrder's execute-transfer call opens a server-sealed route claim and bridge-core.ts derives the persisted rate from THAT claim, inverted onto this wire basis (never from a request-body value), onto the transactions row itself at handoff (baseMetadata) — durable transaction history, never a re-derived read, and never a caller-supplied figure. Once settlement writes the observed to_amount (bridge-webhook/index.ts's transfer.payment_processed handler) AND Bridge's own transfer receipt proves the actual settled source amount, this field is RECOMPUTED as that receipt-proven source amount divided by the observed to_amount — a provider can legitimately re-price between quote and arrival for some corridors, so the settled figure is never assumed equal to the quoted one even when they agree; rate_basis flips to at_arrival in the same response. A row whose receipt never proves that source amount, or that reaches a terminal status without ever settling, publishes neither rate nor rate_basis rather than a stale or unproven figure. Still absent for every other order — a Sell/Swap order, a non-Bridge provider, or a Bridge buy this fix's write-time source never reached (a row created before this deploy) — never guessed.
rate_basisQualifies rate (above; both regimes share the same from_asset per to_asset basis). quoted: the taken offer's quote-time price, published only while the order is still active — the persisted estimate, never the final word; a terminal row without a receipt-proven settled figure publishes neither rate nor rate_basis (fixer round 3, P2, independent review 2026-09-20 — this paragraph had drifted to say the opposite of what buildOrderRateProjection actually gates on; rate's own description above, and RESOURCE-MODEL.md §2.4, already stated the gate correctly). at_arrival: the REALIZED rate, Bridge's own receipt-proven settled source amount divided by the observed amounts.receive once its transfer receipt lands — never assumed equal to the quoted figure, since a provider can legitimately re-price between quote and arrival for some corridors (Bridge's own "rate fixed on arrival" settlement semantics this field exists to name structurally, replacing a copy caveat with a field). Published alongside rate for a Bridge-native buy order (BSR-10); still absent alongside it for every other order, for the identical reason.
A provider's identity and, where the caller is pricing or reviewing capabilities, its registry-published limits — reused verbatim on Quote.provider, Order.provider and QuoteProviderResult (D-26, D-56). Capabilities.providers[] (product=buy_sell) uses CapabilityProvider below — a flat schema that duplicates these fields rather than an allOf extension of this one — so its own status vocabulary never collides with QuoteProviderResult.status, and so composing a second closed schema on top of this closed one never produces an unsatisfiable combined schema (K6b — see CapabilityProvider's own description).
payment_methodThe payment method originally requested for this order (transactions.payment_method, e.g. sepa/card/ach_push), verbatim — BL-48. Not restricted to a fiat leg: a Bridge buy funded from a crypto balance can carry a crypto method here too (e.g. bitcoin/ethereum) — prod carries 184 such rows. Absent when the row predates this column or the value was never set. Distinct from payment_rail: this is the REQUEST, not necessarily what settled — see that field.
payment_railBridge's own OBSERVED settlement rail for a Bridge order (transactions.payment_rail, BL-48) — starts equal to payment_method at creation and is overwritten once bridge-webhook/index.ts's buildBridgeTransferUpdatePayload resolves one (from transfer.created onward, not only once settled), so the two can genuinely diverge. Published ONLY for provider.id: bridge orders (fix-pack, independent review): the same column carries Transak's own private rail vocabulary (order.paymentOptionId, e.g. credit_debit_card) for a Transak order, which is not Bridge's vocabulary and disagreed with payment_method on the method itself on 448 prod rows — publishing it under this Bridge-documented field for another provider would misrepresent what the customer paid with. Never inferred — published verbatim from the column, or absent when it was never set or the provider is not Bridge.
The checkout's own money lines (R15 §4: "You pay" / "You receive"). The exact sub-field set is not yet spelled out beyond this pairing in RESOURCE-MODEL — flagged for confirmation rather than invented further. Fixer round 1 (2026-09-22) — either leg is WITHHELD, never published at an unverified decimal scale, when this gateway cannot verify that leg's currency/asset — see docs/api/CHANGELOG.md's entry of the same date. This rule is identical on every order.* event's own amounts (GET /v1/events, the SSE stream, GET /v1/activity) — each leg withholds independently there too, and amounts is absent from the event only when NEITHER leg resolved, never when just one could not be verified — see docs/api/CHANGELOG.md's L5-FIX-event-money-parity fixer round 1 entry (2026-09-22).
money_stateOPTIONAL — absent when this row has never been classified (BL-38, #3086): the writer is the Paybis/Bridge failure-classification pipeline, gated on failed/cancelled Paybis rows only, so most rows never receive one. When present, read it together with money_status: money_state says whether funds were ever captured, money_status carries refund truth. A row can read status: completed while money_state reads never_authorized (D-10). Never infer a money state from status alone, and never treat its absence as never_authorized — that is exactly the fabricated-default bug this field's optionality fixes.
money_statusDrives the "Money:" line on the terminal screens (R15 §3). Every bank rail resolves to unknown; only a card rail is ever positively not_charged.
failure_kindThe seven normalized failure kinds (R15 BS-G9, prod-inventory §1.4). This is the field the failed screen's "Status code" must read from — never status.
failure_reasonSafe, human-readable copy for failure_kind — never a raw provider error string.
return_reasonWhy a settled leg was sent back, as the provider itself worded it (e.g. Receiver Name Mismatch, Too Many Recent Transactions) — the returned leg is a fiat pay-in on a buy, and either the crypto deposit or the fiat payout on a Bridge-native sell (L1-5). Present only on an order whose money actually arrived and was then returned — distinct from failure_reason, which is OUR safe copy for a failure_kind. Same field and same contract as payments.return_reason on a payment link, so one integration handles both. Provider vocabulary is open-ended and NOT an enum: branch on failure_kind/status, and treat this as display and support text.
What the caller must do next before this order can advance, when anything is required of them at all. Present ONLY while a hosted-provider checkout is open and reachable: the order is in an active status, a hand-off URL exists for it, that URL has not expired, and its host is one this API recognises. ABSENT — never null, never a stale URL — the moment any of those stops being true, including on a Bridge-native order (which completes in place and publishes bank_deposit_instructions or deposit_instructions instead). A caller that receives no next_action on a hosted order whose status is still active must request a new quote; there is no way to re-mint a checkout session for an order that already has one. L1-6b (contract only, no producer yet): every route stays silent on this field until L1-6c/d build the hosted-checkout branch and its resume. Published by orders.create's 201 and GET /v1/orders/{id} only — GET /v1/orders never carries it, for any row.
offer_idThe offer actually executed for this order (transactions.offer_id), verbatim. Normally the offer_id the caller sent, or the quote's winning offer when none was sent; it can differ when the selected offer was not executable at order time and the server fell through to the next-ranked one — see offer_selection. Absent on an order created before this field existed or by a path that records no offer.
Why THIS offer ran, when that is not simply "the one you asked for". Published on the 201 from orders.create only — it is a record of a creation-time decision, and a later GET of the same order republishes offer_id but not this object. Absent when the executed offer is the one the request selected.
Present on a self-custody sell that needs crypto sent to a provider-issued address before payout — the shared DepositInstructions component, same shape as wallet.deposit_intents.deposit_instructions (D-24, BS-G7).
Present on a Buy order awaiting a bank-rail payment — the deposit slip (IBAN/account number, beneficiary, reference) the payer sends fiat to (BL-19). Reuses the shared BankDepositInstructions component verbatim rather than a second same-named shape, so this never drifts from the identical projection payment_sessions/payroll funding already publish. UNLIKE payout_destination below, this is deliberately the full, unmasked provider payload (D-24): the payer must have the real IBAN to actually send funds to it. Bound by the same D-24 rule as every BankDepositInstructions consumer — never logged, never placed in a URL or query string.
Present on a buy, sell or swap once its settlement destination is known (the buy crypto branch, BL-48, added below). The bank branch is masked the same way as payouts.beneficiary's output projection — never the full account number, never a public link or token (D-24); a sell order never itself becomes a payout object. The crypto branch (chain/address/tx_hash, BL-19; buy, BL-48) is exclusive with the bank branch: a self-custody payout is on-chain and has no account number to mask, so address is published verbatim (BS-G7's own deposit_instructions.address precedent). tx_hash on the buy branch is read from the SAME Bridge transfer receipt (bridge_data's receipt.destination_tx_hash) the amounts.fee producer reads (fix-pack, independent review — 66 of 70 completed Bridge buys carry one); absent before the transfer settles or on an older payload shape that predates the field, never guessed.
OrderList
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.
OrderEventList
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.
CapabilityCorridor
idcurrencyfiat_raillifecyclepublic_claim_statusstatusThe corridor's eligibility state for this account. in_review and gathering_no_path are never the same answer (V-G1, D-48).
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
The Travel-Rule ceiling for this corridor (§2.2 v2 amendments). On crypto_relay, the per-invoice cap: a payer is refused above it, and payment_links.activate refuses a link it would leave with no payable rail (422 no_payable_rail).
source_chainssender_displaylegal_entity_namePresent only where the settling entity differs from the account holder (e.g. a EUR SEPA IBAN held by the provider's EEA entity).
eta_secondsexecutableAvailability, never authorization — the server re-checks at create and at the money boundary (RESOURCE-MODEL §0.10).
blocked_reasonA machine-readable code for a corridor whose status alone does not say enough — read it per status, never generically. For a payouts corridor, handlePayoutsEligibility's own blocker classification (lifecycle_blocked, minimum_unconfigured, rail_not_enabled, route_unavailable). For a wallet_bank wallet_bank_virtual_account corridor (receive by bank, W-R23), rail_not_enabled with status: not_enabled means the rail is not launched for this account (the wallet_bank_virtual_account_enabled flag does not admit it) — no verification step opens it, so action is none. route_unavailable on that same corridor means the flag admits the account but its active Tempo wallet is not on Tempo mainnet (Bridge settles the deposit as USDC there) — on a staging project whose Bridge runs in its sandbox, not on that project's own test network; POST /wallet/virtual_accounts refuses the identical wallet with 409 network_not_supported, so this is never published alongside an available a create could not honour. collection_account_uses_currency (W-R33) on that same corridor, with status: not_enabled and action: none: a Payment links collection account already receives this currency into this account's Tempo wallet, no activated wallet-funding account does (the create would reuse that one), and the provider hands the collection account back instead of issuing a wallet-funding one — POST /wallet/virtual_accounts refuses the same currency with 409 conflict before any provider call. It outranks the verification rungs (no verification step frees the slot) but never rail_not_enabled or route_unavailable. payin_fiat_pending: a fiat pay-in corridor (payment_links' own kind: 'bank' rows and a launched wallet_bank_virtual_account; never payroll) of an individual account whose Bridge fiat pay-in activation (bridge_customers.capabilities.payin_fiat) is not exactly active even though KYC and endorsement already cleared — a separate Bridge step (C4-D23, docs/capabilities/CAPABILITIES-CANON.md §1.8) status alone cannot distinguish from any other gathering_no_path cause. This is the SAME underlying condition rail_not_allowed's details.reason publishes as merchant_fiat_payin_pending (root.yaml) at the payer's execution-time error boundary — same fact, two scopes: this field describes the account's OWN capability resource, that one describes a payer's refused attempt against a merchant's link. relay_requires_tempo_mainnet (RELAY-ENV-1-T, P-72), only on the payment_links crypto_relay corridor, with status: not_enabled and source_chains: []: Relay routes are admitted (founder_accepted or proven) only into Tempo mainnet, and this deployment settles Payment links on a Tempo test network (dev and staging), or its Tempo network could not be resolved, so the payer page never offers Relay here whatever the flags say. It outranks rail_not_enabled; max_amount is still published while the cap is set. Production (Tempo mainnet) reads it only if its Tempo network cannot be resolved. Absent for every corridor this does not apply to. Open enum: a client tolerates a value it does not know.
review_categoryWhether the rail row gets an action button at all (RailReviewCategory, R17 §3).
display_labelThe one machine-readable label every client renders, so in_review and gathering_no_path never collapse to the same text again (D-48).
detailThe human sentence for status's current value (K6b, §52 C4-D14) — one fixed sentence per CorridorEligibilityStatus, never a per-reason variant, so it never claims more than status itself already promises. One exception: a wallet_bank_virtual_account corridor with blocked_reason: rail_not_enabled reads Receive by bank is not launched yet. (the rail is off for everyone it does not admit, not for this account in particular); with blocked_reason: collection_account_uses_currency it reads A Payment links account already receives <CURRENCY> for this wallet.
actionWhat the caller can do about this corridor right now (K6b, §52 C4-D14), derived from status and, when a richer signal exists, overridden by it: for a payment_links/payroll/wallet_bank corridor the resolver's own next_action, for a payouts corridor handlePayoutsEligibility's own blocker (kyc/endorsement -> verify; address/tos/source_of_funds/additional_details -> provide_details; capability -> none). Never invented: a corridor with no such signal named falls back to the coarse status default alone (Codex review, PR #2989).
requested_atReserved for when this corridor's endorsement was requested (K6b, §52 C4-D14) — always absent today. Bridge stores no per-endorsement request timestamp anywhere in this codebase (bridge_customers.bridge_data.endorsements[] carries name/status/requirements only), and bridge_customers.updated_at bumps on ANY write to the customer (KYC, ToS, an unrelated endorsement), so it is not a safe per-endorsement proxy — publishing it would misreport an old request as freshly made whenever something else on the account changed (Codex review, PR #2989). Left in the schema, nullable, so a future PR can populate it once a real per-endorsement timestamp is persisted, rather than never having a field to fill.
CP-T2 (CP-G16, D-63) — present only on the crypto_tempo corridor of product=payment_links. Whether THIS account can activate a crypto-only link (settlement to its own Swaps Wallet, K13 §4) right now, judged by the same account-level gates payment_links.activate enforces, in the same order: the payment_links_crypto_only flag for this merchant, the account's own status, the crypto_tempo rail and its fee wallet, and the account owner's active Tempo wallet on a Tempo network. No Bridge customer, KYC or endorsement is read or required. The corridor's own status keeps its meaning (the rail is on for this deployment, which a saved Tempo address also uses); crypto_only refines it and is never available while that status is not. Link-level checks (USD currency, attestation, rail restriction) still run at activation. Availability only, never authorization.
CapabilityCryptoOnly
statusThe corridor vocabulary: available, action_required (only wallet_not_found — the account can create its Swaps Wallet), not_enabled (every other reason; nothing the caller can do in this API).
display_labelThe D-48 label for status, the same map every corridor uses.
requires_bridgeAlways false — the crypto-only path never needs a Bridge customer (K13 §4 step 7).
blocked_reasonThe first closed gate, absent when status is available. crypto_only_not_enabled: the payment_links_crypto_only flag does not admit this merchant. account_inactive: the account is not active. rail_not_enabled: the crypto_tempo rail is off or has no fee wallet. wallet_not_found: the account owner has no active Tempo wallet (activation answers 409 wallet_not_found). settlement_destination_unsupported: that wallet does not resolve to a Tempo network this account can settle on (activation answers 422 invalid_request, or 422 settlement_rail_unsupported for a tempo-mainnet wallet on a non-livemode account).
CapabilityPayrollCryptoFunding
availablereasonPresent only when available is false; the first closed gate. crypto_funding_not_enabled: crypto funding is switched off for payroll (a run asking for it answers 409 capability_unavailable). bridge_funding_currency_unsupported and verification_required mean what they mean on the parent funding_currencies[] entry.
assetThe asset to send, e.g. USDC.
chainThe chain the payroll wallet's deposit address is on, e.g. solana.
CapabilityTempoWalletSettlement
availableviareasonPresent only when available is false. tempo_via_bridge_not_enabled: the payment_links_tempo_via_bridge flag does not admit THIS merchant — off entirely (the seeded default), or a per-merchant allowlist that does not list this account's public.users.id. verification_required: the flag admits this merchant (platform-wide, or this merchant is allowlisted) but this account's own bank corridor for the currency (corridors[] above, same currency) has not itself resolved to available — Bridge requires that same endorsement to execute the FX leg. tempo_rail_not_enabled (via: splitter, USD only): the non-custodial crypto_tempo rail's own kill switch is off (fixer round, findings #5/#11) — never fabricate available: true for a rail nobody can actually use. wallet_not_provisioned/wallet_network_not_supported (via: bridge only): the flag admits this merchant, but the account either has no active Tempo wallet on file, or its wallet is not on THIS project's Tempo network (tempo-mainnet in production) — the SAME two gates payment_links.activate's wallet-via-bridge branch checks, read from the SAME wallet_entities row, so this field and that write path can never disagree about which merchants can actually activate.
Capabilities
versionShape varies by product: payouts publishes {kind, bps, applies_to}; payment_links publishes {kind, bps, applies_to, rails, rounding, rounding_decimals}; payroll publishes {bps, shape, enabled} (employer-funded-on-top, never a percentage deducted from the recipient — §2.5). All keys are optional here to cover every shape.
payment_links — the Swaps fee on the splitter rails named in rails (crypto_tempo, crypto_relay), the same constant the server charges: applies_to: payer — added on top of the invoice and paid by the payer; the merchant receives the full invoice. For an invoice of amount, the payer's deposit instructions charge fee = floor(amount × 10^rounding_decimals × bps / 10000) / 10^rounding_decimals (a USD invoice with at most two decimals is never rounded); Payment.fee and Payment.amount_expected on these rails publish the same figures at that scale. It applies to crypto subscriptions too: every subscription invoice is a crypto-only payment link paid on these rails. A rail not listed in rails carries no fee claim from this object (no rate, no bearer); the fee on a Bridge-routed rail (bank rails, crypto_bridge) is deducted from what the merchant receives and is published separately as deducted_fee. Provider fees (Relay relay/gas, Bridge network/processing) are not this fee and are quoted per payment. Present whatever the rails' current availability (corridors[]).
product=payment_links only — the Swaps fee on the Bridge-routed rails named in rails (bank rails and crypto_bridge): applies_to: merchant — the payer pays exactly the invoice and the provider deducts this percentage from what arrives, so the merchant receives what arrived minus it (founder ruling «Bank: 1% is deducted from what you receive»). This is the rate configured now; the figure a payment was charged, to the cent and rounded up, is that payment's own Payment.deducted_fee, read from the provider receipt (null there when the payer paid in another currency than the invoice — crypto_bridge's USDC source included — or no consistent receipt was recorded). Omitted when no fee is configured.
product=payouts only — the «Pay with» sources for this caller: external_wallet (USDC from any wallet, always available) and swaps_wallet («Wallet balance», coming_soon while its kill switch is off; unavailable to a business key and in test mode).
product=buy_sell only — the resolved first-load defaults (exchange_bootstrap, D-23, BS-G4).
product=buy_sell only — the country × method × provider availability grid (D-23, BS-G5).
product=wallet_bank only — the currency → endorsement → status map (§2.3 v2 amendments).
product=payroll only (BL-60, C4-D37) — for each payroll fiat currency (the SAME set supabase/functions/payroll/lib.ts's FIAT_CURRENCIES accepts as a run currency), whether Bridge can issue a FUNDING account for it today, with a reason when it cannot. Additive: the run-currency picker keeps every fiat currency (the founder rejected removing COP — a LIVE bank_bridge/co_bank_transfer payout corridor, see corridors above — over dropping it just because it has no Bridge funding rail yet) and now reads this array to warn honestly up front instead of the gap surfacing only after a run is created. funding_account_available is the bank-transfer method; crypto_funding (PAYROLL-CRYPTO-1) is the USDC method for the same currency, and its absence means the USDC method is not available.
product=payment_links only (§52.33, PL-TEMPO-BRIDGE-1-T) — for each link fiat currency (the same currencies the bank corridors above cover), whether a link in that currency can settle its Tempo-wallet destination and through which rail. An entry without tempo_wallet.available: true reads as not available for that currency today.
product=wallet_bank only — the V1 ceiling ($3,000, §2.3 v2 amendments).
degradedproduct=buy_sell only — true when this read is not backed by a fresh route snapshot: the upstream hop (best-quote) was unreachable or itself had no fresh route-truth lease (NB-5). providers[] is still a live, independent read; defaults/coverage are OMITTED — never an empty grid or a fabricated "nothing is available" claim, and never a permanent 503 for a condition that cannot change.
degraded_reasonPresent only when degraded is true. Reserved: unavailable_in_test_mode is never emitted since A4-FIX-6; a test-mode caller gets 503.
MonthlyVolumeBand
An enum id, never a raw number — closes the label/midpoint disagreement bug class named in V-G14 (D-53). RESERVED (L3-2, 2026-09-21): not referenced by eligibility.get today — that operation is public/ unauthenticated only (D-16 forbids this field on any anonymous path), so this enum is kept, unreferenced, for the authenticated eligibility variant a future item builds once the router's optional-auth route class exists (see paths/platform.yaml's eligibility.get description).
Eligibility
countryThe normalized (upper-cased) country the caller passed.
customer_typedisclaimersMachine-readable disclaimer codes (fix round 1, P2) — mirrors resolveEligibility's own disclaimerKeys (src/lib/eligibility/engine.ts), stripped of the complianceNav.disclaimer. i18n prefix. not_advice is present on every response; approximate when any product's confidence is approximate; eea for an EEA country; sanctioned/unknown_country replace the other three for those two hard-block branches (fix round 2, P3 corrected the spelling — round 1 shipped the reversed country_unknown, which is actually the SEPARATE reasons[] code this same branch publishes; disclaimers[] and reasons[] are deliberately different vocabularies and this was their one accidental collision). Never omitted — an empty array would itself be a false "no caveats" claim on the operation this dark-flag legal sign-off exists to gate.
Customer
id^cus_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectcustomer_typeverification_stateSwaps' own verification-state vocabulary (R17 §3): not_started (verification not yet begun), pending (submitted, awaiting review), under_review (a review is actively in progress), approved (cleared), rejected (declined — see rejection_outcome/rejection_guidance), stale_mapping (our record of this customer needs an operator to repair it before verification can continue), action_required (a requirements_due[] entry is blocking progress). Normalized to these seven values today, but the field is marked forward-compatible (x-swaps-open-enum) — treat an unrecognized member as opaque rather than a deserialization failure.
requirements_outstandingCount backing requirements_due[].
endorsementsThe payment rails this customer is cleared for (R17 §3) — Swaps' own rail codes: base (the crypto/stablecoin base rail every customer needs first), sepa (EUR via SEPA), spei (MXN via SPEI), pix (BRL via Pix), faster_payments (GBP via Faster Payments), cop (COP via bank transfer) — the array is open: a rail code outside this list is possible and must be ignored, never treated as an error.
Per-rail status — one entry per rail for which the upstream snapshot carries a per-rail status object, including rails not yet approved; it can be empty even when endorsements[] is not. Each item names its own rail in endorsement; never pair by index with endorsements[], which lists only the approved subset and can be shorter or differently ordered.
next_stepThe single next step that unblocks verification (source text for get_verification_status).
tos_statusTerms-of-service acceptance status, as our verification partner reports it today — not yet normalized onto a closed Swaps enum (unlike verification_state above); treat any non-approved value as "not yet accepted" rather than enumerating every possible string.
kyc_statusKYC/identity-verification status, as our verification partner reports it today — already folded into verification_state above for the canonical read; not yet normalized onto its own closed Swaps enum.
available_actionsstaleRestart onboarding — survives to the wire, never collapsed into another state.
degradedCached — we could not reach the provider. Survives to the wire, never collapsed.
rejection_outcomeAn outcome enum converted server-side from developer_reason — never the raw provider reason (RESOURCE-MODEL §2.4). Known values include verification_unavailable and terminal; the full set is not yet published upstream.
rejection_guidanceBounded, safe-copy only — never developer_reason passthrough (V-G3).
compliance_review_stateassociated_persons_countA count, never a name — the only UBO fact on the base object (V-G4).
CustomerCreateRequest
customer_typeWhich kind of verification to start: a natural person, or a business. Never written to the account: Account.customer_type follows the type Swaps records for the customer, which is this one for a new customer and the customer's own type when our verification partner already holds a customer for the account's owner (the 201 body's customer_type shows it).
country^[A-Z]{2}$ISO 3166-1 alpha-2, when known — validated against the SAME market registry POST /v1/accounts validates it against (400 invalid_request, param: country, for a well-formed but unrecognised code — fix-pack, independent review, P2), then persisted ONLY when the account has no country on file yet; a country the account already declared is never silently overwritten. Does not yet select a payment rail for this customer — a future PR can map country to a rail once product confirms which one.
legal_namecustomer_type: business ONLY — the registered company name, trimmed, forwarded to our verification partner as the entity's own legal name. Not personal data (round-2 architect ruling, 2026-09-21): a business's registered name is fine to accept from the caller, unlike a natural person's legal name, which this endpoint never accepts from any request body. 400 invalid_request (param: legal_name) when customer_type is individual. Omitted, a business customer is created without a name on file — same treatment an individual with no name on file gets.
CustomerVerificationLinkRequest
kindOne polymorphic mint across every hosted verification hand-off (D-52, V-G17): tos (terms of service), kyc (identity/business verification), business_questionnaire (compliance questionnaire), business_ubo (beneficial-owner check), remediation (a requested fix to a prior submission).
return_toRequired, and validated same-origin (https://swaps.app/https://www.swaps.app) — but NOT YET threaded into the underlying hosted hand-off, so the dead end where every live hand-off lands on /confirm instead of the hub (D-52, V-G18, prod P-B3) is not yet closed. This field is accepted and checked, not silently ignored, but is not yet "honoured" in the sense of actually steering the redirect; that wiring is tracked as a follow-up.
AssociatedPerson
label"Owner 1", "Owner 2", … — never a raw internal object id, never a name (CAPABILITIES-CANON owner-task rule).
namePresent only when the caller is the holder's own delegated session (dashboard_session); always null for a business key or a third-party agent.
ownership_percentOmitted, never defaulted to 0, when our verification partner's raw per-person payload does not carry a recognized ownership field. A client must render the absence honestly (e.g. "—"), never as an observed 0% — the contract trap issues #3086/#3088 name (a server default is not an observation).
statusPer-owner verification status. Not yet given a fixed Swaps enum — optional and omitted until a confirmed per-person status field exists upstream; today no source in this codebase reads one (only associated_persons.length is read). A follow-up derives it from the same endorsement missing[] object-scoped bundles the Verification Hub already reads, once a live upstream response confirms the shape.
AssociatedPersonList
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.
ComplianceProfileRequest
account_purposeThe union of the individual (11) and business (13) account_purpose values (16 unique — some overlap). Which of these are valid is further restricted by the customer's own customer_type; a mismatched-but-listed value still gets 400.
account_purpose_otherFree text — only meaningful (and only read) when account_purpose is other. 400 past 1024 characters (never silently truncated) — see this schema's own description.
source_of_fundsThe union of the individual (12) and business (11) source_of_funds enum values (22 unique — one overlap, pension_retirement).
source_of_funds_descriptionFree text. 400 past 1024 characters (never silently truncated) — see this schema's own description.
employment_statusIndividual only — current employment status, Swaps' own six-value vocabulary.
expected_monthly_payments_usdExpected monthly payment volume through Swaps, banded in USD.
business_descriptionFree text — the business's own description of what it does. 400 past 1024 characters (never silently truncated) — see this schema's own description.
business_typeBusiness only — legal structure, Swaps' own seven-value vocabulary.
business_industry^[0-9]{6}$A single 2022 NAICS six-digit code identifying the business's industry — a public US federal classification standard, not a Swaps- or partner-specific list. Too large to enumerate here, so only the ^[0-9]{6}$ shape is checked at this layer; the closed set of valid codes is enforced server-side, never free text.
estimated_annual_revenue_usdBusiness only — estimated annual revenue, banded in USD.
high_risk_activitiesZero or more of 15 regulated-activity values (includes none_of_the_above).
ComplianceProfileResult
acceptedCustomerEventList
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.
CustomerList
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.
Wallet
id^wlt_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectaddressChecksum-cased Tempo address. Never lower-cased on read or write.
chainThe chain this wallet lives on. Today always Tempo.
networkThe Tempo network this wallet lives on, from the wallet record. tempo-moderato is Tempo's test network: what it holds is test money (pathUSD), never real funds. A project serves only wallets on its own network, so every wallet one environment returns carries the same value, and it is the same value WalletBalances.network reports. Use it to pick the token a balance, send or signature is about; never infer the network from the environment's name or a client build setting. chain stays tempo.
statusRESOURCE-MODEL §2.3 names only one live state.
labelWalletCreateRequest
addressThe Tempo address the client derived from its passkey credential. Case-preserved verbatim.
credential_idThe WebAuthn credential id already registered via the passkey key-manager, when more than one exists for this holder.
labelWalletList
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.
WalletBalances
chain_idThe chain this balance snapshot was read from. Today always Tempo.
networkThe Tempo network these balances were read on: the caller's wallet network (Wallet.network), or, when the caller has no wallet yet, the network this environment serves. tempo-moderato is Tempo's test network, so its balances (pathUSD) are test money. The primary token is USDC.e on tempo-mainnet and pathUSD on tempo-moderato; a client decides which balance can pay from this field, never from its own build.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
skipped_non_usdToken symbols excluded from usd_total because their decimals are not 6 — named so a client never reads the total as complete.
WalletTransactionList
chain_iddegradeddegraded_reasonWalletTransaction
hashchain_idstatusnot_found is a 200 body naming the lookup result, not itself an HTTP 404 — RESOURCE-MODEL §2.3 names it as a state of this resource.
fromtoPresent only on a transaction the wallet did not send whose decoded TIP-20 Transfer logs of the accepted tokens hold exactly one transfer touching the wallet; absent on a transaction the wallet sent.
symbolThe accepted Swaps-dollar token of amount; present exactly when amount is.
directionblock_numbertimestampcounterparty_labelThe caller's own address_book label for the counterparty address — same address_book lookup as WalletTransactionList.transfers[].counterparty_label (§52 C4-D14 / K5c). On a transaction the wallet did not send, the counterparty is the decoded TIP-20 Transfer log's sender (in) or recipient (out), as on the list, and null when several transfers touch the wallet. On a transaction the wallet sent it is the receipt-level to, which for every TIP-20 send is the TOKEN CONTRACT, not the recipient (WalletSendModal.tsx) — so that out lookup is null for essentially every real send (Codex review of #2988, P2: fail-closed, never a wrong guess). null when unsaved or when there is no counterparty address; absent only when status: not_found.
networkThe network this transaction happened on — same convention as WalletTransactionList.transfers[].network (§52 C4-D14 / K5c). Absent when status: not_found (no transaction to place on a network).
Same as WalletTransactionList.transfers[].origin, for a mined (success) in transaction; null on every other one. Absent only when status: not_found.
WalletTransactionOrigin
kindvirtual_account_idThe VirtualAccount.id (va_…) the deposit was made into. Never a provider id.
currencyThe currency the bank deposit was made in, uppercase — the account's destination.currency (e.g. USD).
railThe bank rail the account receives on — the same value and spelling as that account's destination.rail (ach for a USD account). Open enum: a client tolerates a value it does not know.
WalletRouteStatus
The 4-value funding-route status, exact match confirmed by R13-wallet §3 — it backs both deposit_routes and send_routes. A cross-network route is unsupported on a deployment whose wallets live on a Tempo test network; one that would otherwise be open is temporarily_unavailable while the server cannot confirm which Tempo network it serves.
DepositRoute
idsource_chainsource_assettarget_networktarget_assetstatusThe 4-value funding-route status, exact match confirmed by R13-wallet §3 — it backs both deposit_routes and send_routes. A cross-network route is unsupported on a deployment whose wallets live on a Tempo test network; one that would otherwise be open is temporarily_unavailable while the server cannot confirm which Tempo network it serves.
enabledtrue only when the server opens this route now; never on a Tempo test network, whatever the route's proof says.
proof_statusestimated_timeA human-readable ETA hint for this route.
fee_hintA human-readable fee hint for this route — not a computed quote.
messageA short server-computed line about this route's status; on a Tempo test network every route is unsupported and this names that network as the reason.
DepositRouteList
DepositQuoteRequest
source_chainsource_assetA Money value that must be strictly positive — used on every request field that creates or moves value.
DepositQuote
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
haircut_pctDecimal string, percent (e.g. "0.15" means 0.15%), never a float — mirrors developer_fee_percent's representation elsewhere in this file.
DepositIntentStatus
9 values (7 base + 2 recovery), confirmed exact by R13-wallet §2 W-G11 against the DB CHECK constraint — RESOURCE-MODEL §2.3's v1 draft omitted cancelled.
DepositIntent
id^din_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectstatus9 values (7 base + 2 recovery), confirmed exact by R13-wallet §2 W-G11 against the DB CHECK constraint — RESOURCE-MODEL §2.3's v1 draft omitted cancelled.
source_chainsource_assettarget_chaintarget_assetSingle-use, amount-bound — minted once per intent (RESOURCE-MODEL §2.3 D-2). Case-preserved verbatim.
expires_atOrdered transition history for this intent.
source_tx_hashtarget_tx_hashfail_reasonSet ONLY when the intent reached failed — §52 C4-D14 / K5c. Mirrors the already-recorded wallet_deposit_intents.fail_reason column (added 20260625150000, populated by the wallet-funding watcher). An OPAQUE, provider-defined (Relay) string: the raw failReason Relay attached to the request (e.g. SLIPPAGE on a tiny-deposit auto-refund) when it sent one, otherwise the Relay status word itself (refund/refunded/failure/failed) as a fallback so this is never blank. The watcher maps ALL FOUR of those Relay states to the SAME failed intent status — this field does NOT encode whether Relay auto-refunded the sender or hard-failed the request, and no vocabulary is guaranteed stable; do not pattern-match a specific string to infer "refunded" (Codex review of #2988, P2). null in every other state, including expired, cancelled and manual_recovery_required — the watcher only ever writes it while setting status: 'failed'.
DepositIntentCreateRequest
source_chainsource_assetA Money value that must be strictly positive — used on every request field that creates or moves value.
DepositIntentList
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.
SendRoute
idkindsource_chainsource_assetsource_token_addressThe source-leg TIP-20 contract address on Tempo. A direct_tempo send signs a transferWithMemo(address,uint256,bytes32) call against this address client-side (a plain send uses the 32-byte zero memo), since that route never touches /v1 at all; a cross_chain send never needs it directly, but it is exposed uniformly rather than splitting the schema by kind. This is always the live mainnet contract address, independent of any test-mode credential used to call this operation — route discovery does not vary by livemode. Returned exactly as stored, never lower-cased by the server — though for an EVM contract address this is a cosmetic guarantee, not a money-safety one: unlike BTC/Tron/Solana addresses, an EVM address is case-insensitive at the protocol level (EIP-55 checksum casing is a display convention, not part of the address).
destination_chaindestination_assetstatusThe 4-value funding-route status, exact match confirmed by R13-wallet §3 — it backs both deposit_routes and send_routes. A cross-network route is unsupported on a deployment whose wallets live on a Tempo test network; one that would otherwise be open is temporarily_unavailable while the server cannot confirm which Tempo network it serves.
enabledtrue only when the server opens this route for the caller now; a cross_chain route is never enabled on a Tempo test network.
intent_requiredfalse for a direct same-chain Tempo send, which never touches /v1 at all (RESOURCE-MODEL §0.10, R13-wallet §0) — true for every cross-network send, which must go through send_intents.
messageA short per-route status string the server always computes — e.g. why a coming_soon cross-chain route isn't live yet, or that a route is non-custodial. Published for parity with DepositRoute.message (same shape and purpose) and populated on every route without exception, so it is required and never null here (DepositRoute.message stays nullable+optional for its own producer, which can omit it). No approved design surface reads this field today — docs/api/consumers/R13-wallet.md §1 Group S and the approved wallet-send.html design both render disabled cross-network rows from a fixed client-side sub-label plus a status/enabled-driven "Behind flag" tag, not this string — and the text itself is server-generated English, not routed through the dashboard's i18n shards, so a screen must not render it verbatim in a localized surface. Exposed as a diagnostic/parity field now so a future screen (or a non-dashboard API caller) has it without a second contract change. On a Tempo test network a cross_chain route is unsupported and this names that network as the reason.
estimated_timeA human-readable ETA hint for this route. null for every route in the current catalogue — no entry in wallet-send/handler.ts's static route table populates it yet, and no approved design surface reads it today (docs/api/consumers/R13-wallet.md §4 sources every send-flow ETA elsewhere: a client-side chain-gas estimate for direct_tempo, Relay /quote/v2 for cross_chain). Published nullable, not omitted, and always present, unlike DepositRoute.estimated_time (a non-nullable string the deposit projection omits when unset) — reserved for a future route (e.g. a provider-quoted cross-chain ETA) that can set it.
fee_hintA human-readable fee hint for this route — not a computed quote. null for every route today, for the same reason as estimated_time above; not currently read by any approved surface. Published nullable and always present, unlike DepositRoute.fee_hint (non-nullable, omitted when unset).
SendRouteList
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.
SendIntentStatus
8 values, confirmed exact by R13-wallet §2 W-G11 — RESOURCE-MODEL §2.3's v1 draft omitted cancelled.
awaiting_signature: a payout-funding send payouts.fund handed to one session, not yet recorded.
SendIntent
id^sin_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectstatus8 values, confirmed exact by R13-wallet §2 W-G11 — RESOURCE-MODEL §2.3's v1 draft omitted cancelled.
awaiting_signature: a payout-funding send payouts.fund handed to one session, not yet recorded.
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
destination_chaindestination_assetdestination_addressUnsigned. The API cannot sign; the holder's passkey signs every step.
expires_atThe quoted "recipient gets" amount. Null until the routing provider has quoted it — a client renders "exact amount shown next" while it is null (R13-wallet §2 W-G2).
source_tx_hashThe source leg's transaction hash, verbatim. Recorded by source_tx.set, or by the server for a payout-funding send whose client record never landed; a later source_tx.set with the same hash is a no-op and a different hash is refused.
SendIntentCreateRequest
A Money value that must be strictly positive — used on every request field that creates or moves value.
destination_chaindestination_assetdestination_addressSendIntentSourceTxRequest
source_tx_hashSendIntentList
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.
UnsignedCall
toThe call's target address.
dataHex-encoded calldata.
value^[0-9]+$ · requiredMinor-unit integer as a string, mirroring Money.amount's representation.
UnsignedStep
kindA raw, unsigned chain call. The API never signs (RESOURCE-MODEL §0.10) — the holder's own passkey is the only signer.
OfframpQuoteRequest
destination_currencydestination_payment_railA Money value that must be strictly positive — used on every request field that creates or moves value.
OfframpQuote
destination_currencydestination_payment_railA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
The estimated amount and asset the holder's wallet will send. Indicative, and quantised to 2 decimals — decimals is 2 here even though USDC itself is a 6-decimal asset, because the quote engine rounds this figure to cents before it is published; a finer scale would state precision that was never quoted. The binding figure is re-priced at offramp_intents.create.
ExternalAccount
id^ba_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectcurrencyrailbank_namelast4The last 4 characters of the account/IBAN/CLABE identifier — bare characters, never a masking marker such as ••; a client formats its own display string. Matches payroll's identically-named destination_snapshot.last4.
ExternalAccountCreateRequest
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object · rail="ach" · requires: attest_own_account, currency, account_number +2 more | |
| type = object · rail="wire" · requires: attest_own_account, currency, account_number +2 more | |
| type = object · rail="sepa" · requires: attest_own_account, currency, iban +1 more | |
| type = object · rail="spei" · requires: attest_own_account, currency, clabe |
attest_own_accountMust be true. Re-verified against the provider again at spend time.
currencyrailaccount_numberrouting_numberaccount_typebank_nameExternalAccountCreateRequestAch
attest_own_accountMust be true. Re-verified against the provider again at spend time.
currencyrailaccount_numberrouting_numberaccount_typebank_nameExternalAccountCreateRequestWire
attest_own_accountMust be true. Re-verified against the provider again at spend time.
currencyrailaccount_numberrouting_numberaccount_typebank_nameExternalAccountCreateRequestSepa
attest_own_accountMust be true. Re-verified against the provider again at spend time.
currencyrailibanbicbank_nameExternalAccountCreateRequestSpei
attest_own_accountMust be true. Re-verified against the provider again at spend time.
currencyrailclabebank_nameExternalAccountList
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.
OfframpIntentStatus
11 values, American spelling canceled, exact match confirmed by R13-wallet §3 against RESOURCE-MODEL §2.3.
OfframpIntent
id^ofr_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectstatus11 values, American spelling canceled, exact match confirmed by R13-wallet §3 against RESOURCE-MODEL §2.3.
external_account_id^ba_ · requireddestination_currencydestination_payment_railA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
developer_fee_percentThe transit deposit's address, amount and on-chain memo (R13-wallet §2 W-G12) — chain is always tempo and the memo, when the rail needs one, is carried on memo. This is a direct same-chain Tempo call the holder's own passkey signs; it is never a /v1 write.
The provider-observed amount actually settled to the bank account, in destination_currency. Set only once the intent reaches payment_processed — this is Bridge's flexible-amount off-ramp, so rate movement between quote and deposit (or a partial/rounded deposit) can make this genuinely differ from amount, the originally requested figure. Absent (never a fabricated copy of amount) before settlement, and on an intent whose provider payload never carried a settlement receipt.
OfframpIntentCreateRequest
external_account_id^ba_ · requireddestination_currencydestination_payment_railA Money value that must be strictly positive — used on every request field that creates or moves value.
OfframpIntentList
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.
VirtualAccount
id^va_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectstatuspurposeWhich product owns this account. wallet_funding: the wallet's own "receive by bank" account. collection: a pay-in account for Payment links and Buy orders, one per customer, source currency and settlement destination — it belongs to Payment links, not to the wallet screen. legacy_wallet (W-R25): a wallet-purpose account whose stored destination is not this holder's Tempo wallet — an account of the wallet's previous settlement destination (e.g. Solana, Ethereum) from before the Tempo cutover, shown honestly rather than folded into collection. Read-only through /v1: the wallet API manages only accounts that settle to this holder's Tempo wallet; while status is activated, deposits still settle to settles_to. virtual_accounts.deactivate / .reactivate refuse it with 409 conflict (legacy_wallet_read_only). Filter with GET /v1/wallet/virtual_accounts?purpose=wallet_funding.
Where deposits to this account are credited, read from the account's stored settlement destination. null when that destination is unknown — never guessed.
Provider-issued instructions for funding this VA from an external source. Masked on read where the canon requires it.
developer_fee_percentZero on receive-by-bank today — the holder-identity distinction is the load-bearing fact, not price.
provider_environmentThe provider environment this account was issued in, read only from Swaps' stored record of the account — never from the caller's livemode or from the server's configuration. sandbox: a provider test account — bank details are test data and no real bank transfer arrives. production: a live provider account. null: not recorded — never evidence of a live account. Every live account today answers null, and so, in the provider sandbox, does any account not created through POST /v1/wallet/virtual_accounts (for example a Payment links collection account, or one created by the dashboard's own action or found by a provider sync) and any account created before 2026-09-29. Only sandbox marks a test account: show a test-mode marker on the bank details for sandbox and for nothing else.
Redacted event history for this VA.
VirtualAccountSettlesTo
kindlabelDisplay label, e.g. Swaps Wallet, USDC on Ethereum, Bank account (SEPA).
networkThe destination network for wallet and address kinds, e.g. tempo, ethereum.
assetThe destination asset ticker (e.g. USDC), for kind: address — read from the account's stored destination currency (W-R25: makes a legacy_wallet account's destination machine-readable alongside network and masked_address, not just the label text). Absent when the stored currency is unknown.
masked_addressThe destination address, masked. Absent for bank.
addressThe full settlement address. Present only for a live session (bearer) whose role on the account is owner or admin, which are the only callers these operations admit (a business key is refused with 403 scope_denied). Until per-client binding (K12) exists, a session token presented by any client counts as that session. Exactly as stored, case preserved (Solana, Tron and BTC addresses are case-sensitive). Absent for bank.
VirtualAccountCreateRequest
currencyusd, eur or mxn (case-insensitive) — the matching wallet_bank_virtual_account corridor.
VirtualAccountList
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.
VirtualAccountHistoryList
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.
ConversionTrackingStatus
The tracked leg's status (RESOURCE-MODEL §2.3). A kind: dex conversion — the only kind this API builds (D-25: G cut the proposed kind: cex branch, 2026-09-08, Exolix removed) — reaches only pending (built, unsigned) and submitted (its POST .../source_tx recorded and on-chain-verified the swap's own broadcast transaction); settled/failed are not yet advanced automatically past that point.
Conversion
id^cnv_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectkindAlways dex — on-chain, returns steps[] for the holder's passkey to sign. The proposed cex (no-wallet exchange leg) branch was withdrawn by G on 2026-09-08 (Exolix removed); this field is kept, pinned to dex, so a future rail can extend the enum without a breaking projection change.
provider_idchain_idfrom_tokento_tokenA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
The quoted receive amount, before the slippage bound (R13-wallet §2 W-G9).
A money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
rateDecimal string, never a float (R13-wallet §2 W-G9). Example "0.9998".
slippage_bpstracking_idA1-6: additive alias of id (see this schema's own description) — kept for one version for claude/dashboard-v2's existing caller. New callers should prefer id.
Ordered, unsigned — a two-step ceremony (approve then swap) is two entries, not one call{} — structurally explicit rather than inferred from calling create twice (R13-wallet §2 W-G9).
tracking_statusThe tracked leg's status (RESOURCE-MODEL §2.3). A kind: dex conversion — the only kind this API builds (D-25: G cut the proposed kind: cex branch, 2026-09-08, Exolix removed) — reaches only pending (built, unsigned) and submitted (its POST .../source_tx recorded and on-chain-verified the swap's own broadcast transaction); settled/failed are not yet advanced automatically past that point.
ConversionCreateRequest
from_tokento_tokenA Money value that must be strictly positive — used on every request field that creates or moves value.
chain_idslippage_bpsConversionSourceTxRequest
source_tx_hashConversionPair
from_tokento_tokenhas_liquidity