Payment links
Request money — links, clients, products, payer sessions.
Jump to an operation:
- GET /payment_links
- POST /payment_links
- GET /payment_links/{id}
- PATCH /payment_links/{id}
- POST /payment_links/{id}/activate
- POST /payment_links/{id}/cancel
- POST /payment_links/{id}/send_invoice
- GET /payment_links/{id}/reminder_schedule
- PATCH /payment_links/{id}/reminder_schedule
- POST /payment_links/{id}/reminder_schedule/disable
- GET /payment_links/{id}/events
- GET /payment_links/{id}/receipt
- GET /clients
- POST /clients
- GET /clients/{id}
- PATCH /clients/{id}
- POST /clients/{id}/archive
- GET /products
- POST /products
- GET /products/{id}
- PATCH /products/{id}
- POST /products/{id}/archive
- GET /payment_sessions/{token}
- GET /payment_sessions/by_code/{short_code}
- POST /payment_sessions/{token}/view
- POST /payment_sessions/{token}/payments
- GET /payment_sessions/{token}/payments/{payment_id}
- POST /payment_sessions/{token}/receipt_email
- POST /payment_sessions/{token}/mark_sent
- POST /payment_sessions/{token}/pay_with_wallet
List payment links — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
List this account's payment requests, newest first (business key, agent, dashboard session; test mode is unavailable (503 temporarily_unavailable)). status_group matches the dashboard's chip buckets (RESOURCE-MODEL §2.1 v2 amendment); q searches title, memo and invoice number; settlement_kind narrows to one settlement kind. Do not use this to poll one link — use GET /v1/payment_links/{id}.
query Parameters
limitDefault 25. Out of range is never rejected: a value outside 1..100, a fraction, or a non-number is not a 400 — 0, a negative number, a non-integer or anything non-numeric falls back to the default (25); anything above 100 is capped to 100. A documented clamp, not a silent one; minimum/maximum are deliberately absent from this schema so a generated client (the MCP tool included) does not reject a value the API itself accepts.
cursorOpaque cursor from a previous response's next_cursor.
status_groupThis resource's own bucket set (open, needs_attention, paid, ended), not shared with other resources (CMP-6).
qclient_id^cli_settlement_kindOnly links whose published settlement_kind is this value (CP-T1). crypto_only is the Crypto processing hub's list; bridge is every Bridge-settled link. Applies to the page and to ?expand=summary. A draft created with accepted_rail_kinds: [crypto] publishes settlement_kind: null until it activates, so neither value matches it. Any other value is 400 invalid_request with param: settlement_kind.
expandComma-separated. summary (LIST-SUMMARY-1) adds summary: counts computed server-side over EVERY row matching this request's filters, not over the returned page, so a client never adds up a paginated page and calls it a total. limit/cursor never change it. An unrecognised value is ignored rather than rejected (the GET /account expand rule).
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
List payment links — available › Responses
A page of payment links.
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.
Draft a payment link — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Create a draft — nothing is charged and no rail goes live until activate (D-15). Business key, agent, dashboard session; test mode is unavailable (503 temporarily_unavailable). Setting accepted_rail_kinds: [crypto] drafts a crypto-only invoice (R19 CP-G2) — there is no separate settlement_kind input; it is system-set at activation. That one combination is answered 409 capability_unavailable only while the capability is closed for this account; every OTHER non-empty accepted_rail_kinds value ([bank], [card], or more than one kind) is answered 409 capability_unavailable unconditionally, flag or no flag — no activation path exists for it at all. An allowed_rails the link could not be paid under is refused here rather than stored: 400 allowed_rails_invalid_for_currency when the currency has no matching rail, with the rails that would work in error.details.offerable_rails; 400 allowed_rails_invalid_for_settlement when a crypto-only draft's restriction names anything but crypto_tempo, its one payable rail, with the same error.details.offerable_rails. settlement_destination (BL-50) names where activation will settle funds — a saved /v1/address_book entry the account already owns, never a raw address or bank detail on the wire; an id you do not own answers 404 not_found, identical to an unknown one. It may also be set later with update (which RETIRES a Swaps-Wallet intent instead of refusing it). Naming both settlement_destination and accepted_rail_kinds: [crypto] on this SAME create request is refused 400 settlement_conflicts_with_destination — reachable only while the crypto-only capability is open for this account; closed, accepted_rail_kinds: [crypto] is itself 409 capability_unavailable first.
Headers
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Draft a payment link — available › Request Body
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_emailDraft a payment link — available › Responses
The created draft.
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.
Get a payment link — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
Read one payment link the caller owns (business key, agent, dashboard session; test mode is unavailable (503 temporarily_unavailable)).
path Parameters
id^pl_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Get a payment link — available › Responses
The payment link.
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.
Update a draft payment link — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Draft-only (RESOURCE-MODEL §2.1) — refused with conflict once the link has left draft. A partial merge: an omitted field keeps its stored value. allowed_rails: null is the one way to REMOVE a rail restriction, after which the link offers every rail available for its currency; [] is a 400, not a clear. A request that CHANGES the currency or the rails re-checks that pair against the same yardstick activate uses (400 allowed_rails_invalid_for_currency, with error.details.offerable_rails); re-sending the stored value alongside another edit changes nothing and is never refused. A settlement_destination-only change is NOT re-checked against the stored rail restriction here — that pairing is judged at activate, which can still refuse a combination this PATCH accepted. settlement_destination (BL-50) follows the identical three-state convention as allowed_rails: omit to leave it, null to clear it, {address_book_id} to set it — a foreign or unknown id is 404 not_found, never a distinguishing error. Business key, agent, dashboard session; test mode is unavailable (503 temporarily_unavailable).
path Parameters
id^pl_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Update a draft payment link — available › Request Body
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_emailUpdate a draft payment link — available › Responses
The updated draft.
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.
Activate a payment link — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable · Money boundary: per-transaction human confirmation required
The money boundary (D-15): runs the eligibility gate, records the merchant's compliance attestation as evidence, takes a settlement snapshot, mints the live url. REST-only — excluded from MCP because an agent must never set attestation_accepted on the merchant's behalf. A draft whose allowed_rails leaves it with no payable rail at all is refused 409 allowed_rails_invalid_for_currency (with error.details.offerable_rails) and stays a draft — clear the restriction with allowed_rails: null or change the currency, then activate. A draft whose only payer rail would be crypto_relay above its per-invoice cap (the crypto_relay corridor's max_amount in capabilities.get) is refused 422 no_payable_rail with error.details = {rail, reason: amount_above_rail_maximum, max_amount} and stays a draft; a draft any other rail can pay activates as before (Relay delivers on the Tempo leg, so crypto_tempo pays such a link today). A draft edited between the read and the write is refused 409 too, rather than activated on a value that has moved. A wallet request (settlement: 'swaps_wallet', settling to the merchant's own Swaps Wallet) is routed by currency (§52.33, PL-TEMPO-BRIDGE-4-T, issue #3839): a USD link takes the Bridge-less crypto_only splitter — currency not USD there is refused 422 crypto_only_currency_not_usd (the crypto_tempo rail has no FX leg, so a non-USD link would pay out its face amount in USD-stablecoins 1:1, not the honest converted figure — C4-D27; unreachable through this operation today since a non-USD wallet request never reaches this branch, kept as a defense-in-depth backstop). A NON-USD wallet request instead settles through the SAME Bridge collection path (payment_rail: tempo + the account's own wallet address, resolved server-side — no address-book entry required) 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, so an available: true account can always activate: off for this merchant is refused 422 tempo_via_bridge_not_enabled (mirrors capabilities' own reason), an admitted merchant with no Swaps Wallet on file is refused 422 wallet_not_provisioned, and an admitted merchant whose Swaps Wallet is not on THIS project's Tempo network (tempo-mainnet in production) is refused 422 wallet_network_not_supported — capabilities.get reads the SAME wallet row and reports available: false with the matching reason for both. The identical no-FX hazard is refused 422 tempo_settlement_currency_not_usd for an ORDINARY draft whose chosen address-book settlement destination resolves to the Tempo splitter (not routed via Bridge) — the same rail, reached by a different door. All five carry error.details.currency (plus required_currency on the two _not_usd codes). An ORDINARY (non-wallet) draft with no settlement_destination set (BL-50, via create/update) is refused 400 settlement_destination_required and stays a draft; one set but unusable for this link's rails is refused 422 settlement_rail_unsupported (the destination's rail has no offramp route), 422 settlement_account_holder_missing or 422 settlement_details_incomplete (the address-book entry itself is missing a field Bridge requires) — pick or complete a different destination and retry. Business key, agent, dashboard session; test mode is unavailable (503 temporarily_unavailable). A USD crypto-only draft activates on the Bridge-less eligibility branch (R19 CP-G2); a non-USD wallet draft activates on the SAME Bridge eligibility (KYB/KYC, EEA, p2p cap) an address-book destination needs. A crypto-only draft (accepted_rail_kinds: [crypto]) activates without e-mailing its invoice, and one that still carries a reminder schedule is refused 422 invalid_request (param: reminder_schedule) and stays a draft until reminders are disabled.
path Parameters
id^pl_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Activate a payment link — available › Request Body
attestation_acceptedMust be true; any other value is refused.
Activate a payment link — available › Responses
The activated link.
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.
Cancel a payment link — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Cancel an unpaid link and void any payment the payer has started. One operation covers every confirm copy the dashboard shows (discard a draft, cancel a live link, cancel while funds are pending) — there is no separate discard verb (R11 PL-G10). Calling it twice is safe. Business key, agent, dashboard session; test mode is unavailable (503 temporarily_unavailable). A partially paid (underpaid) crypto_tempo/crypto_relay payment is not voided: it is kept as unmatched for support (money held, amount_received = its running total, unmatched_reason = partial_before_cancel) and payment.unmatched is emitted once. While a payment is being settled — including a crypto_relay payment whose funds Relay has received and not refunded (a Relay refund counts only once its refund transaction is recorded) — the whole cancel answers 409 conflict and changes nothing.
path Parameters
id^pl_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Cancel a payment link — available › Responses
The cancelled link.
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.
Email the invoice to the payer — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Email or re-email the payment link to its payer. Requires payer_email to already be set on the link; the reminder cadence is a separate call to PATCH .../reminder_schedule. A crypto-only link (accepted_rail_kinds: [crypto]) answers 409 conflict: the invoice is not e-mailed on the crypto rail today, so share its url instead. Business key, agent, dashboard session; Test mode is unavailable (503 temporarily_unavailable).
path Parameters
id^pl_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Email the invoice to the payer — available › Responses
Send accepted.
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.
Get the reminder schedule — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
Read the cadence and what has already fired. Business key, agent, dashboard session; test mode is unavailable (503 temporarily_unavailable).
path Parameters
id^pl_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Get the reminder schedule — available › Responses
The reminder schedule.
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).
Set the reminder cadence — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Set offsets_days[]. Editable while the link is draft, active or viewed (RESOURCE-MODEL §2.1). sent[] is server-owned and ignored if supplied. A crypto-only link (accepted_rail_kinds: [crypto]) takes no schedule: reminders are not delivered on the crypto rail today, so it answers 422 invalid_request (param: reminder_schedule); disabling stays allowed. Business key, agent, dashboard session; test mode is unavailable (503 temporarily_unavailable).
path Parameters
id^pl_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Set the reminder cadence — available › Request Body
offsets_daysSet the reminder cadence — available › Responses
The updated schedule.
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).
Turn reminders off — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Sets the schedule's status to off without discarding offsets_days[], so re-enabling does not require re-entering the cadence. Business key, agent, dashboard session; test mode is unavailable (503 temporarily_unavailable).
path Parameters
id^pl_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Turn reminders off — available › Responses
The disabled schedule.
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).
List a link's events — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: sandbox
This link's own event timeline, newest first (RESOURCE-MODEL §2.1 "events (per link)") — merges the K8 events outbox (api_events) with the older payment_link_events audit table, so a link's full history is returned whether or not it predates the outbox. Only the 11 publishable payment_link.* types are ever returned (D-12, RESOURCE-MODEL §3); 9 internal audit types have no public counterpart and are never published. Callers: business key, agent, dashboard session. Test mode: sandbox (K11-6, §4.2 drift, was declared full) — a test object's events are written by DEV handlers into DEV's own api_events outbox; a prod-local read would return an empty page forever. BL-16 (2026-09-14): closes the Timeline card's only missing read.
path Parameters
id^pl_ · requiredquery Parameters
limitDefault 25. Out of range is never rejected: a value outside 1..100, a fraction, or a non-number is not a 400 — 0, a negative number, a non-integer or anything non-numeric falls back to the default (25); anything above 100 is capped to 100. A documented clamp, not a silent one; minimum/maximum are deliberately absent from this schema so a generated client (the MCP tool included) does not reject a value the API itself accepts.
cursorOpaque cursor from a previous response's next_cursor.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
List a link's events — available › Responses
A page of events on this link.
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.
Get the collection receipt — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
The merchant's receipt for a collected link (RESOURCE-MODEL §2.1 v2 amendment: mirrors the payout receipt). Available only while the link's own status is paid or settled and exactly one payment completed it; any other state — draft, open, processing (including a short payment held for review), expired, cancelled, refunded — answers 409 receipt_not_available. A link the caller does not own is 404. JSON only: no PDF or HTML rendition exists yet; the payer's receipt is the e-mail swaps-pl-payer-receipt. Business key, agent, dashboard session.
path Parameters
id^pl_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Get the collection receipt — available › Responses
The receipt.
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.
List clients — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
List this account's saved clients, active only — archived clients are hidden. Business key, agent, dashboard session.
query Parameters
limitDefault 25. Out of range is never rejected: a value outside 1..100, a fraction, or a non-number is not a 400 — 0, a negative number, a non-integer or anything non-numeric falls back to the default (25); anything above 100 is capped to 100. A documented clamp, not a silent one; minimum/maximum are deliberately absent from this schema so a generated client (the MCP tool included) does not reject a value the API itself accepts.
cursorOpaque cursor from a previous response's next_cursor.
qHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
List clients — available › Responses
A page of clients.
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.
Save a client — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Save a client so future payment links can be addressed to them. Business key, agent, dashboard session.
Headers
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Save a client — available › Request Body
display_nameemailcompanycountrynotesSave a client — available › Responses
The created client.
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_atGet a client — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
Read one client the caller owns. Business key, agent, dashboard session.
path Parameters
id^cli_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Get a client — available › Responses
The client.
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_atUpdate a client — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Business key, agent, dashboard session.
path Parameters
id^cli_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Update a client — available › Request Body
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.
Update a client — available › Responses
The updated client.
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_atArchive a client — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Sets archived_at; never a hard delete. Business key, agent, dashboard session.
path Parameters
id^cli_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Archive a client — available › Responses
The archived client.
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_atList products — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
List this account's saved products, active only — archived products are hidden. Business key, agent, dashboard session.
query Parameters
limitDefault 25. Out of range is never rejected: a value outside 1..100, a fraction, or a non-number is not a 400 — 0, a negative number, a non-integer or anything non-numeric falls back to the default (25); anything above 100 is capped to 100. A documented clamp, not a silent one; minimum/maximum are deliberately absent from this schema so a generated client (the MCP tool included) does not reject a value the API itself accepts.
cursorOpaque cursor from a previous response's next_cursor.
qHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
List products — available › Responses
A page of products.
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.
Save a product — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Save a reusable line item. Business key, agent, dashboard session.
Headers
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Save a product — available › Request Body
nameA Money value that must be strictly positive — used on every request field that creates or moves value.
currencydescriptionSave a product — available › Responses
The created 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_atGet a product — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
Read one product the caller owns. Business key, agent, dashboard session.
path Parameters
id^prd_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Get a product — available › Responses
The 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_atUpdate a product — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Business key, agent, dashboard session. Existing line items on already-created links keep their locked price.
path Parameters
id^prd_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Update a product — available › Request Body
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).
Update a product — available › Responses
The updated 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_atArchive a product — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailable
Sets archived_at; never a hard delete. Business key, agent, dashboard session.
path Parameters
id^prd_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Archive a product — available › Responses
The archived 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_atRead a payer session — available
Status: Available · Callers: public token · Test mode: unavailable
A pure read (D-4) — never advances active to viewed; call POST .../view for that transition. Resolves exactly one link's payer projection off the capability token in the path; structurally unreachable with a business key or a dashboard session (RESOURCE-MODEL §0.9). Public token only, no key. Test mode does not apply: a test-mode link's token lives on DEV and is 404 here.
path Parameters
token^plk_ · requiredA plk_ capability token — a secret, never an id: never logged, never echoed.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Read a payer session — available › Responses
The payer's projection of the link.
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.
Resolve a short code to a payer session — available
Status: Available · Callers: public token · Test mode: unavailable
C5-SHORT-LINK — resolves a payment link's short_code (the last segment of PaymentLink.short_url) to its payer session token and payer page url, so a short URL reaches the payer page. A pure read, never a status change. Public, no key — the same caller model and error envelope as GET /v1/payment_sessions/{token}, but its own stricter per-IP rate limit (10/min), since a hit unlocks the full payer token. Upper-case input is accepted and normalized. Every miss is one 404 not_found: a malformed or unknown code, a draft link, and a link that has no payer page on this project's host (the other plane's code, or no payer host configured). Test mode does not apply: a test-mode link lives on DEV and resolves only there.
path Parameters
short_codeA PaymentLink.short_code. Resolves to a capability token, so it is never logged.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Resolve a short code to a payer session — available › Responses
The payer session token and payer page for this code.
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).
Mark the session viewed — available
Status: Available · Callers: public token · Test mode: unavailable
Advances the link active to viewed (D-4). Idempotent: calling it again on an already-viewed link is a no-op — no Idempotency-Key is accepted; the handler's own state check is the replay guard. Public token only, no key. Test mode does not apply: a test-mode link's token lives on DEV and is 404 here.
path Parameters
token^plk_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Mark the session viewed — available › Responses
The updated session.
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.
Select a rail and start a payment — available
Status: Available · Callers: public token · Test mode: unavailable · Money boundary: per-transaction human confirmation required
Selects a rail — for a crypto rail, also a network and stablecoin (RESOURCE-MODEL §2.1 v2 amendment) — and returns deposit instructions. A live attempt on the same rail returns its existing instructions rather than minting a second provider transfer (RESOURCE-MODEL §2.1 invariants) — idempotent per (token, rail) by the handler itself, so no Idempotency-Key is accepted here. The payer's consent checkbox is enforced client-side, not as a field here. Public token only, no key. Test mode does not apply: a test-mode link's token lives on DEV and is 404 here. crypto_relay (CP-X2) is live behind the crypto_relay_rail flag, off in production: it needs network and refund_address, allows one open payment per link (the same network and refund_address get that payment back; anything else is 409 payment_in_progress until Relay can no longer fill it and either saw no transfer or refunded it with the refund transaction recorded), and answers the Relay deposit in deposit_instructions (never the splitter address) only while the address is payable; deposit_instructions.quote_expires_at is the last time to send.
path Parameters
token^plk_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Select a rail and start a payment — available › Request Body
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.
Select a rail and start a payment — available › Responses
The started payment — the payer-safe projection (SEC-2), never the merchant's full Payment.
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).
Poll a payment's status — available
Status: Available · Callers: public token · Test mode: unavailable
Read one payment attempt started on this session. The payer's own client polls this with a backoff, never the chain directly. Public token only, no key. Test mode does not apply: a test-mode link's token lives on DEV and is 404 here.
path Parameters
token^plk_ · requiredpayment_id^pay_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Poll a payment's status — available › Responses
The payment — the payer-safe projection (SEC-2), never the merchant's full Payment.
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).
Set the payer's receipt email — available
Status: Available · Callers: public token · Test mode: unavailable
An opt-in email address for a receipt. Delivery on the crypto rail is a known gap (R19 CP-G12) — the field exists and is accepted regardless. Idempotent — re-saving the same or a corrected email is a plain overwrite, so no Idempotency-Key is accepted. Public token only, no key. Test mode does not apply: a test-mode link's token lives on DEV and is 404 here.
path Parameters
token^plk_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Set the payer's receipt email — available › Request Body
attempt_id^pay_ · requiredemailSet the payer's receipt email — available › Responses
Saved.
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.
Payer self-report: I've sent it — available
Status: Available · Callers: public token · Test mode: unavailable
Records the payer's own claim that they sent the transfer. authoritative:false on the resulting event (RESOURCE-MODEL §3) — it is never settlement. Idempotent (a compare-and-swap update) — no Idempotency-Key is accepted. Public token only, no key. Test mode does not apply: a test-mode link's token lives on DEV and is 404 here.
path Parameters
token^plk_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Payer self-report: I've sent it — available › Request Body
attempt_id^pay_ · requiredPayer self-report: I've sent it — available › Responses
Recorded.
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.
Prepare a one-tap Swaps Wallet payment — planned
Status: Planned — not built yet · Callers: public token · Test mode: fixtures · Money boundary: per-transaction human confirmation required · Non-custodial: response carries unsigned steps — Swaps never signs
This operation is planned — not built yet. It is documented for the contract it will carry, but it does not run today. Never call it expecting a live result.
Planned (R19 §2.4, CP-G11). Returns unsigned steps scoped to the splitter address — nothing leaves the payer's wallet until their own passkey signs on their device (RESOURCE-MODEL §0.10). This is the money boundary for the one-tap path: never chain these steps into execution without the payer's own confirmation. Public token only; test mode returns fixtures.
path Parameters
token^plk_ · requiredHeaders
Idempotency-Key^[A-Za-z0-9_-]{16,12… · requiredCaller-generated, 16-128 characters of [A-Za-z0-9_-] (a UUID or a base64url token both qualify). The value is part of a primary key, so the shape is enforced, not advisory: outside it the request is 400 invalid_request/idempotency_key_invalid. A replay within 24 hours returns the stored response; a different body under the same key is idempotency_error; a replay of a request that failed after it may have taken effect is 409 idempotency_failed — reconcile, then retry under a NEW key. After 24 hours the key is forgotten and the request is new (D-108).
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Prepare a one-tap Swaps Wallet payment — planned › Responses
Unsigned steps for the payer's own passkey to sign.
request_idexpires_at