Developers
API keys, webhook endpoints, events, usage.
Jump to an operation:
- GET /account
- PATCH /account
- GET /accounts
- POST /accounts
- GET /account/readiness
- GET /account/setup_guide
- POST /account/sessions/revoke_all
- GET /activity
- GET /activity/summary
- GET /events
- GET /events/{id}
- GET /events/stream
- GET /webhook_endpoints
- POST /webhook_endpoints
- GET /webhook_endpoints/{id}
- PATCH /webhook_endpoints/{id}
- DELETE /webhook_endpoints/{id}
- POST /webhook_endpoints/{id}/rotate_secret
- POST /webhook_endpoints/{id}/send_test_event
- GET /webhook_deliveries
- GET /webhook_deliveries/{id}
- POST /webhook_deliveries/{id}/replay
- POST /webhook_waitlist
- POST /card_waitlist
- GET /address_book
- POST /address_book
- GET /address_book/{id}
- PATCH /address_book/{id}
- DELETE /address_book/{id}
- POST /address_book/{id}/recheck
- POST /address_book/{id}/beneficiary
- GET /credits
- GET /credit_events
- POST /credit_checkouts
- GET /api_keys
- POST /api_keys
- GET /api_keys/{id}
- POST /api_keys/{id}/roll
- POST /api_keys/{id}/revoke
- GET /api_keys/{id}/usage
- GET /request_logs
Read the signed-in holder's own account — available
Status: Available · Callers: dashboard session, agent (business key), business key · Scope:
account.read· Test mode: full
The caller's own profile — display name, email, verification tier, locale, access state, the persona's market (BL-25, D-PF-9a — country + default currency for the embedded Buy & sell widget) and the dashboard's saved preferences. Reads "the selected account" (D-109: Swaps-Account, or the caller's default) — never a lookup by id; 404 names the pre-existing, rare case where the account row a valid credential resolves to is itself gone. ?expand=readiness,setup_guide (API-PERF-1 M5) merges either or both of those reads' payloads in under readiness/setup_guide — folding the dashboard's own account-load sequence into one round trip instead of three; omitted when not requested. A REQUESTED expansion that fails (fix round 1) never fails the account read or answers a 404 of its own: the account, and any other requested expansion that succeeded, are still published at 200, and expand_errors names which field is missing and why. caller names who is calling and, for a session, its role on this account, so a client can withhold an owner/admin-only action (e.g. PATCH /account) from a member instead of learning it from 403 role_denied. Callers: dashboard session, agent (holder), business key (read-only). Test mode: full. With an sk_test_ business key on a test twin, email is the only field read from the live owner; the other user-level fields are defaults, never the owner's values: language and support_contact are null, notification_preferences is {marketing: false, product_tips: true} and dashboard_preferences is {}. A test-mode dashboard session sees its own values. caller describes the credential, not the owner, and is published in both modes (business_key, role null, for a key).
query Parameters
expandComma-separated resources to merge into this response (API-PERF-1 M5): readiness, setup_guide, or both. Each maps to its own GET /account/{resource} payload under the matching key — additive, never required, and answers the exact same data those standalone reads would; an unrecognised value is ignored rather than rejected. Exists to fold the dashboard's own account-load sequence (account + readiness + setup_guide, three round trips today) into one.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read the signed-in holder's own account — available › Responses
The account. When ?expand= was requested, may additionally carry readiness/setup_guide (on success) and/or expand_errors (on a per-field failure — see the operation description).
id^acct_ · requiredThe account's id. Never looked up directly by a business key or a session — reached only as "the selected account" (GET /account) or via GET /accounts (mine).
display_nameemailmember_sincecustomer_typeindividual or business. Follows the account owner's customer (customers.get, GET /customers/{id}): it is the type Swaps has recorded for that customer, not a live read from our verification partner. Swaps sets it whenever a customer of the owner is recorded or changes type, in either direction (business when any owner has a business customer). Until the owner has a customer, the type the account was opened with. Not writable through PATCH /account, and customers.create does not write it. A test-mode account keeps the type it was created with. Not itself a KYB projection (RESOURCE-MODEL §2.4).
countryISO 3166-1 alpha-2. null until a source column exists (проверить — public.users has none today).
languagenull until a source column exists (проверить — public.users has none today). Always null for an sk_test_ key (test mode).
access_stateReason for a restricted/suspended/blocked state stays internal — never on the wire.
livemodesupport_contactL4-9 — an EXPLICIT opt-in support contact for the payer surfaces (PaymentSession.merchant_contact on /pay/<token>). NEVER derived from email above (the login e-mail), KYC, Bridge or settlement data — null until the holder sets it here. Also null for an sk_test_ key: withheld in test mode, not unset.
BL-25 (D-PF-9a) — the persona's market for the embedded Buy & sell widget, which previously had no signal to resolve country/currency from and fell back to the public-site default regardless of who was signed in. A read-only projection over the same static country→currency registry the public-site widget itself defaults from (_shared/services/config.ts's countries.json, ISO 4217 local currency per ISO 3166-1 alpha-2 country) — never a live/provider-routed pick, so this carries no money path. Not itself writable. Resolved, in order of trust, from country above (declared once at POST /accounts, validated against this same registry there), then the holder's Bridge KYC address country (not published as its own field), then the public-site no-signal default. Always present — source names which case applied, and a consumer MUST treat source: 'default' as "no persona signal at all" rather than seed anything from it (it is the exact value an anonymous, signed-out visitor gets).
For an sk_test_ key (test mode) always {marketing: false, product_tips: true}, not the owner's values; a test-mode dashboard session sees its own.
Cross-device UI preferences persisted server-side (R18 TA-G9) — not UI-private, since prod already syncs them. For an sk_test_ key (test mode) always {}, not the owner's values; a test-mode dashboard session sees its own.
Present only when ?expand=readiness (or ?expand=readiness,setup_guide) is requested on GET /account AND that expansion succeeded (API-PERF-1 M5, fix round 1) — the exact GET /account/readiness payload, merged in. Absent, never null, when not requested; also absent, with a matching entry in expand_errors, when requested but the expansion itself failed (an unresolvable owner or a Bridge outage) — a failed expansion never fails the account read.
Present only when ?expand=setup_guide (or ?expand=readiness,setup_guide) is requested on GET /account AND that expansion succeeded (API-PERF-1 M5, fix round 1) — the exact GET /account/setup_guide payload, merged in. Absent, never null, when not requested; also absent, with a matching entry in expand_errors, when requested but the expansion itself failed.
Present only when at least one requested ?expand= field could not be computed (API-PERF-1 fix round 1). The account read itself, and any OTHER requested expansion that DID succeed, are still published — this names exactly which field is missing and why, rather than failing the whole response the way the standalone GET /account/readiness/GET /account/setup_guide routes do on the same failure. Absent entirely when ?expand= is unset, or when every requested field succeeded.
Who is making this request, on this account: the credential type and, for a session, the caller's membership role. Read it before offering an owner/admin-only action (e.g. changing display_name with PATCH /account, API keys, the wallet families) instead of learning it from a 403 role_denied. It comes from the same resolved credential the role gate checks, never a second lookup. Present on GET /account and PATCH /account, and on the one GET /accounts item this request's credential resolved to (the selected account). Absent, not null, on any other account (the other GET /accounts items, POST /accounts): this request never resolved a role there — select that account with Swaps-Account and read GET /account. Published in test and live mode alike.
Update the holder's own profile fields — available
Status: Available · Callers: dashboard session · Scope:
account.write· Test mode: unavailable
Updates display name, language, notification preferences, dashboard preferences and the opt-in support contact (L4-9) payer surfaces may show. Email, country, customer type and access state are not writable here — email is an identity-plane change and the rest are derived elsewhere. Callers: dashboard session, agent (holder), never a business key. Test mode: unavailable (503). Language, dashboard preferences, notification preferences and the support contact belong to the holder and are shared with live mode; the handler also refuses them from a test-mode caller with 400 invalid_request, code livemode_boundary, before anything is written.
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.
Update the holder's own profile fields — available › Request Body
display_nameThe account's name. 1–80 characters; leading and trailing whitespace is trimmed, and a value that is empty after trimming is refused with 400 invalid_request, param: display_name. Only an account owner or admin may change it (a member gets 403). Payment links, the payer page, receipts and invoice emails show the profile name of the person the link is recorded under (for a link created through /v1, the account's first owner), not this account name. On a live account, saving this name also sets it for each owner who belongs to no other live account, so their payers see it at once. An owner who belongs to more than one live account keeps their current name on all of them until payer names are resolved per account.
languagesupport_contactL4-9 — the opt-in support contact the payer surfaces may show (§0.12 PATCH null-clears convention): omitted leaves it unchanged, an object sets it, explicit null clears it back to unset. At least one of email/url must be present on a set (never a bare {}).
Update the holder's own profile fields — available › Responses
The updated account.
id^acct_ · requiredThe account's id. Never looked up directly by a business key or a session — reached only as "the selected account" (GET /account) or via GET /accounts (mine).
display_nameemailmember_sincecustomer_typeindividual or business. Follows the account owner's customer (customers.get, GET /customers/{id}): it is the type Swaps has recorded for that customer, not a live read from our verification partner. Swaps sets it whenever a customer of the owner is recorded or changes type, in either direction (business when any owner has a business customer). Until the owner has a customer, the type the account was opened with. Not writable through PATCH /account, and customers.create does not write it. A test-mode account keeps the type it was created with. Not itself a KYB projection (RESOURCE-MODEL §2.4).
countryISO 3166-1 alpha-2. null until a source column exists (проверить — public.users has none today).
languagenull until a source column exists (проверить — public.users has none today). Always null for an sk_test_ key (test mode).
access_stateReason for a restricted/suspended/blocked state stays internal — never on the wire.
livemodesupport_contactL4-9 — an EXPLICIT opt-in support contact for the payer surfaces (PaymentSession.merchant_contact on /pay/<token>). NEVER derived from email above (the login e-mail), KYC, Bridge or settlement data — null until the holder sets it here. Also null for an sk_test_ key: withheld in test mode, not unset.
BL-25 (D-PF-9a) — the persona's market for the embedded Buy & sell widget, which previously had no signal to resolve country/currency from and fell back to the public-site default regardless of who was signed in. A read-only projection over the same static country→currency registry the public-site widget itself defaults from (_shared/services/config.ts's countries.json, ISO 4217 local currency per ISO 3166-1 alpha-2 country) — never a live/provider-routed pick, so this carries no money path. Not itself writable. Resolved, in order of trust, from country above (declared once at POST /accounts, validated against this same registry there), then the holder's Bridge KYC address country (not published as its own field), then the public-site no-signal default. Always present — source names which case applied, and a consumer MUST treat source: 'default' as "no persona signal at all" rather than seed anything from it (it is the exact value an anonymous, signed-out visitor gets).
For an sk_test_ key (test mode) always {marketing: false, product_tips: true}, not the owner's values; a test-mode dashboard session sees its own.
Cross-device UI preferences persisted server-side (R18 TA-G9) — not UI-private, since prod already syncs them. For an sk_test_ key (test mode) always {}, not the owner's values; a test-mode dashboard session sees its own.
Present only when ?expand=readiness (or ?expand=readiness,setup_guide) is requested on GET /account AND that expansion succeeded (API-PERF-1 M5, fix round 1) — the exact GET /account/readiness payload, merged in. Absent, never null, when not requested; also absent, with a matching entry in expand_errors, when requested but the expansion itself failed (an unresolvable owner or a Bridge outage) — a failed expansion never fails the account read.
Present only when ?expand=setup_guide (or ?expand=readiness,setup_guide) is requested on GET /account AND that expansion succeeded (API-PERF-1 M5, fix round 1) — the exact GET /account/setup_guide payload, merged in. Absent, never null, when not requested; also absent, with a matching entry in expand_errors, when requested but the expansion itself failed.
Present only when at least one requested ?expand= field could not be computed (API-PERF-1 fix round 1). The account read itself, and any OTHER requested expansion that DID succeed, are still published — this names exactly which field is missing and why, rather than failing the whole response the way the standalone GET /account/readiness/GET /account/setup_guide routes do on the same failure. Absent entirely when ?expand= is unset, or when every requested field succeeded.
Who is making this request, on this account: the credential type and, for a session, the caller's membership role. Read it before offering an owner/admin-only action (e.g. changing display_name with PATCH /account, API keys, the wallet families) instead of learning it from a 403 role_denied. It comes from the same resolved credential the role gate checks, never a second lookup. Present on GET /account and PATCH /account, and on the one GET /accounts item this request's credential resolved to (the selected account). Absent, not null, on any other account (the other GET /accounts items, POST /accounts): this request never resolved a role there — select that account with Swaps-Account and read GET /account. Published in test and live mode alike.
List every account this login can reach — available
Status: Available · Callers: dashboard session, agent (business key), business key · Scope:
account.read· Test mode: full
"Mine" — every account the caller holds a membership on (D-109). One live account per login is supported today; a login that already held more than one keeps seeing all of them here and can still select any of them with Swaps-Account. A business key is bound to a single account and always sees a one-item list. Callers: dashboard session, agent (holder), business key (read-only). Test mode: full. With an sk_test_ business key on a test twin, email is the only field read from the live owner; the other user-level fields are defaults, never the owner's values: language and support_contact are null, notification_preferences is {marketing: false, product_tips: true} and dashboard_preferences is {}. A test-mode dashboard session sees its own values.
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.
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 every account this login can reach — available › Responses
A page of accounts.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
Create another account under the same login — not open today — available
Status: Available · Callers: dashboard session · Scope:
account.write· Test mode: unavailable
Not open today: one account per login is supported (since 2026-10-05). A login that already owns a live account gets 409 conflict, code account_already_exists, and nothing is written: no account and no membership. Every signed-in login is given its account on its first request without Swaps-Account, and only the owner of the live account a request acts on gets past the earlier refusals, so through /v1 this operation never answers 201 today: a request that gets past them gets this 409. Every refusal this operation answered before still comes first, unchanged: 403 for a business key or a member, 503 for a test-mode session, 400 for a malformed body or an unrecognised country. An account's type follows its owner's Bridge customer (see Account.customer_type). Opening a second account under one login waits for a later program that gives every account its own verification; the request and the 201 below are the contract it will carry, an individual or a business account whose owner is the caller. A business key cannot call this — it is bound to one account and never mints another. Callers: dashboard session only. Test mode: unavailable (503) — a new account is always live.
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 another account under the same login — not open today — available › Request Body
display_namecustomer_typecountryISO 3166-1 alpha-2. Optional — unset until the holder declares one. Fix round 1 (P2): validated against the same countries.json registry market (on Account) reads from — case/whitespace-normalized on write, an unrecognised value is a 400 invalid_request (param: 'country'), never silently stored and never able to disagree with the resolved market.country for the same account.
Create another account under the same login — not open today — available › Responses
The new account — the contract a later program will open. Today no /v1 caller receives it: a request that gets past the earlier refusals always comes from the owner of the live account it acts on, and gets the 409 below.
id^acct_ · requiredThe account's id. Never looked up directly by a business key or a session — reached only as "the selected account" (GET /account) or via GET /accounts (mine).
display_nameemailmember_sincecustomer_typeindividual or business. Follows the account owner's customer (customers.get, GET /customers/{id}): it is the type Swaps has recorded for that customer, not a live read from our verification partner. Swaps sets it whenever a customer of the owner is recorded or changes type, in either direction (business when any owner has a business customer). Until the owner has a customer, the type the account was opened with. Not writable through PATCH /account, and customers.create does not write it. A test-mode account keeps the type it was created with. Not itself a KYB projection (RESOURCE-MODEL §2.4).
countryISO 3166-1 alpha-2. null until a source column exists (проверить — public.users has none today).
languagenull until a source column exists (проверить — public.users has none today). Always null for an sk_test_ key (test mode).
access_stateReason for a restricted/suspended/blocked state stays internal — never on the wire.
livemodesupport_contactL4-9 — an EXPLICIT opt-in support contact for the payer surfaces (PaymentSession.merchant_contact on /pay/<token>). NEVER derived from email above (the login e-mail), KYC, Bridge or settlement data — null until the holder sets it here. Also null for an sk_test_ key: withheld in test mode, not unset.
BL-25 (D-PF-9a) — the persona's market for the embedded Buy & sell widget, which previously had no signal to resolve country/currency from and fell back to the public-site default regardless of who was signed in. A read-only projection over the same static country→currency registry the public-site widget itself defaults from (_shared/services/config.ts's countries.json, ISO 4217 local currency per ISO 3166-1 alpha-2 country) — never a live/provider-routed pick, so this carries no money path. Not itself writable. Resolved, in order of trust, from country above (declared once at POST /accounts, validated against this same registry there), then the holder's Bridge KYC address country (not published as its own field), then the public-site no-signal default. Always present — source names which case applied, and a consumer MUST treat source: 'default' as "no persona signal at all" rather than seed anything from it (it is the exact value an anonymous, signed-out visitor gets).
For an sk_test_ key (test mode) always {marketing: false, product_tips: true}, not the owner's values; a test-mode dashboard session sees its own.
Cross-device UI preferences persisted server-side (R18 TA-G9) — not UI-private, since prod already syncs them. For an sk_test_ key (test mode) always {}, not the owner's values; a test-mode dashboard session sees its own.
Present only when ?expand=readiness (or ?expand=readiness,setup_guide) is requested on GET /account AND that expansion succeeded (API-PERF-1 M5, fix round 1) — the exact GET /account/readiness payload, merged in. Absent, never null, when not requested; also absent, with a matching entry in expand_errors, when requested but the expansion itself failed (an unresolvable owner or a Bridge outage) — a failed expansion never fails the account read.
Present only when ?expand=setup_guide (or ?expand=readiness,setup_guide) is requested on GET /account AND that expansion succeeded (API-PERF-1 M5, fix round 1) — the exact GET /account/setup_guide payload, merged in. Absent, never null, when not requested; also absent, with a matching entry in expand_errors, when requested but the expansion itself failed.
Present only when at least one requested ?expand= field could not be computed (API-PERF-1 fix round 1). The account read itself, and any OTHER requested expansion that DID succeed, are still published — this names exactly which field is missing and why, rather than failing the whole response the way the standalone GET /account/readiness/GET /account/setup_guide routes do on the same failure. Absent entirely when ?expand= is unset, or when every requested field succeeded.
Who is making this request, on this account: the credential type and, for a session, the caller's membership role. Read it before offering an owner/admin-only action (e.g. changing display_name with PATCH /account, API keys, the wallet families) instead of learning it from a 403 role_denied. It comes from the same resolved credential the role gate checks, never a second lookup. Present on GET /account and PATCH /account, and on the one GET /accounts item this request's credential resolved to (the selected account). Absent, not null, on any other account (the other GET /accounts items, POST /accounts): this request never resolved a role there — select that account with Swaps-Account and read GET /account. Published in test and live mode alike.
Resolve the one next-action readiness kind — available
Status: Available · Callers: dashboard session, agent (business key), business key · Scope:
account.read· Test mode: unavailable
Resolves the eight-kind readiness machine to the single kind the persistent Today card shows right now — never a list, one kind wins; re-derive on every call. Callers: dashboard session, agent (holder), business key (read-only). Test mode: full. BL-14 (2026-09-15): implemented — server-side mirror of the client's deriveDashboardReadinessState, Bridge-only today (Paybis/Transak not yet resolved server-side). On a degraded Bridge read (the last persisted snapshot, published as Customer.degraded: true) no fact or sentence says the identity is verified: accept_terms then carries only the terms fact, and the card carries degraded: true. The kind is resolved exactly as on a live read.
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.
Resolve the one next-action readiness kind — available › Responses
The resolved readiness kind.
kindtitlebodyfactsShort supporting facts rendered under the body copy (e.g. a pending-count sentence).
next_actiondegradedPresent, and true, only when the Bridge customer status this card was built from is the last persisted snapshot because the live read failed (Customer.degraded). The card then states no verified identity. Absent on a live read.
Read the six-section onboarding checklist — available
Status: Available · Callers: dashboard session, agent (business key), business key · Scope:
account.read· Test mode: unavailable
Six sections, eighteen steps (Overview, Wallet, Payment links, Payroll, Pay an invoice, Crypto processing) with per-step and per-section completion and the single next step to surface — replaces the multi-request client-side derivation prod pays for today on every dashboard page. Callers: dashboard session, agent (holder), business key (read-only). Test mode: full. BL-14 (2026-09-15): implemented — pay_invoice steps stay permanently undone (no backing resource yet). On a degraded Bridge read (the last persisted snapshot, published as Customer.degraded: true) verify_identity and confirm_eligible report done: false and are not counted, and the guide carries degraded: true; next_step is the step a live read of the same status names, never a step withheld for that reason, and every other step reads as on a live read.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read the six-section onboarding checklist — available › Responses
The setup guide.
donetotalnext_stepdegradedPresent, and true, only when the Bridge customer status this guide was built from is the last persisted snapshot because the live read failed (Customer.degraded). verify_identity and confirm_eligible then report done: false even where the snapshot says verified, and next_step never names a step withheld for that reason: it is the step a live read of the same status names. Absent on a live read.
Sign the holder out of every device — planned
Status: Planned — not built yet · Callers: dashboard session · Scope:
account.write· Test mode: fullThis 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.
The single published exception to the identity-plane carve-out (RESOURCE-MODEL §0.12): every other sign-in, passkey and session concern stays outside the resource contract, but a global revoke is destructive enough to publish. The calling session is not guaranteed to survive it either — expect to re-authenticate. Callers: dashboard session only. Test mode: full.
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.
Sign the holder out of every device — planned › Responses
Every session is revoked.
List the cross-product activity ledger — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
activity.read· Test mode: unavailable
One row per money object across payment links, payouts, payroll, buy & sell, wallet and crypto processing — the read model over the events outbox (RESOURCE-MODEL §0.12). Today's recent-activity slot is this list's newest three rows, byte for byte. object{id, type} points back to the full resource; do not treat status as a shared cross-product vocabulary. status_group is this resource's own six-bucket set, not shared with payment_links, payouts, payroll_runs or orders — read each of those resources' own status_group for their set. K8b moved every payment_link.*/payment.*/payout.* producer off the api-v1 route layer and into the product's own service/action layer (EventType's own description names the full type list) — a dashboard action, a Bridge webhook reducer or a cron sweep now emits too, not only an api-v1-originated mutation. K8c did the same for eight payroll_run.* lifecycle types (created/approved/funding_verified/execution_started/completed/ partial/failed/cancelled), hooked into payroll/lib.ts's own single appendEvent choke point — so payroll rows now exist here for RUN-level lifecycle transitions, driven by any caller (an employer action, an ops-asserted funding attestation, the funding poll). Payroll ITEM-level events (payroll_item.*/payroll_template.*), the run's own non-lifecycle audit types (funding_instructions_*, execution_requested/.blocked, .underfunded) and payout webhook/RPC-driven states still have no producer outside their own SQL RPCs (a named follow-up). No pre-K8 history is backfilled. buy_sell/wallet/crypto_processing rows do not exist until those products wire their own producers. product filters at the query (resource_type IN (...)), never by scanning an already-fetched page. Callers: business key, agent, dashboard session. Test mode: fixtures.
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.
productFilters at the query (resource_type IN (...), K8c) — pushed into the underlying read, never applied after a capped fetch. A product with no live producer yet (buy_sell, wallet, crypto_processing) legitimately returns zero rows.
status_groupThis resource's own six buckets (draft, pending, processing, completed, failed, returned) — not the per-product sets payment_links, payouts, payroll_runs and orders use for their own status_group.
sinceuntilHeaders
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 the cross-product activity ledger — available › Responses
A page of activity rows.
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.
Read unbounded counts across every product — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
activity.read· Test mode: unavailable
Honest, unbounded counts by date window, product and status group — the figures behind the Activity filter chips, covering every row on record, not just the loaded page. A cursor list never returns a total; this is where it lives. by_product_status (K8c, §52 C4-D14) crosses product × status_group into one count-and- minor-unit-sum grid for the product hubs and Today tiles; a cell's amount is null when the cell has no rows or when its rows carry more than one currency — this endpoint never fabricates an FX rate to combine them. Callers: business key, agent, dashboard session. Test mode: fixtures.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read unbounded counts across every product — available › Responses
The summary.
totalPer-product × lifecycle-status totals for the product hub sub-lines and Today tiles (K8c, §52 C4-D14) — the same six products and six status groups as by_product/by_status_group, crossed. Computed over the same capped read as the rest of this resource.
List the cross-domain event stream — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
events.read· Test mode: unavailable
The append-only outbox the product's own service layer writes to for every transition it drives — an api-v1 mutation, a dashboard action, a Bridge webhook reducer or a cron sweep alike (K8b moved the producers off the api-v1 route layer for exactly this reason) — filter by type (a dotted event name from RESOURCE-MODEL §3) and object (the affected resource's id). Poll this only when a live connection is unavailable; prefer GET /v1/events/stream otherwise. EventType's own description names which of the 22 payment_link.*/payment.*/payout.* types (K8) and the eight payroll_run.* RUN-level lifecycle types (K8c, §52 C4-D14) currently have a producer; the five order.* types also have one now (L5-1) — _shared/services/api_events.ts's emitOrderOutboxEvent, wired from the Bridge-native order-create/cancel/status-update paths and the Bridge webhook reducer — and so do the three customer.* types (L5-2), the same file's emitCustomerOutboxEvent, wired from bridge-webhook/index.ts's single bridge_customers writer. See RESOURCE-MODEL §3 for the full writer list. No pre-K8 history is backfilled. Callers: business key, agent, dashboard session. Test mode: unavailable (A4-FIX-6). Test-mode events are not readable through /v1 today — no test object can be created yet; once creates are restored, poll the forwarded per-object */events lists.
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.
typeA dotted event type, e.g. payout.settled.
objectThe affected resource's id.
sinceHeaders
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 the cross-domain event stream — available › Responses
A page of events.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
Read one event by id — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
events.read· Test mode: unavailable
One immutable event, read-only — events are never created through the API. K8b moved every payment_link.*/payment.*/payout.* producer off the api-v1 route layer and into the product's own service/action layer (EventType's own description names the full list) — a dashboard action, a Bridge webhook reducer or a cron sweep emits too, not only an api-v1-originated mutation. No pre-K8 history is backfilled. Callers: business key, agent, dashboard session. Test mode: unavailable (A4-FIX-6). Test-mode events are not readable through /v1 today — no test object can be created yet; once creates are restored, poll the forwarded per-object */events lists.
path Parameters
id^evt_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read one event by id — available › Responses
The event.
id^evt_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_attypeThe exact same catalogue as EventType, as the RESPONSE-side form (Event.type, WebhookEvent.type, WebhookEndpoint.event_types): an unrecognized member is an opaque string, never a deserialization failure, because a delivery may carry a type added after the client was generated. A REQUEST that names a type still validates against the closed EventType and answers 400 invalid_request for an unknown name. Kept as its own schema so the open-enum marker never reaches a request field. x-swaps-event-status is the same generated live/catalogued marking as on EventType.
api_versionThe Swaps-Version this event was minted under.
updated_atrequestNull for events with no originating API request (a webhook reducer, a cron sweep, a watcher).
Subscribe to events over Server-Sent Events — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
events.read· Test mode: unavailable
The live channel every client should prefer over polling /v1/events — same auth, same envelope, one event per SSE frame with id: set to the event id. Authenticate exactly as on every other /v1 call, with the Authorization bearer token or x-api-key header, sent by opening this connection with fetch() against a readable stream — never native browser EventSource, which cannot set a header and would force the credential into the URL. A credential in the query string or path is never accepted here, or anywhere else in the API. On a dropped connection, reconnect with the Last-Event-ID header set to the last id received; the stream resumes immediately after it, never replaying from the start or skipping ahead. A gap wider than the retention window is not silently bridged — fall back to GET /v1/events?since= to recover it. K8b moved every payment_link.*/payment.*/payout.* producer off the api-v1 route layer and into the product's own service/action layer (EventType's own description names the full list) — a dashboard action, a Bridge webhook reducer or a cron sweep emits too, not only an api-v1-originated mutation. No pre-K8 history is backfilled. Callers: business key, agent, dashboard session. Test mode: unavailable — a test-mode caller gets 503 temporarily_unavailable (side-effect free, no Retry-After); the stream is never forwarded to the dev project, because prod could not revalidate the caller's credential on every tick of a forwarded stream (A4-FIX-6). Test-mode events are not readable through /v1 today — no test object can be created yet; once creates are restored, poll the forwarded per-object */events lists.
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.
Last-Event-IDOn reconnect, the id of the last event received — the stream resumes immediately after it.
Subscribe to events over Server-Sent Events — available › Responses
An open text/event-stream connection, one Event object per frame.
One SSE frame per event: id: evt_... / event: <type> / data: <Event JSON>.
List the account's webhook endpoints — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.read· Test mode: sandbox
Callers: business key or a first-party bearer session (dashboard/agent). Test mode: sandbox — the request forwards to the DEV project (K11-6 fixer round 1, finding #7); the endpoint row, its deliveries and the signing all live there, never in prod.
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.
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 the account's webhook endpoints — available › Responses
A page of webhook endpoints.
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.
Register a new webhook endpoint — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.write· Test mode: sandbox
Registers an https endpoint and returns its signing secret once, in this response only — a replayed Idempotency-Key gets the same body back with secret redacted to null, never the cleartext twice. 409 when the account already has 20 endpoints (the limit). Callers: business key, or a first-party bearer session (dashboard/agent) whose account_members.role is owner — a non-owner bearer gets 403 scope_denied, the same bar api_keys management uses. Test mode: sandbox — the request forwards to the DEV project (K11-6 fixer round 1, finding #7, corrected from a draft claim that the row is registered in prod); the endpoint row, its deliveries and the signing all live there, never in prod.
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.
Register a new webhook endpoint — available › Request Body
urlPublic https only. A private, link-local, loopback or otherwise reserved destination is rejected,
and so is any swaps.app host or a Swaps project's own supabase.co host (the platform never delivers to itself).
event_typesSubset of the closed event registry this endpoint receives. Omitted or empty means "all". An unknown
or retired name is 400 invalid_request. A type marked catalogued in EventType's
x-swaps-event-status is accepted but delivers nothing until its outbox allowlist ships.
descriptionRegister a new webhook endpoint — available › Responses
The new endpoint, with its secret.
id^whe_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_aturlPublic https only. Rejected at create/update time if it is a swaps.app host or a Swaps project's own supabase.co host, or
resolves to a private, link-local, loopback or otherwise reserved destination (SSRF hardening); the delivery worker re-resolves and re-checks
the SAME way immediately before every send attempt and never follows a redirect — a 3xx response
is recorded as a failed delivery attempt, not a success. This narrows, but does not eliminate, a
DNS-rebinding window: the guard's own resolution and the subsequent fetch() are two separate DNS
lookups a hostile or compromised resolver could answer differently (docs/api/WEBHOOKS.md §7).
event_typesEvent types this endpoint receives. Empty means "all" (every type this account can ever emit,
present and future). An unknown or retired name is 400 invalid_request, at create and update alike
(validated against the closed EventType at that time — this response echo is open only because
it is an A1-2 response field, not because an unrecognized value can actually appear here). A type
marked catalogued in EventType's x-swaps-event-status is accepted but delivers nothing until its outbox allowlist ships.
enabledupdated_atdisabled_reasonSet only when enabled is false: manual (the owner disabled it via update) or
auto_disabled_repeated_failures (K9's own auto-disable after too many consecutive
fully-exhausted deliveries — see webhook_deliveries.status). Null whenever enabled is true.
descriptionsecret_prefixThe signing secret's own non-secret prefix (e.g. whsec_ab12) — always present once an endpoint has
a secret, safe to display anywhere the full secret is not (it never re-derives the full value).
secretThe full signing secret, in cleartext. Present ONLY in the response to create and
rotate_secret — stored hashed thereafter, never returned again and never re-derivable.
Read one webhook endpoint — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.read· Test mode: sandbox
Never returns secret after creation — read the delivery log to verify an endpoint is receiving events. Callers: business key or a first-party bearer session (dashboard/agent). Test mode: sandbox (K11-6, §4.2 drift, was declared full) — see webhook_endpoints.list.
path Parameters
id^whe_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read one webhook endpoint — available › Responses
The endpoint.
id^whe_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_aturlPublic https only. Rejected at create/update time if it is a swaps.app host or a Swaps project's own supabase.co host, or
resolves to a private, link-local, loopback or otherwise reserved destination (SSRF hardening); the delivery worker re-resolves and re-checks
the SAME way immediately before every send attempt and never follows a redirect — a 3xx response
is recorded as a failed delivery attempt, not a success. This narrows, but does not eliminate, a
DNS-rebinding window: the guard's own resolution and the subsequent fetch() are two separate DNS
lookups a hostile or compromised resolver could answer differently (docs/api/WEBHOOKS.md §7).
event_typesEvent types this endpoint receives. Empty means "all" (every type this account can ever emit,
present and future). An unknown or retired name is 400 invalid_request, at create and update alike
(validated against the closed EventType at that time — this response echo is open only because
it is an A1-2 response field, not because an unrecognized value can actually appear here). A type
marked catalogued in EventType's x-swaps-event-status is accepted but delivers nothing until its outbox allowlist ships.
enabledupdated_atdisabled_reasonSet only when enabled is false: manual (the owner disabled it via update) or
auto_disabled_repeated_failures (K9's own auto-disable after too many consecutive
fully-exhausted deliveries — see webhook_deliveries.status). Null whenever enabled is true.
descriptionsecret_prefixThe signing secret's own non-secret prefix (e.g. whsec_ab12) — always present once an endpoint has
a secret, safe to display anywhere the full secret is not (it never re-derives the full value).
secretThe full signing secret, in cleartext. Present ONLY in the response to create and
rotate_secret — stored hashed thereafter, never returned again and never re-derivable.
Remove a webhook endpoint — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.write· Test mode: sandbox
Deletes the endpoint and stops all future deliveries to it; past deliveries stay in the log. Callers: business key, or a first-party bearer session (dashboard/agent) whose account_members.role is owner — a non-owner bearer gets 403 scope_denied, the same bar api_keys management uses. Test mode: sandbox (K11-6, §4.2 drift, was declared full) — see webhook_endpoints.list.
path Parameters
id^whe_ · 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.
Remove a webhook endpoint — available › Responses
Deleted.
Update url, event types, description or enabled state — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.write· Test mode: sandbox
Updates the url, subscribed event types, description, or enables/disables the endpoint. Never returns or rotates secret — use rotate_secret for that. Callers: business key, or a first-party bearer session (dashboard/agent) whose account_members.role is owner — a non-owner bearer gets 403 scope_denied, the same bar api_keys management uses (a bearer who could repoint url to a host they control would receive the account's signed deliveries). Test mode: sandbox (K11-6, §4.2 drift, was declared full) — see webhook_endpoints.list.
path Parameters
id^whe_ · 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 url, event types, description or enabled state — available › Request Body
urlPublic https only. A private, link-local, loopback or otherwise reserved destination is rejected,
and so is any swaps.app host or a Swaps project's own supabase.co host (the platform never delivers to itself).
event_typesAn unknown or retired name is 400 invalid_request.
enabledSetting false disables the endpoint (disabled_reason becomes manual) and halts future
deliveries — past deliveries stay in the log. Setting true re-enables it and clears
disabled_reason, including one an auto-disable had set.
descriptionUpdate url, event types, description or enabled state — available › Responses
The updated endpoint.
id^whe_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_aturlPublic https only. Rejected at create/update time if it is a swaps.app host or a Swaps project's own supabase.co host, or
resolves to a private, link-local, loopback or otherwise reserved destination (SSRF hardening); the delivery worker re-resolves and re-checks
the SAME way immediately before every send attempt and never follows a redirect — a 3xx response
is recorded as a failed delivery attempt, not a success. This narrows, but does not eliminate, a
DNS-rebinding window: the guard's own resolution and the subsequent fetch() are two separate DNS
lookups a hostile or compromised resolver could answer differently (docs/api/WEBHOOKS.md §7).
event_typesEvent types this endpoint receives. Empty means "all" (every type this account can ever emit,
present and future). An unknown or retired name is 400 invalid_request, at create and update alike
(validated against the closed EventType at that time — this response echo is open only because
it is an A1-2 response field, not because an unrecognized value can actually appear here). A type
marked catalogued in EventType's x-swaps-event-status is accepted but delivers nothing until its outbox allowlist ships.
enabledupdated_atdisabled_reasonSet only when enabled is false: manual (the owner disabled it via update) or
auto_disabled_repeated_failures (K9's own auto-disable after too many consecutive
fully-exhausted deliveries — see webhook_deliveries.status). Null whenever enabled is true.
descriptionsecret_prefixThe signing secret's own non-secret prefix (e.g. whsec_ab12) — always present once an endpoint has
a secret, safe to display anywhere the full secret is not (it never re-derives the full value).
secretThe full signing secret, in cleartext. Present ONLY in the response to create and
rotate_secret — stored hashed thereafter, never returned again and never re-derivable.
Rotate the endpoint's signing secret — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.write· Test mode: sandbox
Issues a new signing secret and keeps the OLD one live for a 24h overlap window — every delivery in that window is signed with BOTH keys, so a receiver still configured with the old secret keeps validating. Returns the new secret once, in this response only, exactly like create (redacted from a replayed Idempotency-Key's stored body). Callers: business key, or a first-party bearer session (dashboard/agent) whose account_members.role is owner — a non-owner bearer gets 403 scope_denied, the same bar api_keys management uses. Test mode: sandbox (K11-6, §4.2 drift, was declared full) — see webhook_endpoints.list.
path Parameters
id^whe_ · 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.
Rotate the endpoint's signing secret — available › Responses
The endpoint, with its new secret.
id^whe_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_aturlPublic https only. Rejected at create/update time if it is a swaps.app host or a Swaps project's own supabase.co host, or
resolves to a private, link-local, loopback or otherwise reserved destination (SSRF hardening); the delivery worker re-resolves and re-checks
the SAME way immediately before every send attempt and never follows a redirect — a 3xx response
is recorded as a failed delivery attempt, not a success. This narrows, but does not eliminate, a
DNS-rebinding window: the guard's own resolution and the subsequent fetch() are two separate DNS
lookups a hostile or compromised resolver could answer differently (docs/api/WEBHOOKS.md §7).
event_typesEvent types this endpoint receives. Empty means "all" (every type this account can ever emit,
present and future). An unknown or retired name is 400 invalid_request, at create and update alike
(validated against the closed EventType at that time — this response echo is open only because
it is an A1-2 response field, not because an unrecognized value can actually appear here). A type
marked catalogued in EventType's x-swaps-event-status is accepted but delivers nothing until its outbox allowlist ships.
enabledupdated_atdisabled_reasonSet only when enabled is false: manual (the owner disabled it via update) or
auto_disabled_repeated_failures (K9's own auto-disable after too many consecutive
fully-exhausted deliveries — see webhook_deliveries.status). Null whenever enabled is true.
descriptionsecret_prefixThe signing secret's own non-secret prefix (e.g. whsec_ab12) — always present once an endpoint has
a secret, safe to display anywhere the full secret is not (it never re-derives the full value).
secretThe full signing secret, in cleartext. Present ONLY in the response to create and
rotate_secret — stored hashed thereafter, never returned again and never re-derivable.
Send a synthetic test event to this endpoint — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.write· Test mode: sandbox
Emits one test.ping event and enqueues exactly one delivery, to this endpoint only — never fanned out to any other endpoint, even one that also subscribes to test.ping (or subscribes to "all" via an empty event_types). The event's livemode matches the caller's own key/session mode. 409 when the endpoint is disabled; 503 while api_v1.webhooks_delivery itself is off (queuing a test would sit forever with nothing to process it). Callers: business key, or a first-party bearer session (dashboard/agent) whose account_members.role is owner — gated the same way as create/update/rotate_secret/delete, for consistency: a non-owner bearer gets 403 scope_denied. Test mode: sandbox (K11-6, §4.2 drift, was declared full) — the queued delivery must be signed and sent by the DEV project's api-webhooks-worker, or the test ping never fires.
path Parameters
id^whe_ · 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.
Send a synthetic test event to this endpoint — available › Responses
The queued test delivery.
id^whd_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atendpoint_id^whe_ · requiredevent_id^evt_ · requiredattemptHow many delivery attempts this row has made so far (the worker's own retry schedule; see docs/api/WEBHOOKS.md).
statuspending: never yet attempted, due now. succeeded: a 2xx response — terminal. failed: the most
recent attempt failed and a further retry is already scheduled at next_attempt_at (automatic — no
action needed). exhausted: every scheduled attempt failed and no further retry will happen — replay
it explicitly with webhook_deliveries.replay.
updated_atnext_attempt_atresponse_statusThe HTTP status the endpoint returned, or null if the attempt never got a response (timeout, DNS/SSRF refusal, connection error).
response_msRound-trip time in milliseconds for the most recent attempt. The response BODY is never stored.
errorA short machine-readable failure reason for the most recent attempt (e.g. timeout, connection_refused, non_2xx_response, url_not_allowed). Never the endpoint's response body.
delivered_atSet once, when status first becomes succeeded.
List webhook delivery attempts — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.read· Test mode: sandbox
Every delivery attempted or scheduled across this account's endpoints, newest first — filter by endpoint_id or status. Callers: business key or a first-party bearer session (dashboard/agent). Test mode: sandbox (K11-6, §4.2 drift, was declared full) — a test caller's deliveries only ever exist on the DEV project, written by its api-webhooks-worker.
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.
endpoint_id^whe_statusHeaders
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 webhook delivery attempts — available › Responses
A page of deliveries.
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.
Read one webhook delivery — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.read· Test mode: sandbox
Callers: business key or a first-party bearer session (dashboard/agent). Test mode: sandbox (K11-6, §4.2
drift, was declared full) — see webhook_deliveries.list.
path Parameters
id^whd_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read one webhook delivery — available › Responses
The delivery.
id^whd_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atendpoint_id^whe_ · requiredevent_id^evt_ · requiredattemptHow many delivery attempts this row has made so far (the worker's own retry schedule; see docs/api/WEBHOOKS.md).
statuspending: never yet attempted, due now. succeeded: a 2xx response — terminal. failed: the most
recent attempt failed and a further retry is already scheduled at next_attempt_at (automatic — no
action needed). exhausted: every scheduled attempt failed and no further retry will happen — replay
it explicitly with webhook_deliveries.replay.
updated_atnext_attempt_atresponse_statusThe HTTP status the endpoint returned, or null if the attempt never got a response (timeout, DNS/SSRF refusal, connection error).
response_msRound-trip time in milliseconds for the most recent attempt. The response BODY is never stored.
errorA short machine-readable failure reason for the most recent attempt (e.g. timeout, connection_refused, non_2xx_response, url_not_allowed). Never the endpoint's response body.
delivered_atSet once, when status first becomes succeeded.
Replay a webhook delivery — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.write· Test mode: sandbox
Enqueues a NEW delivery for the same event, to the same endpoint — a fresh WebhookDelivery row, distinct from (and never mutating) the one replayed; the original stays in the log exactly as it was. Safe to call repeatedly. 409 when the endpoint is currently disabled, or when its CURRENT event_types no longer include this event's type. Callers: business key or a first-party bearer session (dashboard/agent). Test mode: sandbox (K11-6, §4.2 drift, was declared full) — the new delivery it enqueues is signed and sent by the DEV project's api-webhooks-worker, same as the original.
path Parameters
id^whd_ · 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.
Replay a webhook delivery — available › Responses
The new delivery, queued.
id^whd_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atendpoint_id^whe_ · requiredevent_id^evt_ · requiredattemptHow many delivery attempts this row has made so far (the worker's own retry schedule; see docs/api/WEBHOOKS.md).
statuspending: never yet attempted, due now. succeeded: a 2xx response — terminal. failed: the most
recent attempt failed and a further retry is already scheduled at next_attempt_at (automatic — no
action needed). exhausted: every scheduled attempt failed and no further retry will happen — replay
it explicitly with webhook_deliveries.replay.
updated_atnext_attempt_atresponse_statusThe HTTP status the endpoint returned, or null if the attempt never got a response (timeout, DNS/SSRF refusal, connection error).
response_msRound-trip time in milliseconds for the most recent attempt. The response BODY is never stored.
errorA short machine-readable failure reason for the most recent attempt (e.g. timeout, connection_refused, non_2xx_response, url_not_allowed). Never the endpoint's response body.
delivered_atSet once, when status first becomes succeeded.
Join the webhook-delivery waitlist — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
account.write· Test mode: unavailable
Captures an email ahead of the full webhook system existing — the "Join the waitlist" action on the Developers hub. Callers: business key or a first-party bearer session (dashboard/agent). Test mode: full.
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.
Join the webhook-delivery waitlist — available › Request Body
emailJoin the webhook-delivery waitlist — available › Responses
Captured.
Join the card waitlist — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
account.write· Test mode: unavailable
Write-only capture ahead of a card product existing — no corresponding read and no card-issuing machinery behind this call today. Callers: business key, agent, dashboard session (A1-2: widened to match the router's own BOTH_CALLERS entry — the security block below, businessKey, was already the truthful half). Test mode: full.
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.
Join the card waitlist — available › Request Body
emailJoin the card waitlist — available › Responses
Captured.
List saved payment destinations — available
Status: Available · Callers: agent (business key), dashboard session · Scope:
address_book.read· Test mode: unavailable
Every saved destination across all seven rail shapes — crypto and six bank rails. Bank-rail fields are masked on every read regardless of caller. Callers: agent (holder), dashboard session — never a business key (review ruling P1-2: no account-level address book exists yet, so a business-key read would show one member's saved destinations to any other caller holding a key for the same account). Test mode: fixture addresses.
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.
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 saved payment destinations — available › Responses
A page of saved destinations.
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 new payment destination — available
Status: Available · Callers: agent (business key), dashboard session · Scope:
address_book.write· Test mode: unavailable
Saves one of seven rail shapes, discriminated by rail. Bank-rail details cross the boundary once here and are masked on every subsequent read — most clients log tool arguments verbatim. Callers: agent (holder), dashboard session, never a business key. Test mode: fixture addresses.
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 new payment destination — available › Request Body
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object · rail="crypto" · requires: label, network, address | |
| type = object · rail="iban" · requires: label, account_holder, iban +1 more | |
| type = object · rail="ach" · requires: label, account_holder, account_number +2 more | |
| type = object · rail="faster_payments" · requires: label, account_holder, sort_code +1 more | |
| type = object · rail="pix" · requires: label, account_holder, pix_key +1 more | |
| type = object · rail="spei" · requires: label, account_holder, clabe | |
| type = object · rail="swift" · requires: label, account_holder, swift_bic +2 more |
labelrailnetworkSaved as sent; any network can be saved. Screening covers only the networks ScreeningCreateRequest.chain lists — an entry on any other reads screening: null and its address_book.recheck answers 409 network_not_supported.
addressCase-preserved verbatim — never lowercase a BTC, Tron or Solana address.
Save a new payment destination — available › Responses
The saved destination.
id^adr_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atlabelrailupdated_atnetworkrail: crypto only.
addressrail: crypto only. Case-preserved verbatim — never lowercased.
iban_maskedrail: iban only.
bicrail: iban or swift.
routing_number_maskedrail: ach only.
account_number_maskedrail: ach, faster_payments or swift.
sort_code_maskedrail: faster_payments only.
pix_key_typerail: pix only.
pix_key_maskedrail: pix only.
clabe_maskedrail: spei only.
bank_addressrail: swift only.
rail: crypto only — the latest screening of address on network by this account, from screenings.create or address_book.recheck. network is matched the way screenings.create reads chain (a chain id or name, case-insensitive), so an entry saved as Ethereum carries a screening recorded under chain 1. Null only when no such screening exists: no /v1 screening of it yet (checks made in the v1 dashboard are not carried over, so null does not mean never checked), or screening does not cover the entry — a network such as Tempo, or an address that cannot exist on its network — and its address_book.recheck answers 409 network_not_supported, never an Ethereum verdict.
last_checked_atscreened_at of screening; null exactly when screening is null — a check made in the v1 dashboard is not carried over.
self for the holder's own destination, third-party once a beneficiary attestation exists.
Null until a beneficiary attestation exists. Set to individual/legal_entity/vasp_customer by address_book.beneficiary.update; set to self_verified/self_unverified by the dashboard's proof-of-control flow (not itself exposed on /v1).
Read-only. Null until the destination is verified through the dashboard's proof-of-control flow — that verification step is not exposed on /v1 (out of scope for this operation). micro_deposit is published because a live v1 writer already uses it, even though today's DB CHECK constraint has not been widened to accept it (a separate v1 bug) — publishing it now costs nothing, no consumers exist yet.
proof_of_control_verified_atRead-only. Non-null rows are what the address-book hub counts as "verified".
proof_of_control_artifact_urlRead-only. A Supabase Storage object key in a private bucket — not a directly fetchable public URL.
travel_rule_requiredTrue once a transfer through this destination triggered EU TFR / MiCA / FATF R.16 evaluation.
travel_rule_last_attested_atNull until a transfer through this destination has been Travel-Rule-attested. Stamped only by the transfer-time attestation (attestTransfer, mid-transfer) — never written by any /v1 operation, including address_book.beneficiary.update.
counterparty_nameThe saved beneficiary's name. Null until a beneficiary attestation exists. Set by address_book.beneficiary.update. No pattern/length constraint on this READ shape, deliberately — so a row already stored before a request-side constraint existed can never fail the whole list/get response, the same failure mode finding #5 flagged for proof_of_control_method. The constraint lives on the request schema (TravelRuleAttestationCreateRequest) instead.
counterparty_countryISO 3166-1 alpha-2, uppercased on write. Null until a beneficiary attestation exists.
counterparty_vasp_nameThe counterparty VASP's name. Only set when beneficiary_subtype is vasp_customer.
counterparty_vasp_jurisdictionISO 3166-1 alpha-2, uppercased on write. Only set when beneficiary_subtype is vasp_customer.
Read one saved destination — available
Status: Available · Callers: agent (business key), dashboard session · Scope:
address_book.read· Test mode: unavailable
Callers: agent (holder), dashboard session — never a business key (review ruling P1-2, same reasoning as
address_book.list). Test mode: fixture addresses.
path Parameters
id^adr_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read one saved destination — available › Responses
The destination.
id^adr_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atlabelrailupdated_atnetworkrail: crypto only.
addressrail: crypto only. Case-preserved verbatim — never lowercased.
iban_maskedrail: iban only.
bicrail: iban or swift.
routing_number_maskedrail: ach only.
account_number_maskedrail: ach, faster_payments or swift.
sort_code_maskedrail: faster_payments only.
pix_key_typerail: pix only.
pix_key_maskedrail: pix only.
clabe_maskedrail: spei only.
bank_addressrail: swift only.
rail: crypto only — the latest screening of address on network by this account, from screenings.create or address_book.recheck. network is matched the way screenings.create reads chain (a chain id or name, case-insensitive), so an entry saved as Ethereum carries a screening recorded under chain 1. Null only when no such screening exists: no /v1 screening of it yet (checks made in the v1 dashboard are not carried over, so null does not mean never checked), or screening does not cover the entry — a network such as Tempo, or an address that cannot exist on its network — and its address_book.recheck answers 409 network_not_supported, never an Ethereum verdict.
last_checked_atscreened_at of screening; null exactly when screening is null — a check made in the v1 dashboard is not carried over.
self for the holder's own destination, third-party once a beneficiary attestation exists.
Null until a beneficiary attestation exists. Set to individual/legal_entity/vasp_customer by address_book.beneficiary.update; set to self_verified/self_unverified by the dashboard's proof-of-control flow (not itself exposed on /v1).
Read-only. Null until the destination is verified through the dashboard's proof-of-control flow — that verification step is not exposed on /v1 (out of scope for this operation). micro_deposit is published because a live v1 writer already uses it, even though today's DB CHECK constraint has not been widened to accept it (a separate v1 bug) — publishing it now costs nothing, no consumers exist yet.
proof_of_control_verified_atRead-only. Non-null rows are what the address-book hub counts as "verified".
proof_of_control_artifact_urlRead-only. A Supabase Storage object key in a private bucket — not a directly fetchable public URL.
travel_rule_requiredTrue once a transfer through this destination triggered EU TFR / MiCA / FATF R.16 evaluation.
travel_rule_last_attested_atNull until a transfer through this destination has been Travel-Rule-attested. Stamped only by the transfer-time attestation (attestTransfer, mid-transfer) — never written by any /v1 operation, including address_book.beneficiary.update.
counterparty_nameThe saved beneficiary's name. Null until a beneficiary attestation exists. Set by address_book.beneficiary.update. No pattern/length constraint on this READ shape, deliberately — so a row already stored before a request-side constraint existed can never fail the whole list/get response, the same failure mode finding #5 flagged for proof_of_control_method. The constraint lives on the request schema (TravelRuleAttestationCreateRequest) instead.
counterparty_countryISO 3166-1 alpha-2, uppercased on write. Null until a beneficiary attestation exists.
counterparty_vasp_nameThe counterparty VASP's name. Only set when beneficiary_subtype is vasp_customer.
counterparty_vasp_jurisdictionISO 3166-1 alpha-2, uppercased on write. Only set when beneficiary_subtype is vasp_customer.
Remove a saved destination — available
Status: Available · Callers: agent (business key), dashboard session · Scope:
address_book.write· Test mode: unavailable
Callers: agent (holder), dashboard session, never a business key. Test mode: fixture addresses.
path Parameters
id^adr_ · 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.
Remove a saved destination — available › Responses
Deleted.
Rename a saved destination — available
Status: Available · Callers: agent (business key), dashboard session · Scope:
address_book.write· Test mode: unavailable
Updates the label only — every rail field is immutable after creation; remove and re-add to change a destination. Callers: agent (holder), dashboard session, never a business key. Test mode: fixture addresses.
path Parameters
id^adr_ · 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.
Rename a saved destination — available › Request Body
labelRename a saved destination — available › Responses
The updated destination.
id^adr_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atlabelrailupdated_atnetworkrail: crypto only.
addressrail: crypto only. Case-preserved verbatim — never lowercased.
iban_maskedrail: iban only.
bicrail: iban or swift.
routing_number_maskedrail: ach only.
account_number_maskedrail: ach, faster_payments or swift.
sort_code_maskedrail: faster_payments only.
pix_key_typerail: pix only.
pix_key_maskedrail: pix only.
clabe_maskedrail: spei only.
bank_addressrail: swift only.
rail: crypto only — the latest screening of address on network by this account, from screenings.create or address_book.recheck. network is matched the way screenings.create reads chain (a chain id or name, case-insensitive), so an entry saved as Ethereum carries a screening recorded under chain 1. Null only when no such screening exists: no /v1 screening of it yet (checks made in the v1 dashboard are not carried over, so null does not mean never checked), or screening does not cover the entry — a network such as Tempo, or an address that cannot exist on its network — and its address_book.recheck answers 409 network_not_supported, never an Ethereum verdict.
last_checked_atscreened_at of screening; null exactly when screening is null — a check made in the v1 dashboard is not carried over.
self for the holder's own destination, third-party once a beneficiary attestation exists.
Null until a beneficiary attestation exists. Set to individual/legal_entity/vasp_customer by address_book.beneficiary.update; set to self_verified/self_unverified by the dashboard's proof-of-control flow (not itself exposed on /v1).
Read-only. Null until the destination is verified through the dashboard's proof-of-control flow — that verification step is not exposed on /v1 (out of scope for this operation). micro_deposit is published because a live v1 writer already uses it, even though today's DB CHECK constraint has not been widened to accept it (a separate v1 bug) — publishing it now costs nothing, no consumers exist yet.
proof_of_control_verified_atRead-only. Non-null rows are what the address-book hub counts as "verified".
proof_of_control_artifact_urlRead-only. A Supabase Storage object key in a private bucket — not a directly fetchable public URL.
travel_rule_requiredTrue once a transfer through this destination triggered EU TFR / MiCA / FATF R.16 evaluation.
travel_rule_last_attested_atNull until a transfer through this destination has been Travel-Rule-attested. Stamped only by the transfer-time attestation (attestTransfer, mid-transfer) — never written by any /v1 operation, including address_book.beneficiary.update.
counterparty_nameThe saved beneficiary's name. Null until a beneficiary attestation exists. Set by address_book.beneficiary.update. No pattern/length constraint on this READ shape, deliberately — so a row already stored before a request-side constraint existed can never fail the whole list/get response, the same failure mode finding #5 flagged for proof_of_control_method. The constraint lives on the request schema (TravelRuleAttestationCreateRequest) instead.
counterparty_countryISO 3166-1 alpha-2, uppercased on write. Null until a beneficiary attestation exists.
counterparty_vasp_nameThe counterparty VASP's name. Only set when beneficiary_subtype is vasp_customer.
counterparty_vasp_jurisdictionISO 3166-1 alpha-2, uppercased on write. Only set when beneficiary_subtype is vasp_customer.
Re-screen a saved crypto address — available
Status: Available · Callers: agent (business key), dashboard session · Scope:
address_book.write· Test mode: unavailable
Re-runs a free screening against a saved crypto destination on its own network and returns the entry carrying that fresh screening and its last_checked_at — a 200 never answers screening: null for a crypto entry; a no-op on a bank-rail entry. An entry whose network screening does not cover (Tempo, for one), or whose address cannot exist on it, is refused 409 network_not_supported before anything is screened or recorded — never screened as Ethereum. Callers: agent (holder), dashboard session, never a business key. Test mode: fixture addresses.
path Parameters
id^adr_ · 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.
Re-screen a saved crypto address — available › Responses
The destination, with a refreshed screening.
id^adr_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atlabelrailupdated_atnetworkrail: crypto only.
addressrail: crypto only. Case-preserved verbatim — never lowercased.
iban_maskedrail: iban only.
bicrail: iban or swift.
routing_number_maskedrail: ach only.
account_number_maskedrail: ach, faster_payments or swift.
sort_code_maskedrail: faster_payments only.
pix_key_typerail: pix only.
pix_key_maskedrail: pix only.
clabe_maskedrail: spei only.
bank_addressrail: swift only.
rail: crypto only — the latest screening of address on network by this account, from screenings.create or address_book.recheck. network is matched the way screenings.create reads chain (a chain id or name, case-insensitive), so an entry saved as Ethereum carries a screening recorded under chain 1. Null only when no such screening exists: no /v1 screening of it yet (checks made in the v1 dashboard are not carried over, so null does not mean never checked), or screening does not cover the entry — a network such as Tempo, or an address that cannot exist on its network — and its address_book.recheck answers 409 network_not_supported, never an Ethereum verdict.
last_checked_atscreened_at of screening; null exactly when screening is null — a check made in the v1 dashboard is not carried over.
self for the holder's own destination, third-party once a beneficiary attestation exists.
Null until a beneficiary attestation exists. Set to individual/legal_entity/vasp_customer by address_book.beneficiary.update; set to self_verified/self_unverified by the dashboard's proof-of-control flow (not itself exposed on /v1).
Read-only. Null until the destination is verified through the dashboard's proof-of-control flow — that verification step is not exposed on /v1 (out of scope for this operation). micro_deposit is published because a live v1 writer already uses it, even though today's DB CHECK constraint has not been widened to accept it (a separate v1 bug) — publishing it now costs nothing, no consumers exist yet.
proof_of_control_verified_atRead-only. Non-null rows are what the address-book hub counts as "verified".
proof_of_control_artifact_urlRead-only. A Supabase Storage object key in a private bucket — not a directly fetchable public URL.
travel_rule_requiredTrue once a transfer through this destination triggered EU TFR / MiCA / FATF R.16 evaluation.
travel_rule_last_attested_atNull until a transfer through this destination has been Travel-Rule-attested. Stamped only by the transfer-time attestation (attestTransfer, mid-transfer) — never written by any /v1 operation, including address_book.beneficiary.update.
counterparty_nameThe saved beneficiary's name. Null until a beneficiary attestation exists. Set by address_book.beneficiary.update. No pattern/length constraint on this READ shape, deliberately — so a row already stored before a request-side constraint existed can never fail the whole list/get response, the same failure mode finding #5 flagged for proof_of_control_method. The constraint lives on the request schema (TravelRuleAttestationCreateRequest) instead.
counterparty_countryISO 3166-1 alpha-2, uppercased on write. Null until a beneficiary attestation exists.
counterparty_vasp_nameThe counterparty VASP's name. Only set when beneficiary_subtype is vasp_customer.
counterparty_vasp_jurisdictionISO 3166-1 alpha-2, uppercased on write. Only set when beneficiary_subtype is vasp_customer.
Update the Travel Rule beneficiary/counterparty details for a saved destination — available
Status: Available · Callers: agent (business key), dashboard session · Scope:
address_book.write· Test mode: unavailable
Drift blocker from the 28.09 v1→v2 audit (DRIFT-v1-v2-2026-09-28.md item 1) — mirrors the dashboard's Manage tab (CounterpartyForm/CounterpartyModal), the actual v1 write path for these fields (a direct address_book update, not the separate append-only travel_rule_attestations audit log, which is stamped only mid-transfer, by attestTransfer, with originator KYC context and a transaction_id this operation does not have). Sets beneficiary_type to third-party and stores beneficiary_subtype + the counterparty details; it never writes travel_rule_last_attested_at (fixer round 1 #1) — that column stays read-only here and is stamped only by the transfer-time attestation. counterparty_* are readable on every read (fixer round 1 #2), so a Manage-tab re-save can pre-fill from them. Proof-of-control verification is a separate, out-of-scope flow; proof_of_control_* on the response reflect the entry's existing state, untouched by this call. Callers: agent (holder), dashboard session, never a business key — same reasoning as every other address_book write. Test mode: fixture addresses.
path Parameters
id^adr_ · 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 the Travel Rule beneficiary/counterparty details for a saved destination — available › Request Body
beneficiary_subtypecounterparty_namecounterparty_country^[A-Za-z]{2}$ISO 3166-1 alpha-2, either case (uppercased on write).
counterparty_vasp_nameRequired when beneficiary_subtype is vasp_customer; refused blank, exactly like the dashboard form.
counterparty_vasp_jurisdiction^[A-Za-z]{2}$ISO 3166-1 alpha-2, either case (uppercased on write). Only meaningful when beneficiary_subtype is vasp_customer.
Update the Travel Rule beneficiary/counterparty details for a saved destination — available › Responses
The destination, with the beneficiary details updated.
id^adr_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atlabelrailupdated_atnetworkrail: crypto only.
addressrail: crypto only. Case-preserved verbatim — never lowercased.
iban_maskedrail: iban only.
bicrail: iban or swift.
routing_number_maskedrail: ach only.
account_number_maskedrail: ach, faster_payments or swift.
sort_code_maskedrail: faster_payments only.
pix_key_typerail: pix only.
pix_key_maskedrail: pix only.
clabe_maskedrail: spei only.
bank_addressrail: swift only.
rail: crypto only — the latest screening of address on network by this account, from screenings.create or address_book.recheck. network is matched the way screenings.create reads chain (a chain id or name, case-insensitive), so an entry saved as Ethereum carries a screening recorded under chain 1. Null only when no such screening exists: no /v1 screening of it yet (checks made in the v1 dashboard are not carried over, so null does not mean never checked), or screening does not cover the entry — a network such as Tempo, or an address that cannot exist on its network — and its address_book.recheck answers 409 network_not_supported, never an Ethereum verdict.
last_checked_atscreened_at of screening; null exactly when screening is null — a check made in the v1 dashboard is not carried over.
self for the holder's own destination, third-party once a beneficiary attestation exists.
Null until a beneficiary attestation exists. Set to individual/legal_entity/vasp_customer by address_book.beneficiary.update; set to self_verified/self_unverified by the dashboard's proof-of-control flow (not itself exposed on /v1).
Read-only. Null until the destination is verified through the dashboard's proof-of-control flow — that verification step is not exposed on /v1 (out of scope for this operation). micro_deposit is published because a live v1 writer already uses it, even though today's DB CHECK constraint has not been widened to accept it (a separate v1 bug) — publishing it now costs nothing, no consumers exist yet.
proof_of_control_verified_atRead-only. Non-null rows are what the address-book hub counts as "verified".
proof_of_control_artifact_urlRead-only. A Supabase Storage object key in a private bucket — not a directly fetchable public URL.
travel_rule_requiredTrue once a transfer through this destination triggered EU TFR / MiCA / FATF R.16 evaluation.
travel_rule_last_attested_atNull until a transfer through this destination has been Travel-Rule-attested. Stamped only by the transfer-time attestation (attestTransfer, mid-transfer) — never written by any /v1 operation, including address_book.beneficiary.update.
counterparty_nameThe saved beneficiary's name. Null until a beneficiary attestation exists. Set by address_book.beneficiary.update. No pattern/length constraint on this READ shape, deliberately — so a row already stored before a request-side constraint existed can never fail the whole list/get response, the same failure mode finding #5 flagged for proof_of_control_method. The constraint lives on the request schema (TravelRuleAttestationCreateRequest) instead.
counterparty_countryISO 3166-1 alpha-2, uppercased on write. Null until a beneficiary attestation exists.
counterparty_vasp_nameThe counterparty VASP's name. Only set when beneficiary_subtype is vasp_customer.
counterparty_vasp_jurisdictionISO 3166-1 alpha-2, uppercased on write. Only set when beneficiary_subtype is vasp_customer.
Read the holder's credit balance — available
Status: Available · Callers: dashboard session, agent (business key) · Scope:
credits.read· Test mode: unavailable
Free and paid balances plus a lifetime check count — the figures behind every credit-gated screen in Tools and Settings. Callers: dashboard session, agent (holder) — never a business key (review ruling P1-2: no account-level credit ledger exists yet, so this resolves the account OWNER's personal balance; a business-key read would show it to any other caller holding a key for the same account). Test mode: unavailable (K11-6 fixer round 1, finding #6) — the ledger is billed through Stripe and there is no test purchase to read against yet (credit_checkouts.create, design §9 D-4), so this read carries no restored test-mode claim.
Headers
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read the holder's credit balance — available › Responses
The balance.
free_remainingpaid_balancetotal_checksList the credit ledger — available
Status: Available · Callers: dashboard session, agent (business key) · Scope:
credits.read· Test mode: unavailable
One of seven event types per row (consume_free, consume_paid, cache_hit, insufficient, purchase, refund, admin_grant), newest first, including the screened address/chain on consume rows. Callers: dashboard session, agent (holder) — never a business key (review ruling P1-2, same reasoning as credits.get). Test mode: unavailable (K11-6 fixer round 1, finding #6) — same reasoning as credits.get above.
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.
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 the credit ledger — available › Responses
A page of ledger rows.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
events_totalLifetime, exact row count of every credit_events entry for this account, across all type values — never capped by limit/cursor pagination and never limited to the page currently returned in data. Omitted (not null, not 0) on the rare failure of the underlying count query — a caller must treat a missing field as "unknown right now", not as zero.
Displayed side by side with GET /v1/credits's own total_checks as "N events · M reports" — never summed. The two counters are not on the same basis and are not reconcilable against each other: events_total is an append-only row count (it includes the refund row itself, alongside the consume_free/consume_paid row it refunds), while total_checks is a net counter that is incremented on consume and DECREMENTED, clamped at zero, on refund. Summing them double-counts every refunded check, and the two drift further apart with every refund an account accumulates.
Start a Stripe checkout for a credit pack — available
Status: Available · Callers: dashboard session · Scope:
credits.write· Test mode: unavailable
Creates a Stripe Checkout session for one of the three credit packs (5, 10 or 20) and returns its url — redirect the holder there; this call never itself grants credits. Callers: dashboard session, agent (holder). Test mode: unavailable (K11-6, §4.2 drift, corrects an earlier "Stripe test mode" claim) — it opens a real payment for credits and there is no test purchase; refused pending the founder's decision (design D-4). credits.get/ credit_events.list are unaffected — they read the account's own credit ledger, not this checkout.
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.
Start a Stripe checkout for a credit pack — available › Request Body
packStart a Stripe checkout for a credit pack — available › Responses
The checkout session.
urlA Stripe Checkout session url — redirect the holder there to complete the purchase.
List the account's API keys — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.read· Test mode: full
Never returns secret — only create and roll ever do, once each. Callers: dashboard session or business key (read-only) — create/roll/revoke are never a business key, only dashboard session. Test mode: full (K11-4) — credentials and the outer usage log are prod-only by design (§2.3), so a livemode=false caller's own test key(s) answer from the same real row this operation always read, scoped to the caller's test twin account; a live caller's keys and a test caller's are never each other's to see.
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.
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 the account's API keys — available › Responses
A page of keys.
has_morenext_cursorA1-2: required (still nullable) — every producer already emits null rather than omitting the field when there is no next page, so this only removes a third, unused wire state (absent) a strict client had to handle for nothing.
Create a new API key — available
Status: Available · Callers: dashboard session · Scope:
developers.write· Test mode: full
Issues a key through the create_api_key_for_account RPC (K10) and returns its plaintext secret once, in this response only — a replayed Idempotency-Key gets the same body back with secret redacted to null, never the cleartext twice (the same mechanism K9 introduced for webhook_endpoints.create). Callers: dashboard session only — a key can never mint another key. Test mode: full (K11-4) — livemode:false resolves-or-creates the caller's test twin account (a second, livemode=false account row idempotently linked to the caller's own) and mints the sk_test_ key on the twin, never on the caller's live account; this is the one call site a livemode=false account can be created from.
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 new API key — available › Request Body
namescopes<resource>.<read|write>. The legacy screening.read/screening.write are deprecated aliases, accepted until the next Swaps-Version date and stored as screenings.read/screenings.write.
livemodedefault_swaps_versionOptional; defaults to the current Swaps-Version when omitted.
allowed_ipsexpires_atCreate a new API key — available › Responses
The new key, with its secret.
id^key_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atnamekey_prefixsk_live_ or sk_test_, matching livemode.
last_fourdefault_swaps_versionThe Swaps-Version this key pins requests to when the caller sends no Swaps-Version header
(API-CANON §3, RESOURCE-MODEL §0.6). A request that does send the header overrides this default
for that call only; this field is unaffected.
scopesA key created before the screenings.* rename may still list the legacy screening.read/screening.write.
statussecretThe full sk_live_… / sk_test_… value, in cleartext. Present only in the FIRST live response to
create/roll — stored hashed thereafter, never returned again and never re-derivable. A replayed
Idempotency-Key against the same request gets the same body back with this field redacted to null,
never the cleartext a second time (AuthenticatedRoute.secretFields, the same mechanism K9
introduced for webhook_endpoints.create/.rotate_secret).
updated_atallowed_ipsexpires_atlast_used_atrevoked_atRead one API key — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.read· Test mode: full
Callers: dashboard session or business key (read-only) — create/roll/revoke are never a business key, only dashboard session. Test mode: full (K11-4) — scoped by account_id, which for a test caller is the twin account, never the live one.
path Parameters
id^key_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read one API key — available › Responses
The key.
id^key_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atnamekey_prefixsk_live_ or sk_test_, matching livemode.
last_fourdefault_swaps_versionThe Swaps-Version this key pins requests to when the caller sends no Swaps-Version header
(API-CANON §3, RESOURCE-MODEL §0.6). A request that does send the header overrides this default
for that call only; this field is unaffected.
scopesA key created before the screenings.* rename may still list the legacy screening.read/screening.write.
statusupdated_atallowed_ipsexpires_atlast_used_atrevoked_atRotate a key's secret — available
Status: Available · Callers: dashboard session · Scope:
developers.write· Test mode: full
Issues a new secret for the same key identity (via the roll_api_key RPC) and invalidates the old one; the new secret is returned once, in this response only — a replayed Idempotency-Key gets the same body back with secret redacted to null, never the cleartext twice. Callers: dashboard session only. Test mode: full (K11-4) — rolls a key already owned by the caller's own account (live or twin); never resolves a twin itself.
path Parameters
id^key_ · 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.
Rotate a key's secret — available › Responses
The key, with its new secret.
id^key_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atnamekey_prefixsk_live_ or sk_test_, matching livemode.
last_fourdefault_swaps_versionThe Swaps-Version this key pins requests to when the caller sends no Swaps-Version header
(API-CANON §3, RESOURCE-MODEL §0.6). A request that does send the header overrides this default
for that call only; this field is unaffected.
scopesA key created before the screenings.* rename may still list the legacy screening.read/screening.write.
statussecretThe full sk_live_… / sk_test_… value, in cleartext. Present only in the FIRST live response to
create/roll — stored hashed thereafter, never returned again and never re-derivable. A replayed
Idempotency-Key against the same request gets the same body back with this field redacted to null,
never the cleartext a second time (AuthenticatedRoute.secretFields, the same mechanism K9
introduced for webhook_endpoints.create/.rotate_secret).
updated_atallowed_ipsexpires_atlast_used_atrevoked_atRevoke a key permanently — available
Status: Available · Callers: dashboard session · Scope:
developers.write· Test mode: full
Sets status: revoked and revoked_at; every request against the key then fails authentication_error. Irreversible — issue a new key instead of expecting an un-revoke. Callers: dashboard session only. Test mode: full (K11-4) — revokes a key already owned by the caller's own account (live or twin).
path Parameters
id^key_ · 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.
Revoke a key permanently — available › Responses
The revoked key.
id^key_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectlivemodecreated_atnamekey_prefixsk_live_ or sk_test_, matching livemode.
last_fourdefault_swaps_versionThe Swaps-Version this key pins requests to when the caller sends no Swaps-Version header
(API-CANON §3, RESOURCE-MODEL §0.6). A request that does send the header overrides this default
for that call only; this field is unaffected.
scopesA key created before the screenings.* rename may still list the legacy screening.read/screening.write.
statusupdated_atallowed_ipsexpires_atlast_used_atrevoked_atRead one key's today-only usage — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.read· Test mode: full
Today-only counters — no trend history exists yet; use /v1/request_logs for the per-call detail behind these counts. Callers: dashboard session or business key (read-only). Test mode: full (K11-4) — the caller's own account's usage (live or twin), never the other's.
path Parameters
id^key_ · requiredHeaders
Swaps-VersionPins behaviour within /v1 to a dated version; defaults to the key's version; echoed on every response.
Swaps-AccountA1-2. Selects which of the caller's account memberships this request acts on, by Account.id (acct_…), when the credential can reach more than one; the caller's default account otherwise (D-109, supabase/functions/api-v1/auth.ts). Declared on every operation a dashboard session can call (#3561: the runtime caller class, not the formal security list, which names only businessKey on operations both credential classes reach). A value the session is not a member of answers 404 not_found. A business key already resolves to exactly one account (its own) and ignores this header.
Read one key's today-only usage — available › Responses
Today's usage.
requests_todayok_todayerrors_todaylast_request_atList raw per-call telemetry for own keys — available
Status: Available · Callers: business key, agent (business key), dashboard session · Scope:
developers.read· Test mode: full
Raw per-call HTTP telemetry across the caller's own keys — request id, operation, status code, latency and error code — distinct from the product-lifecycle /v1/events catalogue. Callers: dashboard session or business key (read-only), scoped to the caller's own keys. Test mode: full (K11-4) — the usage log of the caller's own account's keys (live or twin), never the other's; this is the outer leg's own log, kept in prod by design (§2.3), not the per-operation /v1/events catalogue a test caller's dev-side calls emit into.
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.
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 raw per-call telemetry for own keys — available › Responses
A page of request logs.
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.