Conventions
The rules every /v1 resource follows, stated once (RESOURCE-MODEL §0) and never re-argued per resource.
Money
A money value is always an object, never a bare number:
Code
amount is a minor-unit integer as a string; decimals states the scale so a client never re-derives it (USDC is decimals: 6). A float never appears on a money path. Basis points, percentages and block counts stay plain integers and are not Money objects.
One stated exception: POST /v1/quotes's from_amount/to_amount are bare decimal strings in major units ("1250.50", matching bestQuoteRequestSchema), not Money objects — deliberately. A quote request is pricing an amount, not yet stating a money fact with a settled scale; every OTHER creating request (payouts.create, payment_links.create, …) uses Money. If you learned the shape from one of those, do not send a Money object to quotes.create — it is a plain decimal string and a Money object there is 400.
Idempotency-Key
Every mutating request carries an Idempotency-Key header:
- Caller-generated, 16–128 characters of
[A-Za-z0-9_-](a UUID qualifies); the shape is enforced — anything else is400 invalid_request/idempotency_key_invalid. - A replay of the same key within 24 hours returns the stored response, never a second money movement.
- A different request body under the same key is
idempotency_error(409) — never a silent merge. - The store is keyed
(key, account, route), so the same key on two different endpoints is not a collision.
Swaps-Version
Behaviour within /v1 is pinned by a dated header, Swaps-Version: 2026-09-04:
- A key carries a default version; a request header overrides it for that call.
- Every response echoes the version it was served under — literally: a date older than the oldest version this deployment still serves is never echoed verbatim, it comes back as that oldest version, because that is the behaviour you actually got.
- A new date is cut only for a genuine behaviour change — additive fields are never a version bump, and a client must ignore fields it doesn't recognise.
- A breaking change gets a new resource path or tool name instead; a published path or tool signature is never repurposed, and old and new overlap for at least six months.
Swaps-Account
A credential that belongs to more than one account (a dashboard session shared across a team) selects which one a request acts on with an optional header, Swaps-Account: acct_…; omitted, it acts on the caller's default account. A business key is unaffected — a key already belongs to exactly one account and cannot be redirected by this header — and the parameter is published only on an operation a dashboard session can call: the server does not read it on the API-key path, so declaring it everywhere would offer a scoping control a business key silently ignores (A1-2 fixer round 1).
Forward compatibility — enum members
Two rules, and they apply to different fields:
- A response field's enum may gain a new member within
/v1, without a version bump, when the API-CANON marks it growth-prone (event types, lifecycle statuses, a partner's own vocabulary). Treat an unrecognized member as an opaque string — do not throw, do not treat it asnull. The generated TypeScript SDK types these fields as"known" | "values" | (string & {})for exactly this reason: the literal union still gives you autocomplete, and thestringhalf keeps a new member from failing to compile or to parse. - A request field's enum never grows silently: sending a value this deployment does not recognise is
400 invalid_request, always — including onwebhook_endpoints.create/.update'sevent_types, which draws from the very same catalogueEvent.typepublishes as open. Subscribing to a name this deployment cannot yet deliver cannot be honoured, so it is refused rather than silently accepted and never fired.
Pagination
List endpoints take limit (default 25, max 100) and an opaque cursor, and return:
Code
next_cursor is always present — null when there is no next page, a cursor string when there is; never omitted. limit outside 1..100 is never rejected: 0, a negative number or anything non-numeric falls back to the default (25); anything above 100 is capped to 100.
The cursor encodes (created_at, id) or the resource's own monotonic identity column. A cursor list never returns a total count — build a "how many" UI from a resource's own /summary endpoint where one exists (for example /v1/activity/summary), not by paging to the end. Capability and eligibility reads are unpaginated — they're small, bounded projections by design.
Errors
One envelope for every /v1 response and every MCP tool failure:
Code
type is the class an agent needs to pick a recovery; code is a stable, additive-only identifier — a code, once shipped, is never renamed or removed. doc_url resolves to the full code list (A1-1), anchored per code — every operation's 4xx responses on the reference also carry a 422/409 where its handler can actually return one, and every available/dark-flag operation declares 500.
Two optional fields carry the specifics: param names the offending request field where one can be named, and details is machine-readable context for the codes that have some — the values you would otherwise parse out of message. Read details by code, never generically: each code documents its own keys, and a code with nothing to add omits the object. allowed_rails_invalid_for_currency carries currency, allowed_rails and offerable_rails.
The type table
type | HTTP | Agent recovery |
|---|---|---|
invalid_request | 400 / 422 | Fix the argument and retry. |
authentication_error | 401 | Re-authorize; do not retry as-is. |
permission_error | 403 | Wrong actor or scope — call as the right one. |
not_found | 404 | The id is not visible to this caller — also the cross-account answer. |
conflict | 409 | State moved — re-read, then decide. |
idempotency_error | 409 | Same key, different body. |
rate_limit_error | 429 | Back off; Retry-After is set. |
capability_unavailable | 409 | Do not retry — this corridor or product cannot serve it. |
temporarily_unavailable | 503 | A kill switch is thrown, or a dependency is out; retry after Retry-After. |
quota_exhausted | 402 | An allowance is spent — backing off does not restore it; upgrade or buy credits. |
provider_error | 502 / 503 | Upstream failed; retry only where stated. |
api_error | 500 | Ours — open a support ticket with the request id. |
capability_unavailable vs temporarily_unavailable
This split is load-bearing. capability_unavailable means this corridor or product cannot serve the request — waiting does not change the answer. temporarily_unavailable means a kill switch is thrown or a dependency is degraded — it can be un-thrown without a deploy, so the honest recovery is "retry later". Never conflate them, and never return a bare 403, a 500 or an empty success where one of these two is the truth.
A cross-account id is always not_found, never permission_error — the alternative would tell an attacker that an id they can't reach nonetheless exists.
One code carries one meaning, but the same meaning can be reachable two ways, and then the type differs while the code does not. allowed_rails_invalid_for_currency is the worked example: POST /v1/payment_links and PATCH /v1/payment_links/{id} answer 400 invalid_request when the rails you are writing would leave the link with nothing a payer could pick — the offending value is in your request, so fix it and retry. POST /v1/payment_links/{id}/activate answers 409 conflict for the same defect — there the offending value is the stored draft, so the recovery is to patch the draft (allowed_rails: null clears the restriction) and activate again. Match on code for what went wrong, on type for what to do about it.
livemode
Every object carries livemode: boolean. For resources with a dedicated livemode column it is set once, at creation, from the calling key's mode (sk_live_…/sk_test_…) and never toggled after the fact; resources with no column yet currently hardcode true on every read regardless of caller — see Test mode for which is which.
Naming
Every operation id is <resource>.<verb>, and it is the method name in the SDK, so these names do not change within /v1:
- The last part is a verb:
payouts.fund,payouts.beneficiary.set,wallet.transactions.list_recent. - Something under a parent is part of the resource name:
payouts.events.list,payroll_runs.items.get,screenings.reports.create,payouts.receipt.get. - Writing a single sub-resource under a parent is
.set(or.updatefor aPATCH):payouts.beneficiary.set,customers.compliance_profile.update. - Five read-only views that the server computes from their parent keep the form
get_<view>:get_readiness,get_funding_instructions,get_setup_guide,get_usage,get_summary.
Scopes are <resource>.<read|write>. Screenings, traces and transaction lookups use screenings.read / screenings.write. A key created with the old screening.* names keeps working until the next Swaps-Version date. Quotes are the one exception: they share orders.read / orders.write, because a quote exists only to become an order.
Your own account is /account. It is a real singleton, and a session that belongs to several accounts picks one with Swaps-Account. Every other resource is addressed by its id. On /customers/{id}, the literal me is a deprecated alias for your customer id. It works until the next Swaps-Version date. Use the cus_… id returned by GET /v1/customers or POST /v1/customers instead.
UK Faster Payments is always faster_payments, never fps.
Status vocabulary
Every operation in the reference is tagged with exactly one of three values (x-swaps-status) — never a fourth:
- available — a live backend exists and
/v1is a thin router in front of it. - dark-flag — live, but gated behind a cohort or feature flag. While the operation's resource switch (
api_v1.<resource>) is off, calling it answers503 temporarily_unavailable— an availability lever, never an authorization one. With that switch on, a closed product or launch flag — for examplepayment_links_crypto_onlywhen creating or resuming a subscription, or Receive by bank not launched when creating a virtual account — or a cohort the account is outside answers409 capability_unavailablefrom the operation itself. - proposed — does not exist yet. Documented for the contract it will carry, never presented as callable, never hidden.
Next: the full error code list · Errors and events · the error type table lives inline in the reference on every operation too.