Crypto processing
Crypto invoices, pay-ins and subscriptions settled to the merchant's own wallet.
Jump to an operation:
- GET /payment_links/{id}/payments
- GET /payments
- GET /payments/{id}
- GET /payments/{id}/events
- GET /subscriptions
- POST /subscriptions
- GET /subscriptions/{id}
- POST /subscriptions/{id}/pause
- POST /subscriptions/{id}/resume
- POST /subscriptions/{id}/cancel
- GET /subscriptions/{id}/invoices
- GET /subscriptions/{id}/invoices/{invoice_id}
List a link's payment attempts — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
A view over the top-level /v1/payments resource, scoped to one link (RESOURCE-MODEL §2.1 v2 amendment). The detail screen must hold this before rendering the cancel confirmation, so the client can pick the right copy without an extra round trip (R11 PL-G11). Business key, agent (owner only), dashboard session; test mode is unavailable (503 temporarily_unavailable).
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 payment attempts — available › Responses
A page of payment attempts 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.
Present only with ?expand=summary (LIST-SUMMARY-1). Computed over every pay-in matching the request's payment_link_id, subscription_id, settlement and status (never cursor/limit). by_status has one key per PaymentStatus value; a status this version does not know counts in total only.
List pay-ins across products — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
List this account's pay-ins, newest first — payment-link attempts and crypto-processing attempts in one cross-product resource (RESOURCE-MODEL §2.1 v2 amendment). underpaid, overpaid, unmatched and processing are planned states (R19 X2/X4) — their absence from a result is not proof a payment never reached that condition. Business key, agent (owner only), dashboard session; test mode is unavailable (503 temporarily_unavailable).
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.
payment_link_id^pl_subscription_id^sub_settlementFilters by settlement destination (CP-T1). wallet is the merchant's own Swaps Wallet (RESOURCE-MODEL §2.3): the pay-ins of links whose settlement_kind is crypto_only, which the Crypto processing Payments list reads. Applies to the page and to ?expand=summary. Any other value is 400 invalid_request with param: settlement.
statusCross-product pay-in lifecycle (RESOURCE-MODEL §2.1 v2 amendment, 12 states). processing is still planned (Bridge-rail only, R19 X4). underpaid / overpaid / unmatched are AVAILABLE (CP-T3, §52.37 item 4 — RESOURCE-MODEL CP-G5's producer): the Tempo watcher (crypto_tempo/crypto_relay) writes them directly onto payment_link_attempts.status — a deposit short of the frozen total is underpaid (stays live: a later top-up can still complete it, settling paid with the SUM of every leg received, never just the last one); a match ABOVE the total settles the merchant's exact invoice as before and is overpaid, surplus published (paid to the Swaps fee wallet together with the fee, never auto-refunded — see Payment.surplus); a deposit to a closed, unpaid payment's address (expired or abandoned) makes that payment unmatched, amount_received = what was observed on it; a merchant cancel of an underpaid payment makes it unmatched too, never abandoned — the money stays held and its running total stays amount_received; Payment.unmatched_reason names which of these two made it (deposit_after_close / partial_before_cancel); a further deposit to an unmatched or already-paid payment's address never changes its status and is recorded for support, not published on /v1; a leg in a different accepted token than an underpaid payment's running total is never summed into it and is recorded for support; a deposit matching no payment at all is recorded for support. Events: payment.underpaid / payment.overpaid are live on TWO independent producers — the Bridge pay-in reducer (below) AND, as of CP-T3, the Tempo watcher, each firing at most once per payment on its own rail. The Bridge pay-in reducer emits one per payment when Bridge reports the received amount in the link's own currency and it falls outside ±1 % of the link amount; data.object carries both figures as amount_received and amount_expected (Money, link currency for the Bridge producer; the observed Tempo stablecoin for the watcher's own producer — crypto_tempo has no FX leg to convert either figure into). No verdict is published for a pay-in in another currency or on an FX-estimated figure. On the Bridge rails ONLY, payment.paid (and payment.settled) also fire for a short payment and come first, and data.object.status keeps reading paid/settled while the link stays processing: a payment.underpaid on the same pay_ id overrides them — do not fulfil on payment.paid alone. On crypto_tempo/crypto_relay this never happens: an underpaid deposit never reaches funds_received at all until it is topped up. payment.marked_sent is live — the payer's non-authoritative "I have sent it" on /pay, status still awaiting. Still catalogued, with the missing observation: payment.detected — Bridge delivers no pre-arrival state on the rails Payment links use (funds_scheduled is ACH-only, collection accounts are GBP/EUR) and the Tempo watcher settles on first sight; payment.processing — Bridge's payment_submitted follows funds_received (already paid) and in_review is only recorded on the link timeline, so no pre-paid processing step is observed. payment.unmatched is live (CP-T3, crypto_tempo/crypto_relay) once per payment, ONLY for a deposit to a closed, unpaid payment's address or a merchant cancel of an underpaid payment, its data.object.unmatched_reason naming which; a further deposit to an unmatched or paid payment and a deposit matching no payment are recorded for support and never published on /v1; a Bridge collection-account deposit with no match is still held in the operator ledger with no Payment to carry it (that half stays unproduced). A1-2 fixer round 1 (finding #2): this is the CLOSED, request-side form — payments.list's status query filter uses it directly, and an unrecognized value there is a real 400. PaymentStatusOpen (below) is the exact same list, response-side only (Payment.status, PaymentAttemptView.status, PaymentLink.latest_attempt_status); round 1 marked this schema directly instead, which leaked the open behaviour onto the query filter too (CONVENTIONS.md: mark the specific field, never the vocabulary in general).
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 pay-ins across products — available › Responses
A page of payments.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
Present only with ?expand=summary (LIST-SUMMARY-1). Computed over every pay-in matching the request's payment_link_id, subscription_id, settlement and status (never cursor/limit). by_status has one key per PaymentStatus value; a status this version does not know counts in total only.
Get a pay-in — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailable
Read one payment the caller owns — amount, fee, net amount, on-chain evidence and screening where it applies. No create, no refund: only a payer session creates one, and Swaps never initiates a refund (RESOURCE-MODEL §0.10). Business key, agent (owner only), dashboard session; test mode is unavailable (503 temporarily_unavailable).
path Parameters
id^pay_ · 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 pay-in — available › Responses
The payment.
id^pay_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atpayment_link_id^pl_ · requiredsubscription_id^sub_ · requiredSet when this pay-in settles a subscription invoice rather than a standalone link (RESOURCE-MODEL §2.1 v2 amendment — /v1/payments filters on payment_link_id, subscription_id, settlement).
statusThe exact same 12-state lifecycle as PaymentStatus above, kept as its own schema so the open-enum marker never reaches the payments.list status query filter (A1-2 fixer round 1, finding #2). Response-only — an unrecognized member here is an opaque string, never a deserialization failure. See PaymentStatus above for the state descriptions.
source_currencysource_chainThe one chain field (CMP-8) — the v2 crypto-extension's source_network duplicate is removed; this carries the payment_sessions.network vocabulary for every crypto rail.
source_assetsource_addressMasked (RESOURCE-MODEL §2.1 v2 amendment).
deposit_addressThe splitter address, case-preserved verbatim — never lowercased (AGENTS.md address-case rule).
Where to send crypto for this attempt (MON-7, CMP-5) — present only for a crypto rail; null on a bank rail (bank_deposit_instructions below is set instead). An underpaid crypto_tempo payment with a recorded running total names only the token that total counts, or is null when that token cannot be told apart.
The Bridge-issued bank transfer target for this attempt (K3b) — present only for one of the seven bank rails (ach, wire, fednow, sepa, faster_payments, pix, spei); null on a crypto rail (deposit_instructions above is set instead). Exactly one of the two is ever non-null. See BankDepositInstructions.holder_kind for who the beneficiary actually is on this attempt.
payer_typepayer_marked_sent_atA payer self-report, never authoritative on its own.
return_reasonRaw code from the returns dictionary (R11 §3); the client renders the display label.
settlement_tx_hashonchain_tx_hashCrypto rails only (RESOURCE-MODEL §2.1 v2 amendment; R11 PL-G7).
BL-39 (C4-D27) — this attempt's parent link's token identity, frozen from settlement_snapshot at ACTIVATION (never a live chain read, never derived from a rate, never recomputed per attempt). null for anything that is not a Tempo crypto-only settlement.
The ask — what this attempt requires the payer to send, fixed at creation and unaffected by what has actually arrived (MON-8; RESOURCE-MODEL §2.1 v2 amendment; R11 PL-G2).
The observation — what has actually been detected on-chain or at the rail so far. Null before status=detected; once populated it never goes back to null (MON-8). On unmatched (crypto_tempo/crypto_relay): everything observed up to the moment the payment became unmatched; later deposits to its address are recorded for support only.
The Swaps fee on crypto_tempo/crypto_relay, payer-borne on top of the invoice, at the settlement token's scale (the deposit instructions' figure; amount_expected includes it); null on every other rail, Bridge-routed rails included — the fee those rails deduct from what arrives is deducted_fee.
The Swaps fee deducted from what arrived on a Bridge-routed rail (bank rails, crypto_bridge) — the provider receipt's developer fee, recorded when the payment settled, in the invoice currency, rounded up to the currency's scale; the merchant bears it (net_amount is what arrived minus it). null before the payment is settled (and again if it is later returned); on crypto_tempo/crypto_relay (their fee is fee, paid on top); when the payer paid in another currency than the invoice (a receipt is never converted — the USDC source of crypto_bridge included); and while no consistent receipt is recorded (none sent, terms that do not add up, a provider exchange or gas fee on top). The configured rate is Capabilities.deducted_fee.
What the merchant receives, once the payment is settled; null before. On crypto_tempo/crypto_relay the invoice amount (the fee is paid on top). On a Bridge-routed rail only the provider receipt's figure — what arrived minus deducted_fee, in the invoice currency, before any conversion to the settlement asset, rounded down to the currency's scale — and null whenever deducted_fee is null for a receipt reason; never copied from the invoice there.
Present only when status=overpaid (RESOURCE-MODEL §2.1 v2 amendment; live, CP-T3/CP-R4 — the Tempo watcher's own producer, crypto_tempo/crypto_relay only). amount_received minus the frozen total the deposit instructions quoted, denominated in the observed stablecoin. Paid to the Swaps fee wallet together with the fee when the payment settles; returned to the sending address, minus the network fee, only on request through support (manual, from the fee wallet; in force after the legal sign-off on CP-G6, founder §52.60 54A); never auto-refunded. The merchant's own release is untouched: they still receive exactly their invoice regardless of this figure.
Present only when status=underpaid (live, CP-T3/CP-R4 — crypto_tempo/crypto_relay only): the frozen total minus everything observed toward this attempt SO FAR (every deposit leg summed, not just the latest one). A later top-up that completes the payment clears this back to null and settles paid with the sum. null rather than a zero or negative figure, or one across two token scales.
Present only when status=unmatched (CP-T4-T): which of the two paths made the payment unmatched — see PaymentUnmatchedReason. null on every other status, and on a payment that became unmatched before this field existed (no reason was recorded: show neutral copy, never a guessed reason).
RESOURCE-MODEL §2.1 v2 amendment. not_screened is a legitimate value, not an absent field (R19 CP-G13) — a payment that exists but was never run through the guard action reads not_screened, distinct from the field being missing.
expires_atcrypto_relay: the send-by time of its Relay quote (the quote_expires_at of its deposit instructions). null on every other rail today.
updated_atpayment_railcrypto_relay (R19 X2): the payer sends USDC on another EVM network to a Relay deposit address whose recipient is this attempt's splitter; live behind the crypto_relay_rail flag, off in production.
List a payment's events — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: sandbox
This payment's own event timeline, newest first (RESOURCE-MODEL §2.1, mirrors /payment_links/{id}/events exactly — BL-16 closed the same class for payment_links). A view over the K8 events outbox (api_events) scoped to this payment's own resource_id; unlike the link-level route there is no legacy audit table to merge, and payment.* had no producer before the outbox — so this page is complete since the events outbox was enabled for this account, not "the complete history" for a payment that predates the flip. Only the 5 publishable payment.* types this account's flag has actually produced are ever returned: payment.created, .awaiting, .paid, .settled, .expired. Callers: business key, agent, dashboard session. Test mode: sandbox (K11-6, §4.2 drift, was declared full) — a test payment'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-33 (2026-09-15): closes the crypto-processing payment timeline's fallback to attempt.updated_at once the outbox has rows for the account; the timeline must keep that fallback until then. Ownership is checked against the payment link's merchant_user_id, but the event query is scoped to the caller's resolved account_id; a merchant who owns more than one account can see an empty page for a payment that legitimately belongs to their OTHER account, rather than a 404 (pre-existing on every /v1/events-family read, not introduced here — _shared/services/api_events.ts's owner-walk always resolves to the OLDEST account).
path Parameters
id^pay_ · 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 payment's events — available › Responses
A page of events on this payment.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
List subscriptions — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
List this account's version-1 scheduled-invoice subscriptions. Each row carries overdue (NB-2) — derived from its own open, past-due invoices, never from the subscription's own status. Dark-flag — gated behind api_v1.subscriptions (K13c), off in production pending legal sign-off (G-8, MiCA/crypto-processing). Business key, agent, dashboard session. With api_v1.subscriptions closed, the answer is 503 temporarily_unavailable (an availability lever, never an authorization one), not capability_unavailable.
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.
statusexpandComma-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 subscriptions — dark-flag › Responses
A page of subscriptions.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
Present only with ?expand=summary (LIST-SUMMARY-1). Computed over every subscription of this account matching the request's status (never cursor/limit), in one grouped query.
Create a subscription — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
Create a version-1 scheduled-invoice subscription — a crypto invoice reissued on a schedule, never an authorised pull (RESOURCE-MODEL §2.6). Every subscription settles crypto_only (the no-FX crypto_tempo rail), so amount.currency must be USD — anything else is refused 422 crypto_only_currency_not_usd (C4-D27), the same code and reason payment_links.activate refuses a non-USD crypto-only link with. Dark-flag — gated behind api_v1.subscriptions (K13c), off in production pending legal sign-off (G-8, MiCA/crypto-processing). 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.
Create a subscription — dark-flag › Request Body
nameA Money value that must be strictly positive — used on every request field that creates or moves value.
intervalclient_id^cli_ · requiredattestation_acceptedfirst_due_atCreate a subscription — dark-flag › Responses
The created subscription.
id^sub_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atkindOnly scheduled_invoices is buildable for v1. authorised_pull names the v2 shape (SwapsSubscription contract) so the collision cannot be claimed by accident — it stays behind the founder-gated third-party audit (PRD §11.6) and has no producer.
statusDesign-proposed (R19 §2.3) — PRD-CP-001 does not itself define a status enum.
nameA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
intervalupdated_atoverdueNB-2 — derived: this subscription has at least one invoice that is open and past its due_at. Never stored, computed fresh from the invoice table at read time (one query per page, never N+1) — the same zero-grace rule SubscriptionInvoice.overdue uses. status itself never flips to reflect a missed payment (D-K13-6); this is the field that says so instead. Present on every /v1 read of this object (list, get, create, pause, resume, cancel) and ABSENT from subscription.* event and webhook payloads, which describe the transition that happened rather than a live invoice poll — read subscriptions.get for the current value.
first_due_atnext_due_atclient_id^cli_The merchant's settlement destination — always the Swaps Wallet for v1 (RESOURCE-MODEL §2.6). Sub-shape not further enumerated there.
referenceSUB-00nn (RESOURCE-MODEL §2.6).
paused_atC-55 (§52.37 item 4) — the instant pause set it; resume clears it back to null. null on an active or cancelled subscription, or one never paused. Present on every /v1 read (list, get, create, pause, resume, cancel) — the same producer/consumer pairing as overdue above.
Stored but inert — no delivery exists on the crypto rail yet (R19 CP-G10).
Get a subscription — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
Read one subscription the caller owns. overdue (NB-2) is derived from its own open, past-due invoices at read time and never stored — the subscription's own status never flips to reflect a missed payment. Dark-flag — gated behind api_v1.subscriptions (K13c), off in production pending legal sign-off (G-8, MiCA/crypto-processing). Business key, agent, dashboard session. With api_v1.subscriptions closed, the answer is 503 temporarily_unavailable (an availability lever, never an authorization one), not capability_unavailable.
path Parameters
id^sub_ · 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 subscription — dark-flag › Responses
The subscription.
id^sub_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atkindOnly scheduled_invoices is buildable for v1. authorised_pull names the v2 shape (SwapsSubscription contract) so the collision cannot be claimed by accident — it stays behind the founder-gated third-party audit (PRD §11.6) and has no producer.
statusDesign-proposed (R19 §2.3) — PRD-CP-001 does not itself define a status enum.
nameA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
intervalupdated_atoverdueNB-2 — derived: this subscription has at least one invoice that is open and past its due_at. Never stored, computed fresh from the invoice table at read time (one query per page, never N+1) — the same zero-grace rule SubscriptionInvoice.overdue uses. status itself never flips to reflect a missed payment (D-K13-6); this is the field that says so instead. Present on every /v1 read of this object (list, get, create, pause, resume, cancel) and ABSENT from subscription.* event and webhook payloads, which describe the transition that happened rather than a live invoice poll — read subscriptions.get for the current value.
first_due_atnext_due_atclient_id^cli_The merchant's settlement destination — always the Swaps Wallet for v1 (RESOURCE-MODEL §2.6). Sub-shape not further enumerated there.
referenceSUB-00nn (RESOURCE-MODEL §2.6).
paused_atC-55 (§52.37 item 4) — the instant pause set it; resume clears it back to null. null on an active or cancelled subscription, or one never paused. Present on every /v1 read (list, get, create, pause, resume, cancel) — the same producer/consumer pairing as overdue above.
Stored but inert — no delivery exists on the crypto rail yet (R19 CP-G10).
Pause a subscription — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
Stops future invoices from issuing until resumed. Dark-flag — gated behind api_v1.subscriptions (K13c), off in production pending legal sign-off (G-8, MiCA/crypto-processing). Business key, agent, dashboard session. With api_v1.subscriptions closed, the answer is 503 temporarily_unavailable (an availability lever, never an authorization one), not capability_unavailable.
path Parameters
id^sub_ · 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.
Pause a subscription — dark-flag › Responses
The paused subscription.
id^sub_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atkindOnly scheduled_invoices is buildable for v1. authorised_pull names the v2 shape (SwapsSubscription contract) so the collision cannot be claimed by accident — it stays behind the founder-gated third-party audit (PRD §11.6) and has no producer.
statusDesign-proposed (R19 §2.3) — PRD-CP-001 does not itself define a status enum.
nameA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
intervalupdated_atoverdueNB-2 — derived: this subscription has at least one invoice that is open and past its due_at. Never stored, computed fresh from the invoice table at read time (one query per page, never N+1) — the same zero-grace rule SubscriptionInvoice.overdue uses. status itself never flips to reflect a missed payment (D-K13-6); this is the field that says so instead. Present on every /v1 read of this object (list, get, create, pause, resume, cancel) and ABSENT from subscription.* event and webhook payloads, which describe the transition that happened rather than a live invoice poll — read subscriptions.get for the current value.
first_due_atnext_due_atclient_id^cli_The merchant's settlement destination — always the Swaps Wallet for v1 (RESOURCE-MODEL §2.6). Sub-shape not further enumerated there.
referenceSUB-00nn (RESOURCE-MODEL §2.6).
paused_atC-55 (§52.37 item 4) — the instant pause set it; resume clears it back to null. null on an active or cancelled subscription, or one never paused. Present on every /v1 read (list, get, create, pause, resume, cancel) — the same producer/consumer pairing as overdue above.
Stored but inert — no delivery exists on the crypto rail yet (R19 CP-G10).
Resume a paused subscription — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
Dark-flag — gated behind api_v1.subscriptions (K13c), off in production pending legal sign-off (G-8, MiCA/crypto-processing). Business key, agent, dashboard session.
path Parameters
id^sub_ · 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.
Resume a paused subscription — dark-flag › Responses
The resumed subscription.
id^sub_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atkindOnly scheduled_invoices is buildable for v1. authorised_pull names the v2 shape (SwapsSubscription contract) so the collision cannot be claimed by accident — it stays behind the founder-gated third-party audit (PRD §11.6) and has no producer.
statusDesign-proposed (R19 §2.3) — PRD-CP-001 does not itself define a status enum.
nameA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
intervalupdated_atoverdueNB-2 — derived: this subscription has at least one invoice that is open and past its due_at. Never stored, computed fresh from the invoice table at read time (one query per page, never N+1) — the same zero-grace rule SubscriptionInvoice.overdue uses. status itself never flips to reflect a missed payment (D-K13-6); this is the field that says so instead. Present on every /v1 read of this object (list, get, create, pause, resume, cancel) and ABSENT from subscription.* event and webhook payloads, which describe the transition that happened rather than a live invoice poll — read subscriptions.get for the current value.
first_due_atnext_due_atclient_id^cli_The merchant's settlement destination — always the Swaps Wallet for v1 (RESOURCE-MODEL §2.6). Sub-shape not further enumerated there.
referenceSUB-00nn (RESOURCE-MODEL §2.6).
paused_atC-55 (§52.37 item 4) — the instant pause set it; resume clears it back to null. null on an active or cancelled subscription, or one never paused. Present on every /v1 read (list, get, create, pause, resume, cancel) — the same producer/consumer pairing as overdue above.
Stored but inert — no delivery exists on the crypto rail yet (R19 CP-G10).
Cancel a subscription — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.write· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
Terminal — a cancelled subscription behaves like a cancelled payment link and issues no further invoices. Dark-flag — gated behind api_v1.subscriptions (K13c), off in production pending legal sign-off (G-8, MiCA/crypto-processing). Business key, agent, dashboard session. With api_v1.subscriptions closed, the answer is 503 temporarily_unavailable (an availability lever, never an authorization one), not capability_unavailable.
path Parameters
id^sub_ · 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 subscription — dark-flag › Responses
The cancelled subscription.
id^sub_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atkindOnly scheduled_invoices is buildable for v1. authorised_pull names the v2 shape (SwapsSubscription contract) so the collision cannot be claimed by accident — it stays behind the founder-gated third-party audit (PRD §11.6) and has no producer.
statusDesign-proposed (R19 §2.3) — PRD-CP-001 does not itself define a status enum.
nameA money value. amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is 6). Floats never appear on a money path.
intervalupdated_atoverdueNB-2 — derived: this subscription has at least one invoice that is open and past its due_at. Never stored, computed fresh from the invoice table at read time (one query per page, never N+1) — the same zero-grace rule SubscriptionInvoice.overdue uses. status itself never flips to reflect a missed payment (D-K13-6); this is the field that says so instead. Present on every /v1 read of this object (list, get, create, pause, resume, cancel) and ABSENT from subscription.* event and webhook payloads, which describe the transition that happened rather than a live invoice poll — read subscriptions.get for the current value.
first_due_atnext_due_atclient_id^cli_The merchant's settlement destination — always the Swaps Wallet for v1 (RESOURCE-MODEL §2.6). Sub-shape not further enumerated there.
referenceSUB-00nn (RESOURCE-MODEL §2.6).
paused_atC-55 (§52.37 item 4) — the instant pause set it; resume clears it back to null. null on an active or cancelled subscription, or one never paused. Present on every /v1 read (list, get, create, pause, resume, cancel) — the same producer/consumer pairing as overdue above.
Stored but inert — no delivery exists on the crypto rail yet (R19 CP-G10).
List a subscription's invoices — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
The child-invoice ladder — one row per period, issued on its due date, never earlier. Dark-flag — gated behind api_v1.subscriptions (K13c), off in production pending legal sign-off (G-8, MiCA/crypto-processing). Business key, agent, dashboard session. Resuming a paused subscription deletes any never-issued invoice whose period fell inside the paused span, so a sequence gap can appear across a resume with no corresponding event. With api_v1.subscriptions closed, the answer is 503 temporarily_unavailable (an availability lever, never an authorization one), not capability_unavailable.
path Parameters
id^sub_ · 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 subscription's invoices — dark-flag › Responses
A page of invoices.
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 a subscription invoice — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
payment_links.read· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
No create: the emitter is a cron, not a caller. Dark-flag — gated behind api_v1.subscriptions (K13c), off in production pending legal sign-off (G-8, MiCA/crypto-processing). Business key, agent, dashboard session. With api_v1.subscriptions closed, the answer is 503 temporarily_unavailable (an availability lever, never an authorization one), not capability_unavailable.
path Parameters
id^sub_ · requiredinvoice_id^inv_ · 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 subscription invoice — dark-flag › Responses
The invoice.
id^inv_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atsubscription_id^sub_ · requiredsequence1, 2, 3… (Invoice 1, Invoice 2…).
due_atstatusupdated_atissued_atSet on the due date, never earlier.
payment_link_id^pl_Nullable until issued.
overdueDerived: past due and unpaid. Never stored (RESOURCE-MODEL §2.6) — a clock skew cannot desync it from the invoice's real status.