Customers
Verification state — status only, never documents.
Jump to an operation:
List this business's customers — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
customers.read· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
A business key lists only its own customers; an agent never lists across holders. L3-1: today's model allows at most ONE customer per account owner (docs/api/RESOURCE-MODEL.md D-3), so this is always a one-element or empty list, never paginated in practice — has_more is always false. Empty (never 404) before customers.create has ever been called for this account. Fix-pack (independent review, P2): a customer record that exists but whose mapping to our verification partner is stale answers the SAME 409 customer_mapping_stale GET /v1/customers/{id} answers for it — never a retry-forever 503. Dark-flag — gated behind api_v1.customers (K6), the same flag customers.get/.create already use — no new flag.
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.
verificationWhere the account's verification facts come from. live (the default) reads the verification provider on this request. snapshot answers from the copy Swaps stored the last time it received the customer's whole state from the provider — a live read or a provider webhook — while that copy is no more than 24 hours old, and reads the provider live otherwise. Either way the answer names when its facts were checked (verification_checked_at): show it. With snapshot, a live read that cannot reach the provider answers 503 temporarily_unavailable, never the older stored status. Use snapshot only to display status; every money operation (orders, payouts, payroll, wallet bank accounts and withdrawals, payment-link settlement) checks the provider live itself, whatever this read said.
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 this business's customers — dark-flag › Responses
A page of customers.
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.
Start verification for a new customer — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
customers.write· Test mode: unavailable · Money boundary: per-transaction human confirmation requiredThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
Feeds the account-type choice (onb-welcome, R17 §1) into a new not_started customer. Never a public token — a customer is created only by the business key that owns it or by the agent acting for one holder, and it is ALWAYS that caller's own account holder (RESOURCE-MODEL D-3) — never a customer-of-a-customer. The request body never carries a name, email, date of birth, address, document or tax id, with ONE exception below; the server resolves the caller's account owner internally and reads its own email, and — for customer_type: individual — its own name, when a real first+last pair is already on file (never a merchant/brand name, or the email local part). Round-2 architect ruling (2026-09-21): when no trustworthy name is on file, the SAME hosted verification flow starts WITHOUT one — our verification partner's hosted KYC reads the legal name off the identity document itself, never off this request. This removes the round-1 409 customer_name_required refusal, which had no /v1 remedy and permanently blocked every API-only account (fix-pack, independent review, P1 — closed, not narrowed). For customer_type: business, the body may instead carry an OPTIONAL legal_name (1-200 chars, trimmed) — a registered company name, not personal data — which becomes that entity's own legal name on file; omitted, the business customer is created without one. legal_name on an individual request answers 400 invalid_request (param: legal_name) — it is a business-only field. If the account already has a customer, 409 customer_already_exists — our verification partner bills per terminal KYC status, so this never creates a second one; a concurrent or retried call that already claimed the account's onboarding slot for a DIFFERENT customer_type answers 409 customer_type_conflict instead (A4-FIX-L3). Owner only — a bearer session must be the account's owner, never an admin (architect ruling, 2026-09-21): this always acts as the account's oldest owner regardless of which member calls it (an admin-minted business key still reaches this route — key minting is the gate there, POST /v1/api_keys, owner or admin). country, when given, is validated against the same market registry POST /v1/accounts validates it against (400 invalid_request for an unrecognised code) and never overwrites a country the account already declared. A test-mode key (livemode: false) answers 503 temporarily_unavailable here — our verification partner's own sandbox toggle is process-wide, not per-caller, so a test-mode call would otherwise reach the SAME live, billed account (fix-pack, independent review, P2; matches orders.create/quotes.create). The 201 response carries NO hosted link — mint one with the existing POST /v1/customers/{id}/verification_links (kind: tos first, per R17-verification.md). Dark-flag — gated behind api_v1.customers (K6), the same flag every other customers.* operation already uses — no new flag.
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 verification for a new customer — dark-flag › Request Body
customer_typeWhich kind of verification to start: a natural person, or a business. Never written to the account: Account.customer_type follows the type Swaps records for the customer, which is this one for a new customer and the customer's own type when our verification partner already holds a customer for the account's owner (the 201 body's customer_type shows it).
country^[A-Z]{2}$ISO 3166-1 alpha-2, when known — validated against the SAME market registry POST /v1/accounts validates it against (400 invalid_request, param: country, for a well-formed but unrecognised code — fix-pack, independent review, P2), then persisted ONLY when the account has no country on file yet; a country the account already declared is never silently overwritten. Does not yet select a payment rail for this customer — a future PR can map country to a rail once product confirms which one.
legal_namecustomer_type: business ONLY — the registered company name, trimmed, forwarded to our verification partner as the entity's own legal name. Not personal data (round-2 architect ruling, 2026-09-21): a business's registered name is fine to accept from the caller, unlike a natural person's legal name, which this endpoint never accepts from any request body. 400 invalid_request (param: legal_name) when customer_type is individual. Omitted, a business customer is created without a name on file — same treatment an individual with no name on file gets.
Start verification for a new customer — dark-flag › Responses
The new customer, verification_state: not_started.
id^cus_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectcustomer_typeverification_stateSwaps' own verification-state vocabulary (R17 §3): not_started (verification not yet begun), pending (submitted, awaiting review), under_review (a review is actively in progress), approved (cleared), rejected (declined — see rejection_outcome/rejection_guidance), stale_mapping (our record of this customer needs an operator to repair it before verification can continue), action_required (a requirements_due[] entry is blocking progress). Normalized to these seven values today, but the field is marked forward-compatible (x-swaps-open-enum) — treat an unrecognized member as opaque rather than a deserialization failure.
requirements_outstandingCount backing requirements_due[].
endorsementsThe payment rails this customer is cleared for (R17 §3) — Swaps' own rail codes: base (the crypto/stablecoin base rail every customer needs first), sepa (EUR via SEPA), spei (MXN via SPEI), pix (BRL via Pix), faster_payments (GBP via Faster Payments), cop (COP via bank transfer) — the array is open: a rail code outside this list is possible and must be ignored, never treated as an error.
Per-rail status — one entry per rail for which the upstream snapshot carries a per-rail status object, including rails not yet approved; it can be empty even when endorsements[] is not. Each item names its own rail in endorsement; never pair by index with endorsements[], which lists only the approved subset and can be shorter or differently ordered.
next_stepThe single next step that unblocks verification (source text for get_verification_status).
tos_statusTerms-of-service acceptance status, as our verification partner reports it today — not yet normalized onto a closed Swaps enum (unlike verification_state above); treat any non-approved value as "not yet accepted" rather than enumerating every possible string.
kyc_statusKYC/identity-verification status, as our verification partner reports it today — already folded into verification_state above for the canonical read; not yet normalized onto its own closed Swaps enum.
available_actionsstaleRestart onboarding — survives to the wire, never collapsed into another state.
degradedCached — we could not reach the provider. Survives to the wire, never collapsed.
verification_checked_atWhen the verification facts on this object were last checked with the verification provider: the moment of a live read, or, with verification=snapshot, when Swaps last received the customer's whole state from the provider (never more than 24 hours earlier). Show it next to the status ("Checked …"). Absent for a degraded read of a check whose time is unknown.
rejection_outcomeAn outcome enum converted server-side from developer_reason — never the raw provider reason (RESOURCE-MODEL §2.4). Known values include verification_unavailable and terminal; the full set is not yet published upstream.
rejection_guidanceBounded, safe-copy only — never developer_reason passthrough (V-G3).
verification_attempts_exhaustedtrue when verifying is this customer's next step (verification_state not_started, action_required or rejected) and it has used every verification attempt Swaps starts (two rejections): POST /customers/{id}/verification_links with kind: kyc mints no further link, and support is the next step — do not offer a retry. Absent otherwise; never published for an approved, pending or under_review customer. The count itself is not published.
compliance_review_stateassociated_persons_countA count, never a name — the only UBO fact on the base object (V-G4).
Read this customer's verification status — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
customers.read· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
Self only — a customer reads its own record, and an agent reads the one holder it acts for. Status only: never a document, a submitted KYC form value, a bank detail or a beneficial owner's name. Dark-flag — gated behind api_v1.customers (K6). An email log/persist leak on the live status-read's unauthorized branch (D-11, SEC-5) is fixed in the same PR that wires this read (swaps/index.ts's probe fingerprint no longer carries email). BL-40: 404 not_found means no customer has ever been created for this account. A customer that exists but whose live verification-status read failed (a stale internal mapping, or a transient read failure) answers 503 temporarily_unavailable instead — retry, never treat it as day-zero. verification_checked_at names when the status was checked with the verification provider; with verification=snapshot it comes from Swaps' stored check while that is no more than 24 hours old (no provider call), and a live read that cannot reach the provider answers 503, never the older status.
{id} is the account's own cus_… id, from customers.list or customers.create. The literal me is a deprecated alias for it (see the parameter below). Before POST /v1/customers has ever been called for this account, every id answers this exact 404 not_found; call customers.create first.
path Parameters
id^(cus_.+|me)$ · requiredThe customer id (cus_…). The literal me (the caller's own account holder) is a deprecated alias, accepted until the next Swaps-Version date: read the id from customers.list or customers.create.
query Parameters
verificationWhere the account's verification facts come from. live (the default) reads the verification provider on this request. snapshot answers from the copy Swaps stored the last time it received the customer's whole state from the provider — a live read or a provider webhook — while that copy is no more than 24 hours old, and reads the provider live otherwise. Either way the answer names when its facts were checked (verification_checked_at): show it. With snapshot, a live read that cannot reach the provider answers 503 temporarily_unavailable, never the older stored status. Use snapshot only to display status; every money operation (orders, payouts, payroll, wallet bank accounts and withdrawals, payment-link settlement) checks the provider live itself, whatever this read said.
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 this customer's verification status — dark-flag › Responses
The customer's current verification status.
id^cus_ · requiredPrefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
objectcustomer_typeverification_stateSwaps' own verification-state vocabulary (R17 §3): not_started (verification not yet begun), pending (submitted, awaiting review), under_review (a review is actively in progress), approved (cleared), rejected (declined — see rejection_outcome/rejection_guidance), stale_mapping (our record of this customer needs an operator to repair it before verification can continue), action_required (a requirements_due[] entry is blocking progress). Normalized to these seven values today, but the field is marked forward-compatible (x-swaps-open-enum) — treat an unrecognized member as opaque rather than a deserialization failure.
requirements_outstandingCount backing requirements_due[].
endorsementsThe payment rails this customer is cleared for (R17 §3) — Swaps' own rail codes: base (the crypto/stablecoin base rail every customer needs first), sepa (EUR via SEPA), spei (MXN via SPEI), pix (BRL via Pix), faster_payments (GBP via Faster Payments), cop (COP via bank transfer) — the array is open: a rail code outside this list is possible and must be ignored, never treated as an error.
Per-rail status — one entry per rail for which the upstream snapshot carries a per-rail status object, including rails not yet approved; it can be empty even when endorsements[] is not. Each item names its own rail in endorsement; never pair by index with endorsements[], which lists only the approved subset and can be shorter or differently ordered.
next_stepThe single next step that unblocks verification (source text for get_verification_status).
tos_statusTerms-of-service acceptance status, as our verification partner reports it today — not yet normalized onto a closed Swaps enum (unlike verification_state above); treat any non-approved value as "not yet accepted" rather than enumerating every possible string.
kyc_statusKYC/identity-verification status, as our verification partner reports it today — already folded into verification_state above for the canonical read; not yet normalized onto its own closed Swaps enum.
available_actionsstaleRestart onboarding — survives to the wire, never collapsed into another state.
degradedCached — we could not reach the provider. Survives to the wire, never collapsed.
verification_checked_atWhen the verification facts on this object were last checked with the verification provider: the moment of a live read, or, with verification=snapshot, when Swaps last received the customer's whole state from the provider (never more than 24 hours earlier). Show it next to the status ("Checked …"). Absent for a degraded read of a check whose time is unknown.
rejection_outcomeAn outcome enum converted server-side from developer_reason — never the raw provider reason (RESOURCE-MODEL §2.4). Known values include verification_unavailable and terminal; the full set is not yet published upstream.
rejection_guidanceBounded, safe-copy only — never developer_reason passthrough (V-G3).
verification_attempts_exhaustedtrue when verifying is this customer's next step (verification_state not_started, action_required or rejected) and it has used every verification attempt Swaps starts (two rejections): POST /customers/{id}/verification_links with kind: kyc mints no further link, and support is the next step — do not offer a retry. Absent otherwise; never published for an approved, pending or under_review customer. The count itself is not published.
compliance_review_stateassociated_persons_countA count, never a name — the only UBO fact on the base object (V-G4).
Mint a hosted verification link — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
customers.write· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
Accepts all five kind values, but only tos and kyc mint a real link today, through our verification partner's own hosted flow (K6). business_questionnaire, business_ubo and remediation have no backing implementation anywhere in this codebase yet — the partner's endorsement-scoped link shape must be confirmed to distinguish them before they ship for real, so each answers 409 capability_unavailable rather than a fabricated link. return_to is validated against an origin allowlist and is the hand-off's return target (the partner sends the holder back to it). rail (optional, kind: kyc only) asks the partner for that payout rail's approval when an individual customer does not carry it yet and opens the link on its requirements; see the field. Owner only — a bearer session must be the account's owner, never an admin (architect ruling, 2026-09-21): this always acts as the account's oldest owner regardless of which member calls it (an admin-minted business key still reaches this route — key minting is the gate there, POST /v1/api_keys, owner or admin). Dark-flag — gated behind api_v1.customers (K6).
path Parameters
id^(cus_.+|me)$ · requiredThe customer id (cus_…). The literal me (the caller's own account holder) is a deprecated alias, accepted until the next Swaps-Version date: read the id from customers.list or customers.create.
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.
Mint a hosted verification link — dark-flag › Request Body
kindOne polymorphic mint across every hosted verification hand-off (D-52, V-G17): tos (terms of service), kyc (identity/business verification), business_questionnaire (compliance questionnaire), business_ubo (beneficial-owner check), remediation (a requested fix to a prior submission).
return_toRequired, and validated against an origin allowlist (https://swaps.app, https://www.swaps.app, https://staging.swaps.app; anything else is 400 invalid_request, param: return_to). It is the hosted hand-off's return target: the verification partner sends the holder back to this URL when the terms or verification page is done (widget v2 D-35, closing the dead end where every hand-off landed on /confirm instead of the hub — D-52, V-G18, prod P-B3). Swaps appends nothing to it; keep personal data out of it. A kyc link that cannot be minted with this return target fails rather than returning a link that would send the holder elsewhere.
railOptional, and only with kind: kyc (with any other kind it is 400 invalid_request, param: rail): the rail the holder is verifying for — the payout rail of a sell, or the pay-in rail of a buy. For an individual customer, sepa, faster_payments, pix, spei, bre_b and co_bank_transfer ask our verification partner for that rail's approval (its endorsement — sepa, faster_payments, pix, spei, and cop for both Colombian rails) when the customer does not carry it yet, and the link then opens on that approval's own requirements. ach and wire ride the approval every customer is created with, so the link is the same as without rail. A business customer's link is never scoped by rail. Absent: the link is minted exactly as before this field existed. Read Customer.endorsement_statuses for the outcome — a link is not an approval. When the approval cannot be requested right now the answer is 503 temporarily_unavailable; retry with a new Idempotency-Key.
Once an individual's verification is approved (before that the link starts identity verification and asks for the rail as before), and before anything is asked of our verification partner, their live approval for a buy on that rail is read (pix reads pix_onramp; spei, faster_payments, sepa their own; both Colombian rails cop). When a hosted verification link cannot move it, nothing is requested and no link is minted: 403 permission_error, code: endorsement_required, details: {payment_rail, endorsement, next}, where next is accept_terms (mint a kind: tos link first), complete_profile (a questionnaire the hosted flow does not collect), provider_review (the partner is holding it; nothing to do) or not_available (the partner does not offer the rail to this customer). For pix the payout side counts too: the link is minted when either direction (pix_onramp, or pix_offramp / pix) has a step the hosted flow takes or is approved, and otherwise names the most actionable step of either. A status that could only be read from a cached copy does not decide that step: the link is requested as before. The customer's country of residence counts too, on our verification partner's live read, whatever the verification state (purpose, below).
purposeOptional, and only beside a rail (otherwise 400 invalid_request, param: purpose): what the rail-scoped link is for — buy when the holder pays in on the rail, sell when they are paid out on it. The customer's country of residence as our verification partner holds it is read before anything is requested, whatever the verification state, and a rail the partner does not open to residents of that country is refused — 403 permission_error, code: endorsement_required, next: not_available, no link: bre_b / co_bank_transfer for residents of the EU/EEA whatever purpose says (the partner does not offer COP to EU customers in either direction), and with buy, pix outside Brazil. An approval the customer already holds is never refused on residence; spei, faster_payments and sepa never are. A residence the partner does not hold decides nothing until the individual's verification is approved; after that, a buy link on pix or any link on bre_b / co_bank_transfer with no residence on file is refused with next: provider_review. With buy, the pix payout side (pix_offramp / pix) does not count either: only the buy approval (pix_onramp) decides. A pix link with sell or without this field is not refused on residence (a payout on pix is open to residents of other countries). New values may be added.
Mint a hosted verification link — dark-flag › Responses
A one-time hosted link.
urlexpires_atList this business customer's beneficial owners, name-free — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
customers.read· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
{label} plus an OPTIONAL ownership_percent, never a name — for every caller kind, in this K6 pass (stricter than D-49's own default, which would allow name for the holder's own delegated session; that widening is not implemented here). Our verification partner's raw per-person field names are not independently confirmed anywhere in this codebase (only a count is read today) — ownership is read defensively, never guessed at, and is OMITTED (never defaulted to 0) when the raw payload carries no recognized ownership field, so a client must render the absence honestly rather than as an observed 0%. status is omitted (optional on the schema) rather than a fabricated placeholder — no confirmed per-person status field exists anywhere in this codebase yet; a follow-up derives it from the same endorsement missing[] object-scoped bundles the Verification Hub already reads, once a live upstream response confirms the shape. Dark-flag — gated behind api_v1.customers (K6).
path Parameters
id^(cus_.+|me)$ · requiredThe customer id (cus_…). The literal me (the caller's own account holder) is a deprecated alias, accepted until the next Swaps-Version date: read the id from customers.list or customers.create.
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 this business customer's beneficial owners, name-free — dark-flag › Responses
This customer's associated persons — name-free unless the caller is the holder's own delegated session.
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.
Submit structured compliance details (V4 — Questionnaires) — dark-flag
Status: Dark-flag — live behind a cohort or flag · Callers: business key, agent (business key), dashboard session · Scope:
customers.write· Test mode: unavailableThis operation is dark-flag — live behind a cohort or flag. While its resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable; with the switch on, a closed product or launch flag, or a cohort the account is outside, answers409 capability_unavailable. Check its product page before relying on it.
Write-only structured update for our verification partner's own compliance-questionnaire fields — this route never re-implements the enum whitelist or the underlying call, it only resolves the caller's own customer id server-side (never accepted from the request body — R17 PII rule) and forwards the body verbatim. Every field is optional; only the fields present are sent — the server definedOnly-merges, exactly as the underlying action already does. Closes V-G5 (R17-verification.md), the same kind of gap K6 already closed for tos/kyc via verification_links. Responds 202 with {accepted: true} only — per V-G5/V-G15, a submitted value is NEVER echoed back on any read (there is no GET counterpart by design; GET /customers/{id} still carries only status). Test mode: full — this route does not read caller.livemode; a sk_test_ caller's answers are written to the SAME live account a sk_live_ caller would reach (the underlying action has no test/live branch today, matching its sibling customers.verification_links.create). Dark-flag — gated behind api_v1.customers (K6).
path Parameters
id^(cus_.+|me)$ · requiredThe customer id (cus_…). The literal me (the caller's own account holder) is a deprecated alias, accepted until the next Swaps-Version date: read the id from customers.list or customers.create.
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.
Submit structured compliance details (V4 — Questionnaires) — dark-flag › Request Body
account_purposeThe union of the individual (11) and business (13) account_purpose values (16 unique — some overlap). Which of these are valid is further restricted by the customer's own customer_type; a mismatched-but-listed value still gets 400.
account_purpose_otherFree text — only meaningful (and only read) when account_purpose is other. 400 past 1024 characters (never silently truncated) — see this schema's own description.
source_of_fundsThe union of the individual (12) and business (11) source_of_funds enum values (22 unique — one overlap, pension_retirement).
source_of_funds_descriptionFree text. 400 past 1024 characters (never silently truncated) — see this schema's own description.
employment_statusIndividual only — current employment status, Swaps' own six-value vocabulary.
expected_monthly_payments_usdExpected monthly payment volume through Swaps, banded in USD.
business_descriptionFree text — the business's own description of what it does. 400 past 1024 characters (never silently truncated) — see this schema's own description.
business_typeBusiness only — legal structure, Swaps' own seven-value vocabulary.
business_industry^[0-9]{6}$A single 2022 NAICS six-digit code identifying the business's industry — a public US federal classification standard, not a Swaps- or partner-specific list. Too large to enumerate here, so only the ^[0-9]{6}$ shape is checked at this layer; the closed set of valid codes is enforced server-side, never free text.
estimated_annual_revenue_usdBusiness only — estimated annual revenue, banded in USD.
high_risk_activitiesZero or more of 15 regulated-activity values (includes none_of_the_above).
Submit structured compliance details (V4 — Questionnaires) — dark-flag › Responses
Intake confirmed — the fields were forwarded to our verification partner. Not a re-read of the stored profile.
acceptedList one customer's verification timeline — planned
Status: Planned — not built yet · Callers: business key, agent (business key), dashboard session · Scope:
customers.read· Test mode: sandboxThis 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.
customer.verification_state_changed, .rejected, .requirements_updated (RESOURCE-MODEL §3, D-55) — gives a dated ladder for the review-in-progress screens without re-deriving readiness client-side. Proposed.
path Parameters
id^(cus_.+|me)$ · requiredThe customer id (cus_…). The literal me (the caller's own account holder) is a deprecated alias, accepted until the next Swaps-Version date: read the id from customers.list or customers.create.
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 one customer's verification timeline — planned › Responses
This customer's event timeline, oldest first.
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.