{
  "openapi": "3.1.1",
  "info": {
    "title": "Kwiikpay API",
    "description": "This is the primary API surface for operating money — fiat accounts and withdrawals, crypto\nwallets and withdrawals, currency exchange (conversions), payout batches, and fees.\n\nIt acts for a parent or direct customer that **already exists** — that record is created before\nan API key is issued (through the KwiikPay dashboard, or by your account manager), and this\ndocument does not list a direct-customer creation call. Eligible parents can create and\noperate their own direct **sub-customers**, called `end-customers` in the routes. See the\n**Sub-customers** section and [parent integration guide](/guides/sub-customers) for\nonboarding eligibility, parent/child IDs, beneficiaries and fiat withdrawals.\n\nYou can also start and follow your own\nverification: `POST /customers/{customerId}/onboarding/kyc/initiate` (or `/kyb/initiate` for a\nbusiness) opens a Sumsub verification session and returns its hosted link and Web SDK token;\nthe verification itself — document capture, checks and the decision — runs in Sumsub, not in\nthis API. `GET /customers/{customerId}/onboarding/status` then reports where the customer has\ngot to and whether they are held. See the\n[Customers and onboarding guide](/guides/customers-and-onboarding) for the whole sequence.\n\nWebhook subscription management lives on the narrower self-service surface (see the\n[v2 API](/scalar/webhooks) document), which also offers read-only account visibility. The same\n`X-Api-Key` authenticates both, so treat them as one surface with two path prefixes rather than\ntwo generations.\n\n## Core concepts\n\n- **Tenant** — your integration's account with Kwiikpay. Every request is scoped to exactly one\n  tenant, resolved from your API key; you never pass a tenant id yourself.\n- **Customer** — a person or business you have onboarded. `user_type` is `Personal` or\n  `Business`; a business customer is verified via KYB instead of KYC. Every other resource\n  (fiat account, wallet, withdrawal, conversion, transaction) belongs to exactly one customer.\n- **Onboarding (KYC/KYB)** — identity/business verification, run through Sumsub. A customer must\n  reach `Active` before most money-movement endpoints will accept requests for them (fiat\n  account creation, withdrawals, wallet provisioning). You START it here\n  (`POST .../onboarding/kyc/initiate` or `.../kyb/initiate` returns the Sumsub session your\n  customer completes), and you READ it here —\n  `GET /customers/{customerId}/onboarding/status` reports the current state, and a refusal from\n  a money endpoint tells you the customer is not through it yet. Reading a customer by id\n  also refreshes their verification state from Sumsub; the list route does not.\n- **Fiat account** — a virtual bank account (vIBAN, or sort-code/account-number for GBP) issued\n  in the customer's name by a banking provider (Fiat Republic or OpenPayd, chosen by your\n  tenant's routing rules). OpenPayd covers GBP and EUR only, so a USD account is always Fiat\n  Republic. Money sent to its bank details lands as a `deposit.received` webhook and\n  credits the customer's balance; there is nothing to poll.\n- **Crypto wallet** — a per-asset deposit address (e.g. one address per customer per USDC),\n  custodied through Utila. Same deposit model as fiat: funds arriving on-chain trigger\n  `deposit.received` and credit the balance automatically.\n- **Balance and buckets** — a customer's balance for a currency/asset is split into buckets\n  (`Available`, `Reserved`, `Pending`, `ProviderPending`, `Settled`). Only `Available` funds can\n  be withdrawn or exchanged; the others represent money that has arrived but is still clearing,\n  or is currently locked by an open withdrawal/exchange order.\n- **Exchange (conversion)** — converting one currency/asset to another (fiat-to-fiat,\n  fiat-to-crypto, crypto-to-fiat) for a customer. `/transfer/price` returns an indicative\n  rate and expiry so you can show the customer what to expect; creating the conversion\n  request (`from_currency`/`to_currency`/`amount_minor`) executes against the live rate at\n  that moment — it does not reference or lock in the earlier price quote.\n- **Withdrawal / beneficiary** — moving `Available` balance out to an external destination.\n  Fiat withdrawals pay a registered beneficiary (a saved external bank account); crypto\n  withdrawals pay a customer-whitelisted external address (created and approved in the\n  Kwiikpay dashboard — a raw address supplied per-request is refused in the default\n  configuration). Payout batches let you submit many\n  fiat withdrawals for one customer in a single call.\n\n## Authentication\n\nEvery route requires your tenant's API key. Send it as `X-Api-Key: <key>`, or as\n`Authorization: Bearer <key>` — not both. Keys look like `kp_live_<16 hex>.<secret>`; the part\nbefore the dot is a public prefix you can log. Keys are issued and rotated from the customer\ndashboard (Settings → API) or by an admin, and carry a scope set. Every operation below states\nthe scope it requires under `security` and in its description; the full route-to-scope matrix\nand the scope semantics (all listed scopes are required; `transactions:write` does NOT cover\n`payout-batches:write`) are in the [Authentication guide](/guides/authentication). A key is\nbound either to the whole tenant or to a single subject (customer/business) — a subject-bound\nkey can see and act on its own subject and its direct sub-customers; requesting an unrelated customer's resource\nreturns `404`, not `403`, so a subject-bound key cannot use response codes to enumerate other\ncustomers. Never send `X-Tenant-Id`: it is refused with `400`. Authentication, scope and\nrate-limit failures are `application/problem+json` (see the `ProblemDetails` schema), not the\nenvelope.\n\n## Rate limits\n\nEach route enforces a per-key budget counted in a fixed window per key, method and route\ntemplate (`Retry-After` header on `429`); a tenant-wide aggregate ceiling also applies across\nevery API-key call on both surfaces AND your dashboard traffic combined. Repeated failed\nauthentications from one IP are throttled separately and also answer `429`. Current numbers:\n[Authentication guide](/guides/authentication).\n\n## Idempotency\n\nEvery `POST`/`PUT`/`PATCH`/`DELETE` on this surface REQUIRES an `Idempotency-Key` header\n(8–256 visible ASCII characters); a request without one is refused with `400` before the\noperation runs. Retrying the identical request with the same key within 24 hours returns the\nstored result of a successful first attempt rather than executing again; reusing the key with a\ndifferent method, path or body is rejected with `409`. Two limits to know: replay protection\napplies only to JSON requests with a `Content-Length` of at most 1 MiB, and a key whose first\nattempt FAILED (any non-2xx) is not locked — a retry re-executes. Full contract:\n[Idempotency guide](/guides/idempotency).\n\n## Typical integration flow\n\n1. **Find out who you are** — `GET /api/v1/partner/me` returns the customer your key acts for,\n   its binding, and the scopes it carries. Every other call is scoped to a `customerId`, and\n   this is how you obtain yours; it needs no scope beyond a valid key. A tenant-bound key acts\n   for many customers and gets `null` here, so it must supply an id explicitly.\n2. **Check they are ready** — `GET /customers/{customerId}/onboarding/status`. Money-movement\n   endpoints refuse a customer who has not reached an approved status, so read this first\n   rather than discovering it from a withdrawal.\n3. **Issue a fiat account and/or crypto wallet** — `POST .../banking/accounts` and/or\n   `POST .../crypto/wallets/{asset}/address`. Give the customer the returned bank\n   details/address to fund from.\n4. **React to `deposit.received`** — subscribe a webhook endpoint before customers start\n   depositing; it fires when a fiat deposit completes or a crypto deposit is confirmed. What\n   arrived is also readable at any time from `GET /customers/{customerId}/banking/deposits`\n   and `.../crypto/deposits` (scope `deposits:read`), including deposits held for review,\n   which fire no event.\n5. **Move or convert funds** — optionally quote with `/transfer/price` to show the customer an\n   estimate, then create a conversion request (executes at the live rate); or register a\n   beneficiary and create a fiat/crypto withdrawal — send an `Idempotency-Key` on all of these.\n6. **Reconcile** — after downtime, page the deposits, withdrawals and conversion-request\n   lists per customer (`page` + `limit`, real `pagination.total`), and if you mirror the\n   ledger, `GET /customers/{customerId}/banking/accounts/{accountId}/transactions` replays\n   one fiat account's journals. It is per ACCOUNT and fiat-only: there is no customer-level\n   or unified feed, and no crypto ledger feed — crypto has its deposits and withdrawals lists\n   only. The [reconciliation guide](/guides/reconciliation) states exactly what each read\n   carries and what only webhooks carry.\n\n## Two refusals worth knowing before you build\n\n**Blocked jurisdictions.** Opening a fiat account returns **403** with reason code\n`fiat_unsupported_jurisdiction` when the customer's country is one of: AF, AQ, BV, BY, CD, CF,\nCN, CU, ET, GS, HM, HT, IQ, IR, KP, LB, LY, ML, MM, NI, NR, RU, SD, SO, SS, SY, TF, UA, UM,\nVE, YE, ZW. This runs ahead of routing, so it applies to every provider and no configuration\nchanges it. A currency that is not offered for the customer's type returns 403\n`currency_not_available_for_customer_type`.\n\n**A payout batch can fail while returning 200.** If every item is invalid the response is\nstill `200` with the message \"Payout batch created successfully\" and\n`payout_batch.status = \"Rejected\"` — it is not a 4xx. Individual bad items are recorded as\n`Skipped` rather than failing the request, so you must inspect `status` on the batch and\n`status`, `failure_code` and `failure_reason` on each item. The only hard 422s on that\nendpoint are `currency_required`, `items_required`, `too_many_items` (over 500),\n`unsupported_rail` and `subject_not_active`. And a created batch pays nobody by itself: it\nwaits in `Draft` until a Kwiikpay operator approves and executes it, and no webhook fires\nwhen that happens — see the Payout batches guide.\n\n## Outbound webhooks\n\nKwiikpay delivers asynchronous event notifications to HTTPS endpoints you register. Registration\nis not on this document: it lives on the self-service surface, whose bodies are camelCase\n(`eventTypes`) where this document is snake_case — see the [webhooks guide](/guides/webhooks)\nand the [v2 API document](/scalar/webhooks). The routes are:\n\n| Route | Scope | Purpose |\n|---|---|---|\n| `POST /api/v2/public/webhook-subscriptions` | `webhooks:write` | Register an endpoint; the signing secret is returned once |\n| `GET /api/v2/public/webhook-subscriptions` and `GET /api/v2/public/webhook-subscriptions/{subscriptionId}` | `webhooks:read` | List / read endpoints (no secret) |\n| `PATCH /api/v2/public/webhook-subscriptions/{subscriptionId}` | `webhooks:write` | Replace URL, event types, enabled state and attempt limit |\n| `DELETE /api/v2/public/webhook-subscriptions/{subscriptionId}` | `webhooks:write` | Disable (record and history kept) |\n| `POST /api/v2/public/webhook-subscriptions/{subscriptionId}/secret-rotations` | `webhooks:write` | Rotate the signing secret |\n| `GET /api/v2/public/webhook-subscriptions/{subscriptionId}/delivery-attempts` | `webhooks:read` | Diagnose a missed event |\n| `GET /api/v2/public/webhook-spec` | `webhooks:read` | The event-type list and X-Webhook-* signing spec |\n\nEvery delivery is an HTTP `POST` with `Content-Type: application/json` and a common envelope;\nthe per-event fields live under `data`.\n\n```json\n{\n  \"id\": \"019ed568-9f73-7cc3-aa32-f97cdbc620d1\",\n  \"type\": \"customer.created\",\n  \"occurred_at\": \"2026-06-17T11:46:49.000Z\",\n  \"api_version\": \"2026-05-legacy\",\n  \"data\": { }\n}\n```\n\nThe envelope's `id` is the id of the **resource** the event is about (the payment, account or\ncustomer id). Every event about the same resource repeats it, so never deduplicate on it. The\nevent id is in the headers.\n\n### Headers: two signature families, both on every delivery\n\nEvery delivery carries BOTH header families below, signed with the same secret. Verify\nwhichever one you prefer; the `x-kwiikpay-*` family is the stronger scheme because its signed\ninput binds the event id and the body hash. Both stay supported.\n\n| Header | Value |\n|---|---|\n| `X-Webhook-Id` and `x-kwiikpay-webhook-id` | The **event id** — identical on every retry of the same event. **Deduplicate on this.** It does NOT match the envelope `id` (the resource id). |\n| `X-Webhook-Event` and `x-kwiikpay-event-type` | The event type, e.g. `deposit.received` |\n| `X-Webhook-Delivery-Id` and `x-kwiikpay-delivery-endpoint-id` | Your endpoint's subscription id — constant on every delivery to it. Never deduplicate on it. |\n| `X-Webhook-Delivery-Attempt-Id` | Unique per attempt; differs on every retry. Not an event-level key. |\n| `X-Webhook-Timestamp` | ISO 8601 (`2026-08-09T10:15:30.0000000+00:00`) time THIS attempt was signed — every attempt is re-signed at send time |\n| `X-Webhook-Signature` | `sha256=<lowercase hex HMAC-SHA256>` over `\"{X-Webhook-Timestamp}.{raw_body}\"` |\n| `x-kwiikpay-signature-timestamp` | The same signing instant as Unix seconds |\n| `x-kwiikpay-payload-sha256` | Lowercase hex SHA-256 of the raw body |\n| `x-kwiikpay-signature` | `sha256=<lowercase hex HMAC-SHA256>` over `\"{x-kwiikpay-signature-timestamp}.{x-kwiikpay-webhook-id}.{x-kwiikpay-payload-sha256}.{raw_body}\"` |\n| `x-correlation-id` | Present when the operation that produced the event carried a correlation id |\n\nSigning uses your subscription's CURRENT secret at send time. After a rotation, every later\ndelivery — including queued retries of older events — is signed with the new secret; there is\nno overlap window.\n\n### Retries: what your response does\n\n| Your endpoint returns | Outcome |\n|---|---|\n| `2xx` | Delivered. No further attempts. |\n| `408`, `425`, `429`, any `5xx`, a timeout (20 seconds for send plus response read) or a connection failure | Retried: 30 s after attempt 1, then 1 m, 2 m, 4 m, 8 m, capped at 15 m, until the endpoint's attempt limit (default 5, configurable 1-25) — then dead-lettered. |\n| Any other `4xx` — `400`, `401`, `403`, `404`, `410`, `422`, ... | **Dead-lettered on the first attempt. Never retried.** |\n\nA dead-lettered event is not re-sent by anything you can call; recovery is a replay by Kwiikpay\nsupport. So if your endpoint cannot process right now (verifier down, mid-deploy, auth proxy\nmisconfigured), return `503` — never `401`/`403`/`400` — or you permanently lose every event in\nthat window, `deposit.received` included. Deliveries may repeat and may arrive out of order;\nmake handlers idempotent on `X-Webhook-Id`.\n\n### Which endpoint receives which event\n\nAn endpoint's reach is fixed by the API key that registered it and cannot be changed\nafterwards: a subject-bound key creates an endpoint that receives only that customer's events\n(plus those of its own end-customers); a tenant-wide key creates a tenant-wide endpoint. The\nsubscription response does not show which kind you have — you know from the key you used. An\nevent that cannot be attributed to a customer is routed to tenant-wide endpoints only, and an\nevent whose payload resolves no `customer_id`, `business_id` or `user_id` is not delivered at all\n— no delivery-attempt row is written in either case.\n\n### Fan-out: one outcome, several events\n\nSeveral outcomes emit more than one event type. Each is a separate delivery with its own event\nid, the same envelope `id` and the same `occurred_at`; ordering between them is not guaranteed.\nSubscribe to the ones you need and expect them together — they are not duplicates.\n\n| Outcome | Events emitted |\n|---|---|\n| Onboarding started | `customer.created` |\n| Verification approved | `kyc.approved` (person) or `kyb.approved` (business) |\n| Verification pending KwiikPay review | `kyc.pending_review` (person) or `kyb.pending_review` (business) — the provider review is GREEN but KwiikPay has not yet made its compliance decision; the customer cannot transact yet |\n| Verification rejected | `kyc.rejected` or `kyb.rejected` |\n| Fiat account issued | `viban.ready` + `virtual_account_request.completed` + `customer.account_ready` — only `viban.ready` carries the bank coordinates |\n| Fiat account needs more information | `virtual_account_request.needs_correction` + `customer.needs_information` |\n| Fiat account failed | `virtual_account_request.rejected` |\n| Fiat or crypto deposit completed | `deposit.received` (`data.deposit_type` says which) |\n| Fiat or crypto withdrawal settled | `withdrawal.completed` (`data.type` says which) |\n| Fiat deposit recalled to its sender by the bank | `deposit.returned` — the balance is NOT adjusted automatically (`data.balance_adjusted` is `false`, `data.status` is `under_review`; Kwiikpay operations reconcile by hand). If you hold balances for your own clients, freeze the amount on your side |\n| Fiat withdrawal bounced by the beneficiary bank | `withdrawal.returned` — already reversed and its fee refunded when this fires (`data.balance_adjusted` is `true`, `data.ledger_transaction_id` is the reversing entry) |\n| Conversion settled | `conversion.completed` |\n| Beneficiary approved or rejected by Kwiikpay staff | `beneficiary.approved` or `beneficiary.rejected` |\n| A beneficiary becomes usable, stops being usable, or is superseded | `fiat.beneficiary.updated` — fires after a staff approval decision, after a bank-registration transition to Registered or NeedsReview, and when a beneficiary is superseded by a replacement (staff correction/recovery or a self-service/partner replace) |\n\n`beneficiary.approved`, `beneficiary.rejected` and `fiat.beneficiary.updated` are subscribable\nlike any other event type. They are the ONE exception to the envelope above: they arrive in\nthe newer `kwiikpay.webhooks.v1` shape — camelCase keys `specVersion`, `eventType`, `tenantId`,\n`resourceId` (the beneficiary id), `occurredAt`, `correlationId` and `data` — not the legacy\n`id`/`type` envelope. `beneficiary.approved`/`beneficiary.rejected`'s `data` carries\n`beneficiary_id`, `subject_id`, `approval_status`, `provider`, `currency` and\n`rejection_reason` (empty when approved). `fiat.beneficiary.updated`'s `data` carries\n`fiat_beneficiary_id`, `subject_id`, `subject_type`, `status`, `currency_code`,\n`provider_code`, `provider_configuration_id`, `approval_status`, `bank_registration_status`,\n`bank_registration_reason` (customer-safe text, never operator text), `bank_registered_at`,\n`superseded_by_beneficiary_id` (the replacement's id, when this beneficiary is no longer the\nactive payee), `display_name`, `masked_account_identifier`, `payment_rail` and `change`\n(`approval` | `registration` | `superseded`). The signature headers and retry rules are\nidentical.\n\n### Value types\n\nMinor-unit fields (`*_minor`) are integers. `*_major` amounts and `executed_rate` are **JSON\nnumbers** (or `null`) — never quoted strings. Timestamps are ISO 8601 UTC. Ids are UUID strings.\n\n### Event catalogue\n\n**`customer.created`** — an onboarding case was started for a customer or business.\n\n| `data` field | Meaning |\n|---|---|\n| `customer_id` | Id of the customer or business |\n| `tenant_id` | Your tenant id |\n| `email` | Email supplied when onboarding started; `\"\"` when none |\n| `user_type` | `personal` or `business` |\n| `status` | Lowercased onboarding state at emission (see the status table below). Typically `started` or `pendingsumsub` here — not `active`. `inactive` when the state is unknown. |\n\n**`kyc.approved` / `kyc.pending_review` / `kyc.rejected`** (person) and **`kyb.approved` /\n`kyb.pending_review` / `kyb.rejected`** (business). Verification approved by the identity\nprovider (Sumsub) does **not**, by itself, mean the customer may transact:\n`kyc.pending_review` / `kyb.pending_review` fires when the provider review comes back GREEN\nbut KwiikPay has not yet made its own compliance decision — the customer's lifecycle and\n`activity_restrictions` are unchanged, and nothing about them becomes usable yet.\n`kyc.approved` / `kyb.approved` fires only once KwiikPay's own compliance decision approves\nthe case — that is the point the customer actually becomes usable.\n\n| `data` field | Meaning |\n|---|---|\n| `customer_id` (kyc) / `business_id` (kyb) | Id of the subject |\n| `tenant_id` | Your tenant id |\n| `customer_email` (kyc) | **Always `null`** — the verification events carry no email; read the customer resource |\n| `business_name` (kyb) | Your external reference for the business (the registered name is not carried) |\n| `kyc_status` / `kyb_status` | `approved`, `pending_review` or `rejected` |\n| `rejection_reasons` | `[]` on approval and while pending review. On rejection always exactly one element: the provider's reason text, or the literal `\"Verification was rejected.\"` when none was recorded |\n\n**`viban.ready`** — a fiat account is issued and usable. The only event carrying bank coordinates.\n\n| `data` field | Meaning |\n|---|---|\n| `user_id`, `customer_id` | Both the customer id |\n| `tenant_id` | Your tenant id |\n| `virtual_account_id` | The account id (same id as the v1 account routes) |\n| `iban`, `bic` | EUR accounts; `null` on a GBP account |\n| `sort_code`, `account_number` | GBP accounts; `null` on a EUR account |\n| `currency` | ISO 4217 code |\n| `status` | Translated status — `active` for this event (see the status table) |\n| `provider`, `provider_name` | Both the raw provider code, e.g. `OPENPAYD` or `FIAT_REPUBLIC` |\n| `provider_account_id` | The provider's own reference for the account |\n| `account_holder_name`, `bank_name`, `account_country` | As reported by the provider; `null` when not supplied |\n| `bank_address` | **Always `null`** |\n\n**`virtual_account_request.completed` / `.needs_correction` / `.rejected`** and\n**`customer.account_ready` / `customer.needs_information`** — one shape:\n\n| `data` field | Meaning |\n|---|---|\n| `event` | Repeats the event type |\n| `customer_id` | Id of the customer |\n| `request_id`, `virtual_account_id` | Both the account id |\n| `currency` | ISO 4217 code |\n| `provider` | Raw provider code |\n| `status` | Translated status: `active`, `pending`, `needs_information` or `failed` (see the status table) |\n| `vendor_error_code` | The literal `\"provider_status\"` whenever a status reason exists, else `null` — it is not a provider code |\n| `vendor_error_message`, `officer_note` | Both the same status-reason text from the provider, or `null` |\n| `missing_fields` | **Always `[]`**, including on `customer.needs_information` — the reason text is in `vendor_error_message` |\n| `created_at`, `updated_at` | Both equal `occurred_at` (not the account's original creation time) |\n| `completed_at` | `occurred_at` on `.completed` and `customer.account_ready`; `null` otherwise |\n\n**`deposit.received`** — one event type, two shapes, distinguished by `deposit_type`.\n\n| `data` field | `deposit_type: \"fiat\"` | `deposit_type: \"crypto\"` |\n|---|---|---|\n| `customer_id`, `tenant_id` | Customer id, tenant id | Same |\n| `customer_email` | Receiving customer's recorded email identity, or its single active user's email; `null` when unavailable or ambiguous. Never the payer's email | Same |\n| `tenant_name` | Platform tenant/programme name, when available | Same |\n| `sender` | Stored provider-reported payer name, or `null` when not captured; a display name, not independently verified identity | **Always `null`** |\n| `virtual_account_id` | The receiving account id | `null` |\n| `crypto_wallet_id` | `null` | Id of the deposit address request the funds arrived on |\n| `iban`, `bic` | Receiving account coordinates when the provider reported them, else `null` | `null` |\n| `amount_minor`, `amount_major` | Gross amount, minor units and number | Gross amount in the asset's atomic units and number |\n| `fee_minor`, `fee_major` | Fee actually charged (0 when none) | **Always 0** — no fee is reported on this shape |\n| `net_amount_minor`, `net_amount_major` | Gross minus fee | Equal to the gross amount |\n| `asset_code` | `null` | Asset code, e.g. `USDC` |\n| `currency` | ISO 4217 code | The asset code again |\n| `provider` | Banking provider code | Custody provider code |\n| `provider_transaction_id` | Provider's payment reference, or the Kwiikpay payment id when none | The custodian's transaction reference — **not** the chain hash — falling back to the hash only when the custodian gave none |\n| `ledger_transaction_id` | Ledger journal entry id, or `null` | Ledger journal entry id, or `null` |\n| `reference` | Incoming payment/remittance reference, or `null` when not captured; distinct from the provider transaction id and internal hold reference | The **on-chain transaction hash** |\n| `deposit_address_masked` | `null` | First 6 + `...` + last 6 characters — but an address of 12 characters or fewer is returned **in full** |\n| `deposited_at` | `occurred_at` | `occurred_at` |\n\n```json\n{\n  \"id\": \"019ed568-9f73-7cc3-aa32-f97cdbc620d1\",\n  \"type\": \"deposit.received\",\n  \"occurred_at\": \"2026-06-17T11:46:49.000Z\",\n  \"api_version\": \"2026-05-legacy\",\n  \"data\": {\n    \"customer_id\": \"019ecfeb-c15b-760f-9e2b-8e50ce653573\",\n    \"tenant_id\": \"7c9e6679-7425-40de-944b-e07fc1f90ae7\",\n    \"tenant_name\": \"Example Programme\",\n    \"customer_email\": \"customer@example.com\",\n    \"deposit_type\": \"fiat\",\n    \"virtual_account_id\": \"019ed130-7a2c-7def-8a01-aabbccddeeff\",\n    \"crypto_wallet_id\": null,\n    \"iban\": \"GB29NWBK60161331926819\",\n    \"bic\": \"NWBKGB2L\",\n    \"amount_minor\": 2500,\n    \"amount_major\": 25.00,\n    \"fee_minor\": 100,\n    \"fee_major\": 1.00,\n    \"net_amount_minor\": 2400,\n    \"net_amount_major\": 24.00,\n    \"asset_code\": null,\n    \"currency\": \"EUR\",\n    \"provider\": \"OPENPAYD\",\n    \"provider_transaction_id\": \"a0f48150-2532-54d7-9e16-92bd6eaf0bbf\",\n    \"ledger_transaction_id\": \"019ed568-aaaa-7ccc-bbbb-f97cdbc620d1\",\n    \"reference\": \"INV-1042\",\n    \"deposit_address_masked\": null,\n    \"deposited_at\": \"2026-06-17T11:46:49.000Z\",\n    \"sender\": \"Example Payer Ltd\"\n  }\n}\n```\n\n**`withdrawal.completed`** — one event type, two shapes, distinguished by `data.type` (which is\nNOT the envelope `type`).\n\n| `data` field | `type: \"fiat\"` | `type: \"crypto\"` |\n|---|---|---|\n| `customer_id`, `tenant_id` | Customer id, tenant id | Same |\n| `withdrawal_id` | The fiat withdrawal id | The crypto withdrawal id |\n| `amount_minor` | Amount in minor units | Amount in the asset's atomic units |\n| `currency` | ISO 4217 code | **Always `null`** — use `asset_code` |\n| `status` | Always `completed` | Always `completed` |\n| `fee_minor` | Fee actually charged (0 when none) | **Always 0**, even when a fee was charged — read the withdrawal resource for the fee |\n| `provider_transaction_id` | Provider's payment reference, or the withdrawal id when none | Custodian's transfer reference, or the withdrawal id when none |\n| `completed_at` | `occurred_at` | `occurred_at` |\n| `beneficiary_id` | **Always `null`** today | **Always `null`** |\n| `beneficiary_iban_masked`, `beneficiary_name` | **Always `null`** | **Always `null`** |\n| `asset_code` | `null` | Asset code |\n| `destination_address_masked` | `null` | First 6 + `...` + last 6 (addresses of 12 characters or fewer in full) |\n| `blockchain_tx_hash` | `null` | **Always `null`** today — the hash is not carried on this event |\n\n```json\n{\n  \"id\": \"019ed55d-fa3e-7ebc-a2ec-64d28eba354e\",\n  \"type\": \"withdrawal.completed\",\n  \"occurred_at\": \"2026-06-17T11:36:18.000Z\",\n  \"api_version\": \"2026-05-legacy\",\n  \"data\": {\n    \"customer_id\": \"019ecfeb-c15b-760f-9e2b-8e50ce653573\",\n    \"tenant_id\": \"7c9e6679-7425-40de-944b-e07fc1f90ae7\",\n    \"withdrawal_id\": \"019ed55d-fa3e-7ebc-a2ec-64d28eba354e\",\n    \"type\": \"fiat\",\n    \"amount_minor\": 500,\n    \"currency\": \"EUR\",\n    \"status\": \"completed\",\n    \"fee_minor\": 0,\n    \"provider_transaction_id\": \"b2098694-85aa-431a-a2b9-ac19bc2e8c03\",\n    \"completed_at\": \"2026-06-17T11:36:18.000Z\",\n    \"beneficiary_id\": null,\n    \"beneficiary_iban_masked\": null,\n    \"beneficiary_name\": null,\n    \"asset_code\": null,\n    \"destination_address_masked\": null,\n    \"blockchain_tx_hash\": null\n  }\n}\n```\n\n**`conversion.completed`** — an exchange order settled. Note that the amounts are the\n**reserved source and the quoted (expected) target**, not a provider-reported fill.\n\n| `data` field | Meaning |\n|---|---|\n| `conversion_request_id` | The exchange order id |\n| `customer_id`, `tenant_id` | Customer id, tenant id |\n| `from_currency`, `to_currency` | Source asset reserved; target asset expected |\n| `amount_minor` | Source amount reserved, minor/atomic units |\n| `destination_amount_minor` | The **expected** target amount from the quote, minor/atomic units |\n| `executed_rate` | A JSON number: expected target (major) divided by reserved source (major), i.e. derived from the quote; `null` when the source amount is missing or zero |\n| `completed_at` | `occurred_at` |\n\n```json\n{\n  \"id\": \"019ed5a0-2b3c-7d4e-8f90-0a1b2c3d4e5f\",\n  \"type\": \"conversion.completed\",\n  \"occurred_at\": \"2026-06-17T12:05:30.000Z\",\n  \"api_version\": \"2026-05-legacy\",\n  \"data\": {\n    \"conversion_request_id\": \"019ed5a0-2b3c-7d4e-8f90-0a1b2c3d4e5f\",\n    \"customer_id\": \"019ecfeb-c15b-760f-9e2b-8e50ce653573\",\n    \"tenant_id\": \"7c9e6679-7425-40de-944b-e07fc1f90ae7\",\n    \"from_currency\": \"EUR\",\n    \"to_currency\": \"USDC\",\n    \"amount_minor\": 2500,\n    \"destination_amount_minor\": 26800000,\n    \"executed_rate\": 1.072,\n    \"completed_at\": \"2026-06-17T12:05:30.000Z\"\n  }\n}\n```\n\n### `status` vocabulary on the provisioning and customer events\n\n`data.status` on `customer.created`, `viban.ready` and the `virtual_account_request.*` /\n`customer.account_ready` / `customer.needs_information` family is a translation of the internal\nstate: `Completed` or `Active` become `active`; `Failed` becomes `failed`; `ManualReview` becomes\n`needs_information`; `Pending`, `PendingProvider` or `Requested` become `pending`. **Any other\ninternal state arrives lowercased as-is** (for example `started`, `pendingsumsub`, `returned`,\n`closed`), so the set is open — tolerate unknown values rather than failing on them.\n\n## Paying someone: beneficiary approval\n\nEvery beneficiary you create is reviewed by KwiikPay before it can be paid. This applies to\nthe dashboard too — it is not an API restriction.\n\nA beneficiary is created with `approval_status: \"PendingApproval\"` and **cannot be paid until\nit reads `Approved`**. The create response carries the field populated; to follow the\ndecision, subscribe to the `beneficiary.approved` and `beneficiary.rejected` events (see\nWebhooks — they arrive in the `kwiikpay.webhooks.v1` payload shape, not the legacy envelope)\nor poll `GET /api/v1/partner/customers/{customerId}/banking/beneficiaries/{beneficiaryId}`.\n\n**Approval is completed within 24 hours.**\n\nDo not infer payability from `status` or `provider_status`: those describe provisioning at the\nbanking provider, and a beneficiary can be fully provisioned there while still awaiting our\nreview. `approval_status` confirms the staff decision. OpenPayd also requires `bank_registration.status` to be `Registered` once registration is requested.\n\nA rejected beneficiary reads `approval_status: \"Rejected\"`. The reason is not returned on this\nsurface — contact support, who can explain it.",
    "version": "v1"
  },
  "servers": [
    {
      "url": "https://api.kwiikpay.io",
      "description": "API HTTPS endpoint"
    }
  ],
  "paths": {
    "/api/v1/partner/customers/{customerId}/payout-batches": {
      "post": {
        "tags": [
          "Customers"
        ],
        "summary": "Create payout batch",
        "description": "Creates a payout batch: many fiat withdrawals from one customer's balance to that customer's stored beneficiaries, staged in one call.\n\n**What this call does — and does not do.** It validates every item and stores the batch in status `Draft`. It moves no money and reserves no funds. No route on this API submits, approves or executes a batch: a Kwiikpay operator submits it for approval, and a different operator approves and executes it. No webhook fires for any batch transition — poll `GET .../payout-batches/{batchId}` and read `status`. The Payout batches guide has the full lifecycle.\n\n**HTTP 200 is not success.** An item that fails validation (zero, negative or fractional amount; empty or over-long reference; an omitted or all-zero `beneficiary_id`; beneficiary missing, unapproved, still provisioning or in another currency) is returned as `Skipped` with `failure_code` `validation` and a `failure_reason`; the other items proceed. When EVERY item is skipped the batch is created in status `Rejected` — still HTTP 200 with `success: true` and the message \"Payout batch created successfully\". Always read `payout_batch.status` and each item's `status`.\n\n**Currency and rail.** `currency` must be a platform fiat currency (GBP, EUR or USD) — anything else, AUD included, is refused with 422 `unsupported_currency`. `rail` is optional and is resolved against Fiat Republic's scheme table for the currency before anything is stored; the response carries the canonical scheme (FASTER_PAYMENTS comes back as FPS, SEPA as SCT). Details on the `rail` and `currency` fields.\n\n**Idempotency.** Within 24 hours an identical retry is replayed byte for byte (same body, message \"Payout batch created successfully\") and a retry with a different body, path or caller is refused with 409. After 24 hours an identical retry re-runs, finds the batch by key and returns it in its CURRENT state with the message \"Payout batch retrieved successfully\" — that message is the only signal that a batch was not created by this call.\n\n**Errors.**\n- **400** `application/problem+json`, code `idempotency_key_invalid`: `Idempotency-Key` missing, sent twice, or not 8-256 visible ASCII characters.\n- **404** flat error object, title \"Customer was not found.\": unknown customer id, or a customer your key is not bound to (never 403).\n- **409** `application/problem+json`: `idempotency_key_reused` — the key was used with a different body, path or caller: use a new key; `idempotency_key_in_progress` — the first attempt is still running: retry the identical request shortly.\n- **422** envelope, code in `data.code`: `currency_required` (blank currency); `unsupported_currency` (not GBP, EUR or USD — AUD included); `items_required` (missing or empty array); `too_many_items` (more than 500); `unsupported_rail` (rail not a Fiat Republic scheme for this currency, or the internal EAGLE_NET_TRANSFER); `subject_not_active` (the customer is not Active).\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `payout-batches:write` scope.",
        "operationId": "PublicApiV1PayoutBatchesCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreatePayoutBatchRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreatePayoutBatchRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreatePayoutBatchRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiPayoutBatchData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "payout-batches:write"
            ]
          }
        ]
      },
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "List payout batches",
        "description": "Lists this customer's payout batches, newest first (`created_at` then `id` descending), WITHOUT their items — `items` is null on every entry; read a batch by id for per-item outcomes.\n\n`limit` is clamped to 1..500 (0 or a negative value becomes 1; default 100) and `page` is 1-based. `pagination.total` is the real number of batches for this customer and `last_page` is ceil(total / per_page), so a customer with 150 batches and the default limit needs two pages. There is no status or date filter on this route.\n\n**404** (flat error object, title \"Customer was not found.\"): the customer id is unknown, or your key is bound to a different customer. A subject-bound key gets 404 here, never 403.\n\nRequires the `payout-batches:read` scope.",
        "operationId": "PublicApiV1PayoutBatchesList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiPayoutBatchListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "payout-batches:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/payout-batches/{batchId}": {
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Get payout batch",
        "description": "Returns one payout batch with its items, ordered by `sequence`, and each item's outcome. Poll this route to observe status transitions — there are no payout-batch webhooks.\n\nTwo different **404**s, both flat error objects: title \"Customer was not found.\" when the customer id is unknown or your key is bound to a different customer; title \"Payout batch was not found.\" when the batch id is unknown OR the batch belongs to a different customer of your tenant — the two cases are deliberately indistinguishable.\n\nRequires the `payout-batches:read` scope.",
        "operationId": "PublicApiV1PayoutBatchesGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "batchId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiPayoutBatchData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "payout-batches:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/crypto/withdrawal-addresses": {
      "post": {
        "tags": [
          "Customers"
        ],
        "summary": "Submit crypto withdrawal address",
        "description": "Adds a destination to the customer's withdrawal-address whitelist. This route only ever SUBMITS: the entry always starts `PendingVerification`, and still needs a proof-of-control test deposit and Kwiikpay staff approval — neither of which this API can skip or perform — before it reaches `Active` and becomes usable as `whitelisted_address_id` on a withdrawal.\n\n`customerId` may be your own id or a direct end-customer you onboarded, the same delegation every other route on this API honours. `network` is required only for a multi-network asset (USDT: ETHEREUM or TRON); omitting it selects the asset's default. `label` is required.\n\n| Status | `code` | When | Fix |\n|---|---|---|---|\n| 400 | idempotency_key_invalid | `Idempotency-Key` missing, duplicated or not 8–256 visible ASCII (problem+json) | Send one valid header |\n| 404 | — | Customer does not exist or is not visible to this key (flat `{title,detail,status}`) | Check the id and the key's subject binding |\n| 409 | kyc_or_kyb_not_approved | Customer onboarding is not approved | Complete onboarding first |\n| 409 | crypto_withdrawal_address_already_exists | This asset/network/address is already on file for this customer | List entries instead of resubmitting |\n| 409 | crypto_withdrawal_address_workflow_unavailable | Whitelisting is not configured on this deployment | Contact support |\n| 422 | unsupported_asset | `asset` is not BTC, ETH, USDC, USDT or TRX, or `network` is not valid for it | Use a listed asset/network |\n| 422 | label_required | `label` is blank | Send a label |\n| 422 | address_required | `address` is blank | Send an address |\n| 422 | invalid_crypto_withdrawal_address | The address fails format validation for the network | Fix the address |\n\nSee the [crypto guide](/guides/crypto).\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `withdrawal-addresses:write` scope.",
        "operationId": "PublicApiV1CryptoWithdrawalAddressCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateCryptoWithdrawalAddressRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateCryptoWithdrawalAddressRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateCryptoWithdrawalAddressRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiCryptoWithdrawalAddressData"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "withdrawal-addresses:write"
            ]
          }
        ]
      },
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "List crypto withdrawal addresses",
        "description": "Lists the customer's whitelisted crypto withdrawal-address entries, newest first, whatever added them — this route, or the Kwiikpay dashboard. Unbounded and unpaginated.\n\n`status` is PendingVerification, ControlVerified, Active, Rejected or Disabled. Only Active can be used as `whitelisted_address_id` on a withdrawal; poll this route after adding one until it reaches Active.\n\n| Status | `code` | When | Fix |\n|---|---|---|---|\n| 404 | — | Customer does not exist or is not visible to this key (flat `{title,detail,status}`) | Check the id and the key's subject binding |\n\nSee the [crypto guide](/guides/crypto).\n\nRequires the `withdrawal-addresses:read` scope.",
        "operationId": "PublicApiV1CryptoWithdrawalAddressesList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiCryptoWithdrawalAddressListData"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "withdrawal-addresses:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/test": {
      "get": {
        "tags": [
          "API Configuration"
        ],
        "summary": "Test API key",
        "description": "Checks that the supplied public API key resolves to an active tenant and returns its `tenant_id`. NOTE THE SHAPE: this is the one v1 200 that is NOT wrapped in the standard envelope — the body is a bare `{success, message, tenant_id}` object with no `status_code` or `data`. Do not point your envelope parser at it; every other 200 on this surface is `{success, status_code, message, data}`. Requires `onboarding:read`.\n\nRequires the `onboarding:read` scope.",
        "operationId": "PublicApiV1Test",
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiTestResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/me": {
      "get": {
        "tags": [
          "API Configuration"
        ],
        "summary": "Identify this API key",
        "description": "Returns the customer this key acts for, its subject binding, and the scopes it carries. Call this first: every customer-scoped route takes `customerId` in its path, and this is how you obtain yours without a dashboard. Requires no scope beyond a valid key — it discloses only what the key already proves. `customer_id` is null for a tenant-bound key, which acts for every customer on the tenant and so has no single own customer; such a caller must supply an explicit id. `scopes` is returned in the same colon vocabulary the documents use, so a 403 can be diagnosed without contacting support.",
        "operationId": "PublicApiV1IdentityGet",
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiIdentityData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [ ]
          }
        ]
      }
    },
    "/api/public/partner/config": {
      "get": {
        "tags": [
          "API Configuration"
        ],
        "summary": "Get API configuration",
        "description": "Returns the tenant's name and id, which product families are enabled, the v1 base URL for the host you called, and the prefix of the key you called with. Several members are fixed today: `branding.primary_color` is always `#6366f1`, `branding.logo_url`, `branding.favicon_url` and `support.email` are always null. `public_key` is the API key PREFIX, not a signing or webhook key.\n\nRequires the `onboarding:read` scope.",
        "operationId": "PublicApiConfigGet",
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiConfigData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers": {
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "List customers",
        "description": "Lists the customers and businesses this key can act for, newest first, as one paged union of both kinds. `status` is an EXACT, case-sensitive match on the lifecycle status (`Draft`, `Onboarding`, `PendingCompliance`, `Active`, `Rejected`, `Suspended`, `Blocked`, `Closed`); `externalReference` is an exact match on the customer's email/reference; `type` is `personal` or `customer` for people, `business` for companies, omitted for both. IMPORTANT: this list does NOT refresh verification from Sumsub. A customer whose verification was approved since you last read them by id still shows `Onboarding` here until `GET /customers/{customerId}` (or an onboarding status route) is called for them. Poll by id to detect activation, never this list.\n\nRequires the `onboarding:read` scope.",
        "operationId": "PublicApiV1CustomersList",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "description": "Which kind of record to list: `personal` or `customer` for people, `business` for companies. Omit for both. Any other value matches nothing.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Exact, case-sensitive lifecycle status to filter on: `Draft`, `Onboarding`, `PendingCompliance`, `Active`, `Rejected`, `Suspended`, `Blocked` or `Closed`. Note the list does not refresh verification from Sumsub, so a just-approved customer can still filter as `Onboarding` here.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "externalReference",
            "in": "query",
            "description": "Exact match on the customer's `external_reference` (their email as supplied at creation). Returns at most one person and one business.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiCustomerListData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}": {
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Get customer",
        "description": "Returns one customer or business. THIS READ REFRESHES VERIFICATION: before answering, the customer's latest Sumsub case is re-checked and, if Sumsub has approved it, the customer is activated — `status` moves from `Onboarding`/`PendingCompliance` to `Active` and money-movement restrictions are lifted — as a side effect of this call. A customer that is `Suspended`, `Blocked`, `Closed` or `Rejected` is never re-activated by it. If Sumsub cannot be reached the read still succeeds with the stored (possibly stale) status. `GET /customers` does not do this, so the two can disagree until you read by id.\n\nRequires the `onboarding:read` scope.",
        "operationId": "PublicApiV1CustomersGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiCustomerData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:read"
            ]
          }
        ]
      },
      "patch": {
        "tags": [
          "Customers"
        ],
        "summary": "Update customer",
        "description": "Updates a customer's email/reference, name, jurisdiction and/or lifecycle status. Every field is optional; omitted fields are unchanged. `email` and `external_reference` are the same field (external_reference wins when both are sent) and must contain `@`; a value already held by another customer is refused with 409 external_reference_in_use. `status` is ONE-WAY DE-ESCALATION: only `Suspended`, `Blocked` or `Closed` are accepted (422 unsupported_status otherwise) and no route on this API — including a later approved verification — moves the customer back to `Active`. Contact support to reverse it.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `onboarding:write` scope.",
        "operationId": "PublicApiV1CustomersSupplementalUpdate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCustomerUpdateRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCustomerUpdateRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCustomerUpdateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiCustomerData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/onboarding/status": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Get onboarding status",
        "description": "Returns the combined KYC/KYB verification state for one customer or business, refreshed from Sumsub first (the same read-that-refreshes as `GET /customers/{customerId}`: an approved case activates the customer during this call). Three vocabularies appear on the one response: `kyc_status`/`kyb_status` and `steps.*` use the verification vocabulary (`pending`, `in_progress`, `approved`, `rejected`, `blocked`, `closed`, plus `not_applicable` under `steps`), `current_step` is `kyc`, `kyb` or `completed`, and `customer_status` is the raw lifecycle status. Exactly one of `kyc_status`/`kyb_status` is non-null, decided by the customer's type.\n\nRequires the `onboarding:read` scope.",
        "operationId": "PublicApiV1OnboardingStatusGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiOnboardingStatusData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/onboarding/kyc/status": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Get KYC status",
        "description": "Returns the individual verification state for one customer, refreshed from Sumsub first (an approved case activates the customer during this call). For a BUSINESS customer this is not an error — it returns 200 with `kyc_status: null`; use the KYB route. Poll this (or the combined status route) rather than `GET /customers`.\n\nRequires the `onboarding:read` scope.",
        "operationId": "PublicApiV1KycStatusGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiKycStatusData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/onboarding/kyb/status": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Get KYB status",
        "description": "Returns the business verification state for one business, refreshed from Sumsub first (an approved case activates the business during this call). For an INDIVIDUAL customer this is not an error — it returns 200 with `kyb_status: null`; use the KYC route.\n\nRequires the `onboarding:read` scope.",
        "operationId": "PublicApiV1KybStatusGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiKybStatusData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/onboarding/kyc/initiate": {
      "post": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Start KYC onboarding",
        "description": "Opens (or re-opens) the Sumsub KYC verification session for an individual customer and returns its hosted `verification_link` and Web SDK `token`. `/kyc/initiate` and `/kyc/token` are THE SAME OPERATION — one handler, two paths; call either, not both. The session is keyed on the customer, not on your `Idempotency-Key`: calling again returns the same case with a freshly minted token, and no header value forces a new session. Side effect on a `Draft` customer: status becomes `Onboarding` and ALL money movement is blocked until verification is approved. Four outcomes share the 200: a fresh token; a re-minted token on replay; `token` AND `verification_link` both null because the customer is already verified (`status: approved` — do not launch the SDK); or both null because the case was parked for manual review before Sumsub was called. The token lives 14 days (`SdkTokenTtlInSecs`); expiry surfaces inside the Sumsub SDK, not as an API error — call this again for a fresh one. 422 invalid_onboarding_type for a business; 404 for an unknown customer.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `onboarding:write` scope.",
        "operationId": "PublicApiV1KycInitiate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiOnboardingSessionData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/onboarding/kyc/token": {
      "post": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Start KYC onboarding (token alias)",
        "description": "Opens (or re-opens) the Sumsub KYC verification session for an individual customer and returns its hosted `verification_link` and Web SDK `token`. `/kyc/token` and `/kyc/initiate` are THE SAME OPERATION — one handler, two paths; call either, not both. The session is keyed on the customer, not on your `Idempotency-Key`: calling again returns the same case with a freshly minted token, and no header value forces a new session. Side effect on a `Draft` customer: status becomes `Onboarding` and ALL money movement is blocked until verification is approved. Four outcomes share the 200: a fresh token; a re-minted token on replay; `token` AND `verification_link` both null because the customer is already verified (`status: approved` — do not launch the SDK); or both null because the case was parked for manual review before Sumsub was called. The token lives 14 days (`SdkTokenTtlInSecs`); expiry surfaces inside the Sumsub SDK, not as an API error — call this again for a fresh one. 422 invalid_onboarding_type for a business; 404 for an unknown customer.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `onboarding:write` scope.",
        "operationId": "PublicApiV1KycToken",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiOnboardingSessionData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/onboarding/kyb/initiate": {
      "post": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Start KYB onboarding",
        "description": "Opens (or re-opens) the Sumsub KYB verification session for a business customer and returns its hosted `verification_link` and Web SDK `token`. `/kyb/initiate` and `/kyb/token` are THE SAME OPERATION — one handler, two paths; call either, not both. The session is keyed on the customer, not on your `Idempotency-Key`: calling again returns the same case with a freshly minted token, and no header value forces a new session. Side effect on a `Draft` customer: status becomes `Onboarding` and ALL money movement is blocked until verification is approved. Four outcomes share the 200: a fresh token; a re-minted token on replay; `token` AND `verification_link` both null because the customer is already verified (`status: approved` — do not launch the SDK); or both null because the case was parked for manual review before Sumsub was called. The token lives 14 days (`SdkTokenTtlInSecs`); expiry surfaces inside the Sumsub SDK, not as an API error — call this again for a fresh one. 422 invalid_onboarding_type for an individual; 404 for an unknown customer.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `onboarding:write` scope.",
        "operationId": "PublicApiV1KybInitiate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiOnboardingSessionData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/onboarding/kyb/token": {
      "post": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Start KYB onboarding (token alias)",
        "description": "Opens (or re-opens) the Sumsub KYB verification session for a business customer and returns its hosted `verification_link` and Web SDK `token`. `/kyb/token` and `/kyb/initiate` are THE SAME OPERATION — one handler, two paths; call either, not both. The session is keyed on the customer, not on your `Idempotency-Key`: calling again returns the same case with a freshly minted token, and no header value forces a new session. Side effect on a `Draft` customer: status becomes `Onboarding` and ALL money movement is blocked until verification is approved. Four outcomes share the 200: a fresh token; a re-minted token on replay; `token` AND `verification_link` both null because the customer is already verified (`status: approved` — do not launch the SDK); or both null because the case was parked for manual review before Sumsub was called. The token lives 14 days (`SdkTokenTtlInSecs`); expiry surfaces inside the Sumsub SDK, not as an API error — call this again for a fresh one. 422 invalid_onboarding_type for an individual; 404 for an unknown customer.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `onboarding:write` scope.",
        "operationId": "PublicApiV1KybToken",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiOnboardingSessionData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/onboarding/requirements": {
      "get": {
        "tags": [
          "API Configuration"
        ],
        "summary": "Get manual onboarding requirements",
        "description": "Discovery for the manual submission routes: which Sumsub verification level applies and what it needs. Pass `level` explicitly, or let it be resolved: `type=kyb` selects the business level (`kwiikpay-v2-business-unified-risk-profile`), anything else selects the individual level (`individual-onboarding`); `jurisdiction` is accepted but does not change the result today. An unknown `level` is 422 unknown_level. `required_fields` are the exact JSON paths on the submit request body (for example `person.first_name`, `company.address.post_code`). Note that the 422 missing_required_fields detail returned by the submit routes still names fields in an internal camelCase spelling (`person.firstName`, `person.dateOfBirth` for `dob`) — map them back by hand. Requires `onboarding:read`; no customer id.\n\nRequires the `onboarding:read` scope.",
        "operationId": "PublicApiV1ManualOnboardingRequirements",
        "parameters": [
          {
            "name": "level",
            "in": "query",
            "description": "Sumsub verification level to describe. Omit to have it resolved from `type`. Unknown values are refused with 422 unknown_level; the accepted names are listed in that error.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Used only when `level` is omitted: `kyb` resolves the business level, anything else (or omitted) the individual level.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "jurisdiction",
            "in": "query",
            "description": "Country of the applicant. Accepted for forward compatibility; it does not change which level is resolved today.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiOnboardingRequirementsData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/transfer/allowed-destinations": {
      "get": {
        "tags": [
          "Exchange"
        ],
        "summary": "List transfer destinations",
        "description": "Lists the asset codes (fiat AND crypto) the exchange can deliver for your tenant. The list is derived from the tenant's enabled, currently effective exchange fee configuration — not from the fiat account currency list — so it is EMPTY when no exchange fee is configured, which means the exchange is unavailable, not that the tenant has no currencies. Pass `source_currency` to get only the destinations that trade from that source under the pair rules; without it the list is unfiltered and a pair drawn from it can still be refused with 422 `invalid_pair` by the price and conversion routes.\n\nRequires the `exchanges:read` scope.",
        "operationId": "PublicApiV1TransferAllowedDestinations",
        "parameters": [
          {
            "name": "source_currency",
            "in": "query",
            "description": "Optional asset you intend to SELL (for example GBP or USDC). When supplied, the returned list contains only destinations that trade from that source under the pair rules (same asset excluded; BTC and ETH sources trade to fiat only). When omitted the list is unfiltered and a pair drawn from it can still be refused with 422 invalid_pair.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiAllowedDestinationsData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "exchanges:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/transfer/price": {
      "get": {
        "tags": [
          "Exchange"
        ],
        "summary": "Get indicative transfer price",
        "description": "Returns an INDICATIVE price for a source/destination pair, plus the customer-facing fee, the all-in price and the provider's own expiry. It is not executable and does not hold a rate: the response carries no quote id, and creating a conversion re-prices at the live rate rather than referencing anything returned here. The summary previously called this an executable price, which it has never been. Amounts are MINOR units of the source asset and both `amount_minor` and `amount` are 64-bit integers. If you send zero, a negative value, or neither, the request still SUCCEEDS against a silently substituted default probe size — 20 major units for fiat and for USDC/USDT, 0.001 BTC, 0.01 ETH — so the price you get back is for a size you did not request. The response echoes `source_amount_minor`; compare it against what you sent to detect that case.\n\nRequires the `exchanges:read` scope.",
        "operationId": "PublicApiV1TransferPrice",
        "parameters": [
          {
            "name": "pair",
            "in": "query",
            "description": "Asset pair as SOURCE/TARGET, for example GBP/USDC. Split on exactly one forward slash; anything else is refused with 422 invalid_pair.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "amount_minor",
            "in": "query",
            "description": "Source amount in MINOR units of the source asset, as a 64-bit INTEGER — not a decimal. Takes precedence over `amount` when both are positive. IMPORTANT: this is a probe size for pricing, and a value of zero or below — or omitting both parameters — is NOT an error. A default probe size is silently substituted and you receive a real, confident price for an amount you did not ask for: 20 major units for fiat and for USDC/USDT, 0.001 BTC, 0.01 ETH. Send an explicit positive integer if the quote must reflect your size. A non-integer such as 20.5 fails model binding and returns a framework 400, not the usual 422 envelope.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int64"
            }
          },
          {
            "name": "amount",
            "in": "query",
            "description": "Alias of `amount_minor` — MINOR units of the source asset, as a 64-bit INTEGER, not a decimal. `amount_minor` wins when both are positive. Zero, negative, or omitting both is NOT an error: a default probe size is silently substituted (20 major units for fiat and USDC/USDT, 0.001 BTC, 0.01 ETH) and the price returned is for that size, not yours.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int64"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiTransferPriceData"
                }
              }
            }
          },
          "409": {
            "description": "Conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "exchanges:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/conversion-requests": {
      "get": {
        "tags": [
          "Exchange"
        ],
        "summary": "List conversion requests",
        "description": "Lists one customer's conversions, newest first, with `page` and `limit` (1-500) and real totals, in every status. A conversion is finished only when `status` is `Settled`. 404 (flat body) for a customer the key cannot see.\n\nRequires the `exchanges:read` scope.",
        "operationId": "PublicApiV1ConversionRequestsList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiConversionRequestListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "exchanges:read"
            ]
          }
        ]
      },
      "post": {
        "tags": [
          "Exchange"
        ],
        "summary": "Create conversion request",
        "description": "Creates a fiat or crypto conversion request for one customer. **A conversion needs an account or wallet on BOTH sides.** The customer must already hold a Completed fiat account for each fiat asset, and a wallet for each crypto asset, in the pair — this endpoint does not open either one for you. If a side is missing you get **409** `exchange_account_missing` naming which side and which asset; open that account or wallet first and retry. Note the rate is applied at execution, not quoted here: `/transfer/price` is indicative and holds nothing.\n\n**Pair rules.** Source and destination must differ, and a BTC or ETH source converts to fiat only (422 `invalid_pair`). Fiat, USDC and USDT sources have a 20 major-unit minimum (422 `below_minimum_trade_size`). See the [Currencies and rails guide](/guides/currencies-and-rails).\n\n**200 is not settled.** The response carries the order at `Submitted` (or `Reserved`); the destination balance is credited only when `status` reaches `Settled` — poll `GET .../conversion-requests/{conversionRequestId}` or subscribe to `conversion.completed`.\n\nRefusals (envelope `data.code`): 409 `kyc_or_kyb_not_approved`; 422 `invalid_pair`, `invalid_amount`, `below_minimum_trade_size`, `insufficient_onchain_balance` (crypto source); 409 `exchange_account_missing`, `exchange_wallet_missing`, `exchange_funding_account_ambiguous`, `exchange_ledger_prerequisites_missing`, `exchange_workflow_unavailable`, `crypto_balance_unverifiable`, `crypto_network_unavailable`, `insufficient_network_funds`, and `exchange_order_failed` (any other failure — `data.detail` is a raw message, not a contract). 404 (flat body) for an unknown customer; 400 problem+json for a missing or malformed `Idempotency-Key`.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `exchanges:write` scope.",
        "operationId": "PublicApiV1ConversionRequestsCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiConversionRequestCreateRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiConversionRequestCreateRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiConversionRequestCreateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiConversionRequestData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": { }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "exchanges:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/conversion-requests/{conversionRequestId}": {
      "get": {
        "tags": [
          "Exchange"
        ],
        "summary": "Get conversion request",
        "description": "Returns one conversion. Poll this after creating one: it is finished when `status` is `Settled` (`completed_at` set); `Failed` and `Released` mean the held funds were returned; `Parked` means an operator is reconciling it. 404 (flat body) when the conversion does not belong to this customer.\n\nRequires the `exchanges:read` scope.",
        "operationId": "PublicApiV1ConversionRequestsGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "conversionRequestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiConversionRequestListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "exchanges:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/deposits": {
      "get": {
        "tags": [
          "Fiat Deposits"
        ],
        "summary": "List fiat deposits",
        "description": "Lists fiat deposits for one customer — what ARRIVED, including anything still held for review. This is how a partner reconstructs an inbound period after a missed webhook. No status filter: read `status` (only `Completed` is spendable) and `reason_code` (set on a `ManualReview` hold) on every row. Newest first, paged with `page` and `limit` (1-500), real `pagination.total`. 404 when the customer is not the caller's.\n\nRequires the `deposits:read` scope.",
        "operationId": "PublicApiV1DepositsList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiDepositListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "deposits:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/crypto/deposits": {
      "get": {
        "tags": [
          "Crypto Deposits"
        ],
        "summary": "List crypto deposits",
        "description": "Lists crypto deposits for one customer, each with its on-chain transaction hash. No status filter is applied — rows that failed, reorged or are held for review are included, so read `status` (only `Confirmed` is credited) and `reason_code` (set on a `Quarantined` hold) before crediting anyone. Deposits are found through the deposit addresses the customer requested; a customer with no address yet gets an empty page. Newest first, paged with `page` and `limit` (1-500), real `pagination.total`. 404 when the customer is not the caller's.\n\nRequires the `deposits:read` scope.",
        "operationId": "PublicApiV1CryptoDepositsList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiDepositListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "deposits:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/withdrawals": {
      "get": {
        "tags": [
          "Fiat Withdrawals"
        ],
        "summary": "List fiat withdrawals",
        "description": "Lists one customer's fiat withdrawals, newest first, with `page` and `limit` (1-500) and real totals, in every status including `Failed`. Crypto withdrawals are on the crypto routes and deposits on the deposits route. 404 (flat body) for a customer the key cannot see.\n\n**For a sub-customer.** Authenticate with the parent's API key and put the owned child's ID in `customerId`. The beneficiary must belong to that child; withdrawals debit the child's available balance plus its effective fee. Both parent and child must pass lifecycle and compliance controls for a withdrawal. Parent refusals can be 409 `parent_not_active`, 403 `compliance_subject_blocked`, 409 `compliance_subject_manual_review`, or 503 `parent_compliance_unavailable` (flat error bodies). See [Sub-customers & parent access](/guides/sub-customers) for beneficiary approval, payment status and retries with the same key and body.\n\nRequires the `withdrawals:read` scope.",
        "operationId": "PublicApiV1WithdrawalsList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer who owns the beneficiary or withdrawal. For a parent's delegated request, use its own direct sub-customer's ID here, with the parent's API key.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiWithdrawalListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "withdrawals:read"
            ]
          }
        ]
      },
      "post": {
        "tags": [
          "Fiat Withdrawals"
        ],
        "summary": "Create fiat withdrawal",
        "description": "Pays a registered beneficiary from the customer's `Available` balance. Preconditions, in the order they are checked: the customer is verified (409 `kyc_or_kyb_not_approved`); the beneficiary exists for this customer (422 `beneficiary_not_found`), is in the withdrawal currency (422 `beneficiary_currency_mismatch`), is `Completed` with a provider reference (409 `beneficiary_not_ready`) and is staff-approved (409 `beneficiary_not_approved`); and the customer holds a `Completed` account in the currency AT THE BENEFICIARY'S PROVIDER, which funds the payment (409 `source_account_not_ready`). See the [Currencies and rails guide](/guides/currencies-and-rails) for the provider chain.\n\n**200 does not mean paid.** Every outcome after the pre-checks returns 200 with `data.withdrawal`: `PendingProvider` (submitted; final outcome via `withdrawal.completed` or the list route), `Failed` (the provider refused before anything left — the funds are released; the reason is not returned), or `ManualReview` (held for review, funds stay reserved). Branch on `status`, never on the HTTP code.\n\nOther refusals (envelope `data.code`): 422 `unsupported_currency`, `invalid_amount`, `unsupported_rail`, `rail_not_supported_by_beneficiary`, `invalid_payment_reference` (USD at Fiat Republic only); 409 `withdrawal_client_money_blocked`, `source_account_pot_missing`, `fiat_withdrawal_prerequisites_missing`; 409 `fiat_withdrawal_failed` — a submission failure whose `data.detail` is free text, not a stable contract. This code does not prove that no payment row or hold exists. For an uncertain outcome, retry the same URL, Idempotency-Key and unchanged body while reconciling; do not create a replacement payment. 404 (flat body) for an unknown customer; 400 problem+json for a missing or malformed `Idempotency-Key`.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\n**For a sub-customer.** Authenticate with the parent's API key and put the owned child's ID in `customerId`. The beneficiary must belong to that child; withdrawals debit the child's available balance plus its effective fee. Both parent and child must pass lifecycle and compliance controls for a withdrawal. Parent refusals can be 409 `parent_not_active`, 403 `compliance_subject_blocked`, 409 `compliance_subject_manual_review`, or 503 `parent_compliance_unavailable` (flat error bodies). See [Sub-customers & parent access](/guides/sub-customers) for beneficiary approval, payment status and retries with the same key and body.\n\nRequires the `withdrawals:write` scope.",
        "operationId": "PublicApiV1WithdrawalsCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer who owns the beneficiary or withdrawal. For a parent's delegated request, use its own direct sub-customer's ID here, with the parent's API key.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateFiatWithdrawalRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateFiatWithdrawalRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateFiatWithdrawalRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiWithdrawalData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": { }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "withdrawals:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/accounts": {
      "get": {
        "tags": [
          "Fiat Accounts"
        ],
        "summary": "List fiat accounts",
        "description": "Lists one customer's fiat account requests and issued accounts, most recently changed first, with `page` and `limit` (1-500) and real totals. Every status is included — filter with `status` (exact status name) and `currencyCode`. An account is usable only when `status` is `Completed` and `provider_account_id` is set. 404 (flat body) for a customer the key cannot see.\n\nRequires the `accounts:read` scope.",
        "operationId": "PublicApiV1BankingAccountsList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by resource status.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "currencyCode",
            "in": "query",
            "description": "Filter by fiat currency code such as GBP or EUR.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiAccountListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:read"
            ]
          }
        ]
      },
      "post": {
        "tags": [
          "Fiat Accounts"
        ],
        "summary": "Create fiat account",
        "description": "Requests a virtual bank account (vIBAN, or UK sort code and account number) for one customer. The response carries the request as accepted; the bank details arrive when `status` reaches `Completed` — poll `GET .../banking/accounts/{accountId}` or subscribe to `viban.ready`. See the [Currencies and rails guide](/guides/currencies-and-rails) for which provider issues which currency.\n\n**One account per currency.** A customer may hold ONE active (`Pending`, `PendingProvider`, `ManualReview` or `Completed`) fiat account per currency, at any provider. A second request in the same currency at the same provider returns the existing account; at a different provider it is refused with **409** `duplicate_currency_account` — the detail names the provider and status of the account in the way. Close the existing account before opening one at another bank.\n\n**Query parameters.** `currencyCode`, `provider`, `provider_code` and `providerCode` are accepted here as WRITE inputs (fallbacks for the body fields), not as filters, despite what their shared descriptions say; the body wins when both are present.\n\nRefusals in evaluation order (envelope `data.code` unless noted): 409 `kyc_or_kyb_not_approved`; 422 `unsupported_currency`; 422 `unsupported_provider` (any explicit provider but FIAT_REPUBLIC — omit the field to reach OpenPayd); 422 `provider_unavailable` (FIAT_REPUBLIC named while Fiat Republic opening is switched off — omitting the provider bypasses this gate); 403 flat body with `code` `self_serve_provisioning_disabled` (self-serve account opening is locked for this customer — operations opens accounts on request, and this gate runs FIRST inside the workflow, before the offer policy), `fiat_unsupported_jurisdiction` or `currency_not_available_for_customer_type`; 422 `missing_provider_prerequisites` (the customer record lacks data the provider needs and the request is parked at that step — `data.detail` names it; supply it via `provider_input_values`, see the form-schema route); 409 `provider_routing_unavailable` (no provider could be routed for the currency and rail); 409 `onboarding_prerequisite_not_met` (see above, and any other precondition the workflow raises — read `data.detail`). 404 (flat body) for an unknown customer; 400 problem+json for a missing or malformed `Idempotency-Key`.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `accounts:write` scope.",
        "operationId": "PublicApiV1BankingAccountsCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "currencyCode",
            "in": "query",
            "description": "Filter by fiat currency code such as GBP or EUR.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "provider_code",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "providerCode",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "provider",
            "in": "query",
            "description": "Filter by provider code such as FIAT_REPUBLIC, UTILA, HERCLE, or SUMSUB.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiCreateBankingAccountRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiCreateBankingAccountRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiCreateBankingAccountRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiAccountData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": { }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/accounts/{accountId}": {
      "get": {
        "tags": [
          "Fiat Accounts"
        ],
        "summary": "Get fiat account",
        "description": "Returns one fiat account, including its bank details once `status` is `Completed`. 404 (flat body) when the account does not belong to this customer — indistinguishable from one that does not exist.\n\nRequires the `accounts:read` scope.",
        "operationId": "PublicApiV1BankingAccountsGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "accountId",
            "in": "path",
            "description": "Fiat account/account request UUID scoped to the tenant and customer.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiAccountData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/accounts/form-schema": {
      "get": {
        "tags": [
          "Fiat Accounts"
        ],
        "summary": "Get fiat account form schema",
        "description": "Returns the options available to this tenant when OPENING A FIAT ACCOUNT for one customer: supported currencies, supported providers, supported rails, and the three form fields (`currency`, required; `provider` and `rail`, both optional). It describes accounts only — it says nothing about beneficiaries, and it is not a beneficiary field schema. Two limits worth knowing. `supported_providers` lists only what the CREATE route will accept as an explicit value — today Fiat Republic alone — while `supported_currencies`, `supported_rails` and the `providers` array describe what the platform can do. Those are different questions: omitting `provider` entirely is not the same as naming one, and it is the only way to be placed with a provider you cannot name. OpenPayd issues every live GBP and EUR account and cannot be requested by name. And `provider_input_values` is populated only for the provider it belongs to; an entry for a provider whose input contract we have not documented carries an empty list, which means 'not described here', NOT 'no input needed'.\n\nRequires the `accounts:read` scope.",
        "operationId": "PublicApiV1BankingAccountsFormSchema",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiBankingAccountFormSchemaData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/accounts/{accountId}/transactions": {
      "get": {
        "tags": [
          "Fiat Accounts"
        ],
        "summary": "List fiat account transactions",
        "description": "The ledger of ONE fiat account: one row per ledger journal, newest first, paged with `page` and `limit` (1-500) and a real `pagination.total`. A journal is one accounting event, so a deposit can appear twice (pending, then available), a withdrawal at least twice (reserved, then settled), and a fee is its own row. `amount` is signed minor units: positive into the account, negative out, and 0 for a journal that only moves money between the account's own buckets (a hold being reserved). Every bucket is included, so a positive row is not necessarily spendable — `.../banking/balance` is the spendable figure. `status` is always `completed`; `fee_minor`, `spot_rate`, `fee_rate`, `effective_rate` are always null; `net_amount_minor` equals `amount`. 404 when the customer or the account is not the caller's. See the reconciliation guide.\n\nRequires the `accounts:read` scope.",
        "operationId": "PublicApiV1BankingAccountTransactions",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "accountId",
            "in": "path",
            "description": "Fiat account/account request UUID scoped to the tenant and customer.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiTransactionListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/beneficiaries/{beneficiaryId}": {
      "get": {
        "tags": [
          "Fiat Beneficiaries"
        ],
        "summary": "Get fiat beneficiary",
        "description": "Returns one fiat beneficiary, including its `approval_status` and OpenPayd `bank_registration`. Poll this route to watch a pending decision move; the create, update and delete responses carry the same field, populated as of the write. A payee is payable only when `status` is `Completed`, `provider_reference` is set AND `approval_status` is `Approved`; requested OpenPayd registrations also need `bank_registration.status` = `Registered`. Their saved rail is returned in `supported_schemes`. 404 (flat body) when the beneficiary does not belong to this customer — indistinguishable from one that does not exist.\n\n**For a sub-customer.** Authenticate with the parent's API key and put the owned child's ID in `customerId`. The beneficiary must belong to that child; withdrawals debit the child's available balance plus its effective fee. Both parent and child must pass lifecycle and compliance controls for a withdrawal. Parent refusals can be 409 `parent_not_active`, 403 `compliance_subject_blocked`, 409 `compliance_subject_manual_review`, or 503 `parent_compliance_unavailable` (flat error bodies). See [Sub-customers & parent access](/guides/sub-customers) for beneficiary approval, payment status and retries with the same key and body.\n\nRequires the `accounts:read` scope.",
        "operationId": "PublicApiV1BeneficiariesGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer who owns the beneficiary or withdrawal. For a parent's delegated request, use its own direct sub-customer's ID here, with the parent's API key.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "beneficiaryId",
            "in": "path",
            "description": "Fiat beneficiary UUID scoped to the tenant and customer.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiBeneficiaryData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:read"
            ]
          }
        ]
      },
      "patch": {
        "tags": [
          "Fiat Beneficiaries"
        ],
        "summary": "Update fiat beneficiary",
        "description": "OpenPayd rows whose bank registration has started return 409 `beneficiary_registration_immutable`; the existing payee is preserved. A race with registration returns 409 `beneficiary_registration_changed`; refresh and review any pending replacement. For other entries this is a REPLACE, not a partial update: it CREATES A REPLACEMENT beneficiary from the body you send — nothing is merged from the existing row except `currency` and `provider` when you omit them — and then supersedes the old one. Send the COMPLETE payee, exactly as on create. The response is `data.beneficiaries` with NEW ids; the id in the URL is disabled (`status` `Failed`, `provider_status` `LOCAL_DISABLED`) only once the replacement is usable (`Completed`/`PendingProvider` with a provider reference) — if the replacement parks at `ManualReview`, the old beneficiary stays active and you now hold two ids. The replacement starts `PendingApproval` again. Same preconditions and codes as create, with 409 `beneficiary_update_failed` in place of `beneficiary_creation_failed`, plus 404 (flat body) when the beneficiary does not belong to this customer.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\n**For a sub-customer.** Authenticate with the parent's API key and put the owned child's ID in `customerId`. The beneficiary must belong to that child; withdrawals debit the child's available balance plus its effective fee. Both parent and child must pass lifecycle and compliance controls for a withdrawal. Parent refusals can be 409 `parent_not_active`, 403 `compliance_subject_blocked`, 409 `compliance_subject_manual_review`, or 503 `parent_compliance_unavailable` (flat error bodies). See [Sub-customers & parent access](/guides/sub-customers) for beneficiary approval, payment status and retries with the same key and body.\n\nRequires the `accounts:write` scope.",
        "operationId": "PublicApiV1BeneficiariesUpdate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer who owns the beneficiary or withdrawal. For a parent's delegated request, use its own direct sub-customer's ID here, with the parent's API key.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "beneficiaryId",
            "in": "path",
            "description": "Fiat beneficiary UUID scoped to the tenant and customer.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateBeneficiaryRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateBeneficiaryRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateBeneficiaryRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiBeneficiaryBatchData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": { }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:write"
            ]
          }
        ]
      },
      "delete": {
        "tags": [
          "Fiat Beneficiaries"
        ],
        "summary": "Delete fiat beneficiary",
        "description": "Disables the beneficiary LOCALLY: it can no longer be used for a withdrawal, but the payee is NOT removed at the banking provider. The row is kept and keeps appearing on the list route with `status` `Failed` and `provider_status` `LOCAL_DISABLED`; deleting an already-disabled row is a no-op that still returns 200. Unlike create and update, the body is `data.beneficiary` (singular), returned after disabling. Refusals: 409 `kyc_or_kyb_not_approved` (envelope), 404 (flat body) when the beneficiary does not belong to this customer, 400 problem+json for a missing or malformed `Idempotency-Key`.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\n**For a sub-customer.** Authenticate with the parent's API key and put the owned child's ID in `customerId`. The beneficiary must belong to that child; withdrawals debit the child's available balance plus its effective fee. Both parent and child must pass lifecycle and compliance controls for a withdrawal. Parent refusals can be 409 `parent_not_active`, 403 `compliance_subject_blocked`, 409 `compliance_subject_manual_review`, or 503 `parent_compliance_unavailable` (flat error bodies). See [Sub-customers & parent access](/guides/sub-customers) for beneficiary approval, payment status and retries with the same key and body.\n\nRequires the `accounts:write` scope.",
        "operationId": "PublicApiV1BeneficiariesDelete",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer who owns the beneficiary or withdrawal. For a parent's delegated request, use its own direct sub-customer's ID here, with the parent's API key.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "beneficiaryId",
            "in": "path",
            "description": "Fiat beneficiary UUID scoped to the tenant and customer.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiBeneficiaryData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": { }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/beneficiaries": {
      "get": {
        "tags": [
          "Fiat Beneficiaries"
        ],
        "summary": "List fiat beneficiaries",
        "description": "Lists one customer's fiat beneficiaries, newest first, with `page` and `limit` (1-500) and real totals. No status filter is applied: deleted and superseded rows are returned too (`status` `Failed`, `provider_status` `LOCAL_DISABLED`), so filter client-side. `approval_status` and OpenPayd `bank_registration` are populated; requested OpenPayd registrations return their saved `supported_schemes`. A customer the key cannot see returns 404. See the [Beneficiaries guide](/guides/beneficiaries).\n\n**For a sub-customer.** Authenticate with the parent's API key and put the owned child's ID in `customerId`. The beneficiary must belong to that child; withdrawals debit the child's available balance plus its effective fee. Both parent and child must pass lifecycle and compliance controls for a withdrawal. Parent refusals can be 409 `parent_not_active`, 403 `compliance_subject_blocked`, 409 `compliance_subject_manual_review`, or 503 `parent_compliance_unavailable` (flat error bodies). See [Sub-customers & parent access](/guides/sub-customers) for beneficiary approval, payment status and retries with the same key and body.\n\nRequires the `accounts:read` scope.",
        "operationId": "PublicApiV1BeneficiariesList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer who owns the beneficiary or withdrawal. For a parent's delegated request, use its own direct sub-customer's ID here, with the parent's API key.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiBeneficiaryListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:read"
            ]
          }
        ]
      },
      "post": {
        "tags": [
          "Fiat Beneficiaries"
        ],
        "summary": "Create fiat beneficiary",
        "description": "Registers an external bank account the customer can withdraw to. Read the [Beneficiaries guide](/guides/beneficiaries) first — this route has three traps.\n\n**Precondition.** The customer must already hold a fiat account in the same currency whose provider has accepted it (`provider_account_id` set) at Fiat Republic or OpenPayd; otherwise **409** `bank_account_required`. The beneficiary is created at THAT account's provider, and a later withdrawal is funded from that account — provider identity is a chain, not a choice.\n\n**200 is not success.** The response is `data.beneficiaries` — an ARRAY, one row per eligible provider (normally one). Read each row's `status`: only `Completed` with a `provider_reference` is usable. Kwiikpay pre-checks the per-scheme payee shape for USD (422 `beneficiary_details_incomplete`); other malformed GBP/EUR details can return 200 with `status` `ManualReview`. OpenPayd names are pre-checked on every currency: send `attributes.beneficiary_first_name` and `attributes.beneficiary_last_name` for a person, or `attributes.beneficiary_company_name` for a company. Explicit business `type` plus nonblank `name` also supplies the company name; customer defaults never do, and explicit provider identity attributes retain precedence. Missing or invalid identity returns 422 `beneficiary_details_incomplete` before the workflow.\n\n**OpenPayd GBP bank details.** Faster Payments/CHAPS accept a valid UK IBAN or sort code plus account number. A supplied IBAN is checked and converted to local coordinates; any explicit provider coordinates must match it. Invalid, non-GB or conflicting domestic GBP instructions return 422 `beneficiary_details_incomplete` before the workflow. The address country may differ from the bank country. SWIFT remains an explicit route.\n\n**Approval gate.** Every new beneficiary is `PendingApproval` until Kwiikpay staff approve it; a withdrawal before then is refused with 409 `beneficiary_not_approved`. `approval_status` is populated on this response. OpenPayd approval queues bank registration under the exact owning customer; poll GET until `bank_registration.status` is `Registered` before withdrawing.\n\nRefusals (envelope body, `data.code`): 409 `kyc_or_kyb_not_approved`; 422 `unsupported_currency`, `unsupported_payment_scheme`, `beneficiary_details_incomplete`, `unsupported_provider`, `beneficiary_provider_validation_failed`; 409 `bank_account_required`, `beneficiary_creation_failed` (not atomic across providers — list before retrying). 404 (flat body) for an unknown customer; 400 problem+json for a missing or malformed `Idempotency-Key`.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\n**For a sub-customer.** Authenticate with the parent's API key and put the owned child's ID in `customerId`. The beneficiary must belong to that child; withdrawals debit the child's available balance plus its effective fee. Both parent and child must pass lifecycle and compliance controls for a withdrawal. Parent refusals can be 409 `parent_not_active`, 403 `compliance_subject_blocked`, 409 `compliance_subject_manual_review`, or 503 `parent_compliance_unavailable` (flat error bodies). See [Sub-customers & parent access](/guides/sub-customers) for beneficiary approval, payment status and retries with the same key and body.\n\nRequires the `accounts:write` scope.",
        "operationId": "PublicApiV1BeneficiariesCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer who owns the beneficiary or withdrawal. For a parent's delegated request, use its own direct sub-customer's ID here, with the parent's API key.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateBeneficiaryRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateBeneficiaryRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateBeneficiaryRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiBeneficiaryBatchData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": { }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/commercial-config": {
      "get": {
        "tags": [
          "Fees"
        ],
        "summary": "Get commercial configuration",
        "description": "The tenant-wide fee rows in force right now: enabled, effective at the time of the call, for both customer types, excluding rows negotiated for a single customer. One row per fee dimension, but the dimension includes rail/network, geography and product which the row does not expose, so rows can repeat apart from `id`. Not paged.\n\nRequires the `fees:read` scope.",
        "operationId": "PublicApiV1CommercialConfig",
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiCommercialConfigData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "fees:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/fees": {
      "get": {
        "tags": [
          "Fees"
        ],
        "summary": "Get customer fees",
        "description": "The fee rows that apply to one customer right now: filtered to the customer's type (`Consumer` or `Business`), with any customer-specific row replacing the tenant default for the same dimension. Same shape and same rail/network caveat as commercial-config. Not paged. 404 when the customer is not the caller's.\n\nRequires the `fees:read` scope.",
        "operationId": "PublicApiV1CustomerFees",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiCustomerFeesData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "fees:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/balance": {
      "get": {
        "tags": [
          "Fiat Accounts"
        ],
        "summary": "Get fiat balance",
        "description": "Returns the customer's AVAILABLE fiat balance per currency — the funds that can be withdrawn or converted now; money still clearing or locked by an open withdrawal or conversion is excluded. One row per currency in which the customer holds a `Completed` account or a ledger balance, at 0 when nothing has arrived yet; a currency whose account request failed or was closed is omitted. Crypto balances are not included. 404 (flat body) for a customer the key cannot see.\n\nRequires the `balances:read` scope.",
        "operationId": "PublicApiV1BankingBalanceGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiBalanceListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "balances:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/crypto/wallets": {
      "get": {
        "tags": [
          "Crypto Wallets"
        ],
        "summary": "List crypto wallets",
        "description": "Lists every ISSUED deposit address one customer holds, newest first — one row per asset and network, `status` always Active. Addresses still being issued, or held for review, do not appear here; read them with GET .../crypto/wallets/{asset}/balance. Unbounded and unpaginated: there is no `limit` and no pagination object.\n\nBalances are per ASSET, not per address: a customer with USDT on both ETHEREUM and TRON gets two rows that each report the full pooled USDT total, so never sum rows. `balance` includes reserved and pending postings; see the field descriptions. Before answering, the platform reconciles the customer's deposits with the custody provider; if that call fails you receive **503** `provider_balance_refresh_failed` (envelope with `data.code`) — nothing is wrong with your request, retry later.\n\n| Status | `code` | When | Fix |\n|---|---|---|---|\n| 404 | — | Customer does not exist or is not visible to this key (flat `{title,detail,status}`) | Check the id and the key's subject binding |\n| 503 | provider_balance_refresh_failed | Custody-provider reconciliation failed before the read | Retry with backoff |\n\nSee the [crypto guide](/guides/crypto).\n\nRequires the `wallets:read` scope.",
        "operationId": "PublicApiV1CryptoWalletsList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiWalletListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "wallets:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/crypto/wallets/{asset}/address": {
      "post": {
        "tags": [
          "Crypto Wallets"
        ],
        "summary": "Create crypto deposit address",
        "description": "Get-or-create: returns the customer's existing Active deposit address for the asset on its default network, or asks the custody provider to issue one. A fresh `Idempotency-Key` does NOT create a second address for the same asset. The customer must have approved onboarding.\n\n**A 200 does not guarantee an address.** The 200 branch is taken for every outcome that is not a refusal, including two where issuance is still pending: the custody call failed (`wallet.status` ManualReviewRequired) or a concurrent request with the same key is still in flight (`wallet.status` as it stood, typically Requested). In both cases `address` is null. Branch on `address != null`, not on the status code; when it is null, poll GET .../crypto/wallets/{asset}/balance until `status` is Active. The `wallet` object's three balance figures are hardcoded to 0 on this response.\n\n**Network.** The network cannot be chosen here. BTC → BITCOIN, ETH/USDC/USDT → ETHEREUM, TRX → TRON. A USDT address from this route is ALWAYS an Ethereum (ERC-20) address; there is no way to obtain a TRON (TRC-20) USDT deposit address through this API. Tell depositors the network explicitly — USDT sent on TRON to the address returned here is lost.\n\n| Status | `code` | When | Fix |\n|---|---|---|---|\n| 400 | idempotency_key_invalid | `Idempotency-Key` missing, duplicated or not 8–256 visible ASCII (problem+json) | Send one valid header |\n| 404 | — | Customer does not exist or is not visible to this key (flat `{title,detail,status}`) | Check the id and the key's subject binding |\n| 409 | kyc_or_kyb_not_approved | Customer onboarding is not approved | Complete onboarding first |\n| 422 | unsupported_asset | `asset` is not BTC, ETH, USDC, USDT or TRX | Use a listed asset |\n| 409 | crypto_wallet_workflow_unavailable | Wallet workflow not configured on this deployment | Contact support |\n| 422 | crypto_asset_network_policy_not_configured | Your tenant has no policy row for this asset/network (TRX and USDT/TRON are per-tenant enablements) | Ask Kwiikpay to enable the asset |\n| 422 | crypto_deposits_disabled | Deposits are switched off for this asset/network in your tenant | Ask Kwiikpay to enable deposits |\n| 422 | crypto_wallet_opening_not_configured | No ledger/provider configuration for this customer, asset and network | Contact support |\n| 409 | crypto_wallet_provisioning_in_progress, crypto_wallet_provider_exception | The customer's custody wallet container is still being created, so no address was requested yet (`message` \"Crypto deposit address is pending.\") | Retry later with a new key |\n| 200 | (none; `address` null) | Address pending: crypto_deposit_address_provider_exception or crypto_deposit_address_in_progress — the code is NOT in the body | Poll the balance route |\n\nSee the [crypto guide](/guides/crypto).\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `wallets:write` scope.",
        "operationId": "PublicApiV1CryptoDepositAddressCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "asset",
            "in": "path",
            "description": "Crypto asset code: BTC, ETH, USDC, USDT or TRX. Case-insensitive and trimmed (\"usdc\" is accepted). Any other value is 422 unsupported_asset. The network is implied — BTC on BITCOIN, ETH/USDC/USDT on ETHEREUM, TRX on TRON — and cannot be chosen on these routes; a USDT address issued or read here is ALWAYS the ETHEREUM one.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiWalletAddressData"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "wallets:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/crypto/wallets/{asset}/balance": {
      "get": {
        "tags": [
          "Crypto Wallets"
        ],
        "summary": "Get crypto wallet balance",
        "description": "Returns the customer's pooled balance for one asset, plus the newest deposit-address request for that asset on its DEFAULT network (USDT → ETHEREUM; a TRON USDT address is never returned here). Unlike the list route there is no status filter: `wallet.status` can be Requested, ManualReviewRequired, Blocked, Failed or Active, and is the literal Unknown when the customer has no address for this asset and network — in which case `address` is null, `wallet.id` falls back to the pooled ledger account id (or the all-zero GUID) and the timestamps are the time of your request. This is the route to poll after a create-address call returned `address: null`.\n\nThe balance figures are keyed on the asset across ALL networks (a USDT total includes TRON holdings even though the row is labelled ETHEREUM) and include reserved amounts; see the field descriptions. The asset is validated BEFORE the customer, so a bad asset for an unknown customer is 422, not 404. Before answering, the platform reconciles the customer's deposits with the custody provider; a failure there is **503** `provider_balance_refresh_failed`, safe to retry.\n\n| Status | `code` | When | Fix |\n|---|---|---|---|\n| 422 | unsupported_asset | `asset` is not BTC, ETH, USDC, USDT or TRX | Use a listed asset |\n| 404 | — | Customer does not exist or is not visible to this key (flat `{title,detail,status}`) | Check the id and the key's subject binding |\n| 503 | provider_balance_refresh_failed | Custody-provider reconciliation failed before the read | Retry with backoff |\n\nSee the [crypto guide](/guides/crypto).\n\nRequires the `balances:read` scope.",
        "operationId": "PublicApiV1CryptoWalletBalanceGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "asset",
            "in": "path",
            "description": "Crypto asset code: BTC, ETH, USDC, USDT or TRX. Case-insensitive and trimmed (\"usdc\" is accepted). Any other value is 422 unsupported_asset. The network is implied — BTC on BITCOIN, ETH/USDC/USDT on ETHEREUM, TRX on TRON — and cannot be chosen on these routes; a USDT address issued or read here is ALWAYS the ETHEREUM one.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiWalletAddressData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "balances:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/crypto/withdrawals": {
      "get": {
        "tags": [
          "Crypto Withdrawals"
        ],
        "summary": "List crypto withdrawals",
        "description": "Lists one customer's crypto withdrawals, newest first, in every state — Blocked and Failed rows are included, so read `status` before treating a row as a payout. `limit` is clamped to 1–500 and `page` is 1-based; `pagination.total` and `last_page` are real counts. There is no status, date or asset filter, and there is no single-withdrawal GET on this API: to find one withdrawal, page this list and match on `id`, so persist the `id` you receive at create time.\n\nCrypto rows have `type` \"crypto\", `asset_code` set and `currency` null. `amount` is in ATOMIC units (satoshi/wei/10⁻⁶) as a JSON number — beyond JavaScript's safe-integer range for ETH. `destination` is the unmasked on-chain address. `status` is one of Requested, SubmittedToProvider, ProviderPending, ManualReviewRequired, Blocked, Failed, Settled: Settled is the only success, Blocked and Failed are final refusals (no funds left), ManualReviewRequired is parked (see the create route). No webhook reports a Blocked, Failed or parked crypto withdrawal — only `withdrawal.completed` fires, on Settled — so poll this list for anything else.\n\n| Status | `code` | When | Fix |\n|---|---|---|---|\n| 404 | — | Customer does not exist or is not visible to this key (flat `{title,detail,status}`) | Check the id and the key's subject binding |\n\nSee the [crypto guide](/guides/crypto).\n\nRequires the `withdrawals:read` scope.",
        "operationId": "PublicApiV1CryptoWithdrawalsList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiWithdrawalListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "withdrawals:read"
            ]
          }
        ]
      },
      "post": {
        "tags": [
          "Crypto Withdrawals"
        ],
        "summary": "Create crypto withdrawal",
        "description": "Sends `amount` (MAJOR units) of `asset` to a whitelisted destination. In the default configuration `whitelisted_address_id` is REQUIRED: the entry is created and approved in the Kwiikpay dashboard (no API-key route creates or lists it), must be Active, and must match the asset AND the network you resolve to — paying a whitelisted USDT/TRON address means also sending `network: \"TRON\"`. `destination_address` and `destination_tag` are ignored when a whitelist id is supplied.\n\n**Read the status code AND `data.withdrawal.status`.**\n- **200** — submitted to the custody provider; `status` SubmittedToProvider. Poll the list for Settled; `withdrawal.completed` fires on Settled.\n- **202** — parked, NOT accepted for delivery, `status` ManualReviewRequired. Reasons collapse into this one response and the reason code is NOT in the body: a transaction limit was hit (limit_exceeded), address-risk / travel-rule / provider screening put it on hold, wallet delegation is still being provisioned, the custody submission's outcome is indeterminate, or a request with the same key is still in flight. No webhook fires; poll the list until Settled, Blocked or Failed. One further 202 case carries `status` Blocked: a tenant fee policy of manual review blocks the row while answering 202 — treat body status Blocked as final whatever the HTTP code.\n- **422 with a decision code** — refused after the row was created; it stays visible in the list as Blocked or Failed. Codes: insufficient_funds, insufficient_network_funds (the asset is spread over networks and this network's share is short), awaiting_crypto_delivery (coins bought in a recent conversion have not landed yet — `detail` says how much can be sent now), crypto_withdrawals_disabled, crypto_withdrawal_below_minimum (per-tenant minimum, not published — read `detail`), provider_submission_failed (hold released), ledger_available_balance_insufficient (the ledger refused the hold at reserve time — typically the balance moved between the check and the hold), withdrawal_fee_unavailable (no withdrawal fee is configured for this asset and network — a platform configuration gap, not a customer error), or a screening block code.\n\n**Fees.** The Kwiikpay withdrawal fee is charged ON TOP: the customer's ledger is debited amount + fee, the recipient receives exactly `amount`, and no response field reports the fee. There is no maximum amount; an over-limit request parks as 202 rather than failing. A same-key retry within 24h replays the original response verbatim; see the [idempotency guide](/guides/idempotency).\n\n| Status | `code` | When | Fix |\n|---|---|---|---|\n| 400 | idempotency_key_invalid | `Idempotency-Key` missing, duplicated or not 8–256 visible ASCII (problem+json) | Send one valid header |\n| 404 | — | Customer does not exist or is not visible to this key (flat `{title,detail,status}`) | Check the id and the key's subject binding |\n| 409 | kyc_or_kyb_not_approved | Customer onboarding is not approved | Complete onboarding first |\n| 422 | unsupported_asset | `asset` not BTC/ETH/USDC/USDT/TRX, or `network` not valid for that asset (\"Network X is not supported for ASSET.\") | Fix asset/network |\n| 422 | invalid_amount | `amount` ≤ 0, or rounds to zero at the asset's scale | Send a positive amount ≥ one atomic unit |\n| 422 | whitelisted_address_required | No `whitelisted_address_id` and raw addresses are disabled (default) | Whitelist the address in the dashboard and send its id |\n| 422 | whitelisted_address_not_found | No whitelist entry with that id for this customer | Check the id |\n| 409 | whitelisted_address_not_active | Entry exists but is not yet Active (awaiting control verification / approval) | Wait for approval |\n| 422 | whitelisted_address_asset_mismatch | Entry's asset or network differs from the request | Match `asset` and `network` to the entry |\n| 422 | invalid_destination_address | Raw path enabled and `destination_address` is not 20–100 characters | Fix the address |\n| 409 | crypto_withdrawal_prerequisites_missing | Customer has no Active custody wallet or ledger yet | Create a deposit address first |\n| 409 | crypto_withdrawal_workflow_unavailable | Workflow not configured on this deployment | Contact support |\n| 409 | crypto_withdrawal_failed | The workflow threw — including \"Crypto asset/network policy was not found.\" when your tenant has no policy row for this asset/network (TRX, USDT/TRON) and same-key races; `detail` is the raw message | Read `detail`; ask Kwiikpay to enable the asset |\n| 403 / 409 | compliance code | Hard compliance block (403) or a review hold (409); flat `{title,detail,status,code}` | Contact support / wait for the review |\n| 409 | money_movement_frozen | Money movement temporarily paused platform-wide (flat shape) | Retry later |\n\nSee the [crypto guide](/guides/crypto).\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\nRequires the `withdrawals:write` scope.",
        "operationId": "PublicApiV1CryptoWithdrawalsCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateCryptoWithdrawalRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateCryptoWithdrawalRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateCryptoWithdrawalRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiWithdrawalData"
                }
              }
            }
          },
          "202": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiWithdrawalData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "withdrawals:write"
            ]
          }
        ]
      }
    },
    "/api/v1/verification/callback": {
      "get": {
        "tags": [
          "Onboarding"
        ],
        "summary": "Handle verification callback",
        "description": "A no-op acknowledgement. It reads nothing, changes nothing and reports nothing about any verification: the response is the literal `status: received`, the server time, and `received_parameters`, which is YOUR OWN query string echoed back (any key whose name contains token, secret, signature or key is replaced with `[redacted]`). Verification results never arrive here — poll `GET /customers/{customerId}/onboarding/status` or subscribe to webhooks. Exists as a landing target for a hosted verification return URL; it is not something an integration needs to call.\n\nRequires the `onboarding:write` scope.",
        "operationId": "PublicApiV1VerificationCallback",
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiVerificationCallbackData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "onboarding:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/banking/deposits/{depositId}": {
      "get": {
        "tags": [
          "Fiat Deposits"
        ],
        "summary": "Get fiat deposit",
        "description": "Reads one deposit under `data.deposit`, with the same fields and statuses as the list. `sender` is the captured provider-reported payer name; `reference` is the incoming payment reference. Both may be null and may become available after reconciliation. Use the deposit id from the list or `deposit.received` webhook. Requires `deposits:read`; a parent's key may read its authorized child's deposits. 404 when the customer or deposit is outside the caller's scope, unknown, or not a fiat deposit.\n\nRequires the `deposits:read` scope.",
        "operationId": "PublicApiV1DepositGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "depositId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiDepositData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "deposits:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/crypto/deposits/{depositId}": {
      "get": {
        "tags": [
          "Crypto Deposits"
        ],
        "summary": "Get crypto deposit",
        "description": "Reads one deposit under `data.deposit` through the customer's owned deposit address. Same fields and statuses as the list: only `Confirmed` is credited. `reference` is the transaction hash; `sender` is null because an on-chain transfer does not establish a payer name. Requires `deposits:read`; a parent's key may read its authorized child's deposits. 404 when the customer or deposit is outside the caller's scope, unknown, or not a crypto deposit.\n\nRequires the `deposits:read` scope.",
        "operationId": "PublicApiV1CryptoDepositGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Customer or business UUID scoped to the tenant.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "depositId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiDepositData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "deposits:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers": {
      "post": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Create end customer",
        "description": "Creates an end customer (a customer of your customer) under one of your customers. Idempotent on `external_reference` within the parent and subject type: replaying the same reference and subject type returns the same end customer rather than creating a second one. Requires end-customer onboarding to be enabled for both the tenant and that customer.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:write` scope.",
        "operationId": "PublicApiV1EndCustomerCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateEndCustomerRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateEndCustomerRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PublicApiCreateEndCustomerRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiEndCustomerData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:write"
            ]
          }
        ]
      },
      "get": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "List end customers",
        "description": "Lists the end customers belonging to one of your customers.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:read` scope.",
        "operationId": "PublicApiV1EndCustomerList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiEndCustomerListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}": {
      "get": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Get end customer",
        "description": "Returns one end customer. An end customer belonging to a different parent is reported as not found, exactly as one that does not exist.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:read` scope.",
        "operationId": "PublicApiV1EndCustomerGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiEndCustomerData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}/onboarding": {
      "post": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Start end customer verification",
        "description": "Starts KYC (personal) or KYB (business) for one end customer and returns the Sumsub WebSDK session. `token` is null when no session was minted — either the applicant is already verified (`status` is `verified`) or the case is held for review — so do not launch the SDK without one.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:write` scope.",
        "operationId": "PublicApiV1EndCustomerOnboardingStart",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiStartEndCustomerOnboardingRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiStartEndCustomerOnboardingRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiStartEndCustomerOnboardingRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiOnboardingSessionData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}/balance": {
      "get": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Get end customer balance",
        "description": "Returns one end customer's balances. Amounts are atomic (minor units) and each entry carries its own `asset_scale`.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:read` scope.",
        "operationId": "PublicApiV1EndCustomerBalance",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiEndCustomerBalanceData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}/accounts": {
      "post": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Create end customer virtual account",
        "description": "Requests a vIBAN for one end customer. EUR and GBP only; the account is always an OpenPayd virtual account, and the parent must hold a completed account in the same currency first (`parent_master_account_required`). The end customer's own verification must be approved before an account is issued. Requires end-customer onboarding to be enabled for both the tenant and the parent.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `accounts:write` scope.",
        "operationId": "PublicApiV1EndCustomerAccountCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiCreateEndCustomerAccountRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiCreateEndCustomerAccountRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiCreateEndCustomerAccountRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiAccountData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:write"
            ]
          }
        ]
      },
      "get": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "List end customer virtual accounts",
        "description": "Lists the vIBANs issued to one end customer, newest first.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `accounts:read` scope.",
        "operationId": "PublicApiV1EndCustomerAccountsList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by resource status.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "currencyCode",
            "in": "query",
            "description": "Filter by fiat currency code such as GBP or EUR.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of items to return per page. Endpoints clamp this to their configured maximum (500).",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number. Read `pagination.last_page` from the response to know how many pages exist, and `pagination.total` for the number of items across all of them. A page beyond the last returns an empty list with the true totals, never an error.",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 1
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiAccountListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}/accounts/{accountId}": {
      "get": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Get end customer virtual account",
        "description": "Returns one vIBAN belonging to one end customer. An account under a different end customer is reported as not found, exactly as one that does not exist.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `accounts:read` scope.",
        "operationId": "PublicApiV1EndCustomerAccountGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "accountId",
            "in": "path",
            "description": "Fiat account/account request UUID scoped to the tenant and customer.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiAccountData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "accounts:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}/consolidate": {
      "post": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Consolidate end customer funds",
        "description": "Moves an end-customer's fiat balance up to the parent's operating account: a single balanced GBP/EUR ledger transfer, idempotent on the original opaque Idempotency-Key. Requires tenant and parent transfer capabilities, active and compliant parties, no freeze, shared internal-transfer limits, compatible completed OpenPayd accounts and a matching master-pool policy. Posting, receipt and execution audit commit atomically. Completed historic requests remain recoverable.\n\n`Idempotency-Key` is required on this call. Send a stable unique value for the user action so retries are safe. An identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409. After a non-2xx first attempt the key is released — see the Idempotency guide.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:write` scope.",
        "operationId": "PublicApiV1EndCustomerConsolidate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiEndCustomerConsolidationRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiEndCustomerConsolidationRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiEndCustomerConsolidationRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiEndCustomerConsolidationData"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiValidationData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:write"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}/documents": {
      "get": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "List end customer documents",
        "description": "Lists the verification documents held for one end customer: identifiers and metadata only, no file contents.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:read` scope.",
        "operationId": "PublicApiV1EndCustomerDocumentsList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiEndCustomerDocumentListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}/documents/{documentId}": {
      "get": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Download end customer document",
        "description": "Streams one verification document's file content (the Content-Type is the provider's). Every download is recorded in the audit trail BEFORE the file is released; when the audit write is unavailable the download is refused.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:read` scope.",
        "operationId": "PublicApiV1EndCustomerDocumentDownload",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "documentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": { }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}/internal-transfers": {
      "post": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Transfer fiat between parent and sub-customer",
        "description": "Moves an exact GBP/EUR amount between this parent and its direct child. Use parent_to_end_customer to credit the child from the parent's Available balance, including fiat from a settled parent crypto conversion. No conversion or provider payment is performed by this transfer. Tenant and parent internal-transfer capabilities, active parties, compliance/freeze/limits and compatible pooled OpenPayd accounts with an enabled policy are required for a new transfer. POST requires the same non-empty UUID in Idempotency-Key and client_reference; the durable transaction returns the same completed transfer for matching retries and 409 internal_transfer_client_reference_reused for changed parameters. A timeout is recovered by client_reference; an in-flight 404 is not proof of failure, so retry the identical POST with the same key and body. An uncompleted request with an earlier UTC day's limit decision returns 409 internal_transfer_limit_decision_stale; recover before submitting a fresh UUID under today's limits. Completed transfers still replay across days. Reads remain available after write capability is disabled. List uses take (default 50, clamped to 1–200) and returns recent transfers in both directions without page/cursor navigation. POST 200 means status completed: both balance changes and the recovery record committed atomically.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:write` scope.",
        "operationId": "PublicApiV1EndCustomerInternalTransferCreate",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "Stable unique key for the user action so retries are safe. 8-256 visible ASCII characters; an identical retry returns the original result; a different body under a key whose first attempt succeeded or is still running is refused with 409 (see the Idempotency guide for the failed-attempt case).",
            "required": true,
            "schema": {
              "maxLength": 256,
              "minLength": 8,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiInternalTransferRequest"
                  }
                ]
              }
            },
            "text/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiInternalTransferRequest"
                  }
                ]
              }
            },
            "application/*+json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "null"
                  },
                  {
                    "$ref": "#/components/schemas/PublicApiInternalTransferRequest"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiInternalTransferData"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); (3) `Idempotency-Key` is missing, sent twice, or not 8–256 visible ASCII characters (`code: idempotency_key_invalid`; checked AFTER authentication and rate limiting); (4) the JSON body could not be bound to the request schema — malformed JSON, a string where a number is expected, a bad UUID — answered by the framework with an `errors` map of field to messages and no `code`; see `/guides/errors`. When this operation itself rejects the input it answers `application/json` with the `ErrorResponse` shape also declared here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "Also `application/problem+json` from the idempotency layer, title `Idempotency-Key conflict.`: `code: idempotency_key_reused` — this key was already used by this API key for a DIFFERENT method, path or body (mint a new key; do not edit and resend) — or `code: idempotency_key_in_progress` — the first attempt is still executing (retry the IDENTICAL request shortly and you will receive its stored result). Keys expire after 24 hours.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unprocessable Entity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. A failed first attempt does NOT lock its `Idempotency-Key`: retrying with the same key re-executes the request rather than replaying a stored result, so check the resource's state before retrying a money movement. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:write"
            ]
          }
        ]
      },
      "get": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "List recent parent-child internal transfers",
        "description": "Moves an exact GBP/EUR amount between this parent and its direct child. Use parent_to_end_customer to credit the child from the parent's Available balance, including fiat from a settled parent crypto conversion. No conversion or provider payment is performed by this transfer. Tenant and parent internal-transfer capabilities, active parties, compliance/freeze/limits and compatible pooled OpenPayd accounts with an enabled policy are required for a new transfer. POST requires the same non-empty UUID in Idempotency-Key and client_reference; the durable transaction returns the same completed transfer for matching retries and 409 internal_transfer_client_reference_reused for changed parameters. A timeout is recovered by client_reference; an in-flight 404 is not proof of failure, so retry the identical POST with the same key and body. An uncompleted request with an earlier UTC day's limit decision returns 409 internal_transfer_limit_decision_stale; recover before submitting a fresh UUID under today's limits. Completed transfers still replay across days. Reads remain available after write capability is disabled. List uses take (default 50, clamped to 1–200) and returns recent transfers in both directions without page/cursor navigation. POST 200 means status completed: both balance changes and the recovery record committed atomically.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:read` scope.",
        "operationId": "PublicApiV1EndCustomerInternalTransferList",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "take",
            "in": "query",
            "schema": {
              "pattern": "^-?(?:0|[1-9]\\d*)$",
              "type": [
                "integer",
                "string"
              ],
              "format": "int32",
              "default": 50
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiInternalTransferListData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}/internal-transfers/{transferId}": {
      "get": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Get a parent-child internal transfer",
        "description": "Moves an exact GBP/EUR amount between this parent and its direct child. Use parent_to_end_customer to credit the child from the parent's Available balance, including fiat from a settled parent crypto conversion. No conversion or provider payment is performed by this transfer. Tenant and parent internal-transfer capabilities, active parties, compliance/freeze/limits and compatible pooled OpenPayd accounts with an enabled policy are required for a new transfer. POST requires the same non-empty UUID in Idempotency-Key and client_reference; the durable transaction returns the same completed transfer for matching retries and 409 internal_transfer_client_reference_reused for changed parameters. A timeout is recovered by client_reference; an in-flight 404 is not proof of failure, so retry the identical POST with the same key and body. An uncompleted request with an earlier UTC day's limit decision returns 409 internal_transfer_limit_decision_stale; recover before submitting a fresh UUID under today's limits. Completed transfers still replay across days. Reads remain available after write capability is disabled. List uses take (default 50, clamped to 1–200) and returns recent transfers in both directions without page/cursor navigation. POST 200 means status completed: both balance changes and the recovery record committed atomically.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:read` scope.",
        "operationId": "PublicApiV1EndCustomerInternalTransferGet",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "transferId",
            "in": "path",
            "description": "Transfer ID from data.internal_transfer.id, owned by this parent and child.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiInternalTransferData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:read"
            ]
          }
        ]
      }
    },
    "/api/v1/partner/customers/{customerId}/end-customers/{endCustomerId}/internal-transfers/by-client-reference/{clientReference}": {
      "get": {
        "tags": [
          "Sub-customers"
        ],
        "summary": "Recover an internal transfer by client reference",
        "description": "Moves an exact GBP/EUR amount between this parent and its direct child. Use parent_to_end_customer to credit the child from the parent's Available balance, including fiat from a settled parent crypto conversion. No conversion or provider payment is performed by this transfer. Tenant and parent internal-transfer capabilities, active parties, compliance/freeze/limits and compatible pooled OpenPayd accounts with an enabled policy are required for a new transfer. POST requires the same non-empty UUID in Idempotency-Key and client_reference; the durable transaction returns the same completed transfer for matching retries and 409 internal_transfer_client_reference_reused for changed parameters. A timeout is recovered by client_reference; an in-flight 404 is not proof of failure, so retry the identical POST with the same key and body. An uncompleted request with an earlier UTC day's limit decision returns 409 internal_transfer_limit_decision_stale; recover before submitting a fresh UUID under today's limits. Completed transfers still replay across days. Reads remain available after write capability is disabled. List uses take (default 50, clamped to 1–200) and returns recent transfers in both directions without page/cursor navigation. POST 200 means status completed: both balance changes and the recovery record committed atomically.\n\n**Parent access.** Sub-customers are named `end-customers` in these routes. Authenticate with the parent's API key; `customerId` is the parent and `endCustomerId` is its own direct child in the same tenant. Creating a child, starting verification and issuing an account require onboarding eligibility for both tenant and parent (403 `sub_customer_onboarding_disabled` otherwise); existing reads do not require that grant. See [Sub-customers & parent access](/guides/sub-customers) for the complete flow.\n\nRequires the `end-customers:read` scope.",
        "operationId": "PublicApiV1EndCustomerInternalTransferRecover",
        "parameters": [
          {
            "name": "customerId",
            "in": "path",
            "description": "Parent customer/business ID. Authenticate with the parent's key. The child is identified separately by endCustomerId.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "endCustomerId",
            "in": "path",
            "description": "Sub-customer ID from data.end_customer.id. Must be a direct child of customerId in the same tenant; unrelated and unknown IDs both return 404.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "clientReference",
            "in": "path",
            "description": "The UUID used as client_reference and Idempotency-Key on the original internal transfer.",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "The API key was not accepted. Always `application/problem+json` with `code: api_key_authentication_failed` and a `WWW-Authenticate: Bearer` response header. `detail` is specific only when the REQUEST was wrong — no key, both `X-Api-Key` and `Authorization` sent, a non-Bearer scheme, or a key that does not parse. Every refusal that needed a stored key (unknown, revoked, expired, rotated past its overlap window, not yet active, minted for another environment, or a suspended tenant / API access switched off) is the one sentence `API key authentication failed.`, deliberately, so a probe learns nothing. Fix the credential; retrying does not help, and repeated failures from one IP escalate to 429.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always `Bearer`.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Refused. Two shapes, by who refused. `application/problem+json` with `code: api_key_not_authorized` when the key is valid but lacks a scope this operation requires (EVERY scope listed under `security` must be held), or the call came from an IP outside the key's allowlist. `application/json` `ErrorResponse` when the endpoint itself refuses — a compliance hard block (`code` is the decision, e.g. `compliance_subject_blocked`) or an account offer the customer is not eligible for (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). A subject-bound key asking for another customer's resource gets 404, never 403. Diagnose a scope 403 with `GET /api/v1/partner/me`, which lists the scopes the key holds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              },
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests: `application/problem+json`, `code: rate_limited`, `Retry-After` in whole seconds. Three unrelated causes share this shape. (a) This key exceeded the per-route budget — a fixed window counted per key, per method AND per route template, so `/customers/{a}` and `/customers/{b}` share one counter. (b) Your tenant exceeded the aggregate ceiling, counted across every API-key call on both surfaces AND your dashboard traffic. (c) Too many FAILED authentications from your IP address, shared by everyone behind that address — `detail` reads `Too many failed authentication attempts.` and backing off does not clear it: fix the credential. For (a) and (b) `detail` is `Request rate limit exceeded.`; honour `Retry-After`. Current limits: `/guides/authentication`. No `X-RateLimit-*` headers are sent.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying (whole number, at least 1).",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicApiEnvelopeOfPublicApiInternalTransferData"
                }
              }
            }
          },
          "404": {
            "description": "Not Found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service Unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad request, `application/problem+json`, raised by shared middleware before the operation runs. Causes, in the order they are checked: (1) `X-Correlation-Id` was sent more than once or is longer than 128 visible ASCII characters (`code: correlation_id_invalid`); (2) an `X-Tenant-Id` header was sent — this surface derives the tenant from the key and refuses the header outright, even with a valid key and even when empty (no `code`); see `/guides/errors`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected failure, `application/problem+json` with `status: 500` and no `code`. Quote the `X-Correlation-Id` response header when contacting support.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          {
            "PublicApiKey": [
              "end-customers:read"
            ]
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "ApprovedPaymentInstitution": {
        "required": [
          "id",
          "name",
          "country"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Institution reference scoped to the submitting customer's policy.",
            "example": "reviewed-msb-01"
          },
          "name": {
            "type": "string",
            "description": "Institution legal name recorded in the permitted policy entry at capture time.",
            "example": "Example MSB Ltd"
          },
          "country": {
            "type": "string",
            "description": "Two-letter country code recorded in the permitted policy entry.",
            "example": "GB"
          },
          "active": {
            "type": "boolean",
            "description": "Whether this policy entry permits new downstream declarations. In a saved payment snapshot this records permission at capture, not the institution's current policy state.",
            "default": true
          }
        }
      },
      "BankBeneficiaryRegistrationResponse": {
        "required": [
          "status"
        ],
        "type": "object",
        "properties": {
          "status": {
            "enum": [
              "NotRegistered",
              "Queued",
              "Processing",
              "Reconciling",
              "Registered",
              "NeedsReview",
              "NotApplicable"
            ],
            "type": "string",
            "description": "NotRegistered is historical/local only; Queued/Processing wait for registration; Reconciling checks an uncertain bank result without repeating a create; Registered means the saved bank identity and ownership are verified; NeedsReview requires support. Approval alone is not bank confirmation. Accepted values: `NotRegistered`, `Queued`, `Processing`, `Reconciling`, `Registered`, `NeedsReview`, `NotApplicable`.",
            "example": "Registered"
          },
          "reason": {
            "type": [
              "null",
              "string"
            ],
            "description": "Human-readable reason for an admin/operational action.",
            "example": "Customer requested update"
          },
          "accountHolderId": {
            "type": [
              "null",
              "string"
            ]
          },
          "parentBeneficiaryId": {
            "type": [
              "null",
              "string"
            ]
          },
          "bankBeneficiaryId": {
            "type": [
              "null",
              "string"
            ]
          },
          "registeredAt": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          },
          "failureCode": {
            "type": [
              "null",
              "string"
            ]
          },
          "paymentRail": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "ErrorResponse": {
        "required": [
          "title",
          "detail",
          "status"
        ],
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "Short human-readable summary of the refusal. Not stable and not a contract — branch on `status` and `code`.",
            "example": "Customer was not found."
          },
          "detail": {
            "type": [
              "null",
              "string"
            ],
            "description": "Human-readable explanation. Usually a fixed sentence; on provider-related refusals it can be the provider's own message and can change without notice.",
            "example": "No customer exists for the authenticated tenant and customer id."
          },
          "status": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body.",
            "format": "int32"
          },
          "code": {
            "type": [
              "null",
              "string"
            ],
            "description": "Machine-readable reason, or null. Null on every 404 — including a resource that exists but belongs to a customer this key cannot act for, which is deliberately indistinguishable — and on the generic 503. Populated by the policy gates: a compliance hard block or hold (`compliance_subject_blocked`, `compliance_subject_manual_review`, `compliance_subject_lifecycle_unavailable`, `compliance_subject_lifecycle_unknown`), a fiat account the customer is not offered (`fiat_unsupported_jurisdiction`, `currency_not_available_for_customer_type`). Catalogue and remedies: `/guides/errors`. Different namespace from `PublicApiValidationData.code`; do not share one switch between them.",
            "example": "compliance_subject_blocked"
          }
        }
      },
      "PaymentTransparencyParty": {
        "required": [
          "reference",
          "name",
          "type",
          "country"
        ],
        "type": "object",
        "properties": {
          "reference": {
            "type": "string",
            "description": "Stable reference from your customer records, scoped to the submitting customer; at most 100 characters. Use the same reference in subsequent payments and supporting information.",
            "example": "end-customer-123"
          },
          "name": {
            "type": "string",
            "description": "Party's legal name; at most 200 characters.",
            "example": "Example Trading Ltd"
          },
          "type": {
            "enum": [
              "PERSON",
              "BUSINESS"
            ],
            "type": "string",
            "description": "Person or business; trimmed and canonicalized to uppercase. Accepted values: `PERSON`, `BUSINESS`.",
            "example": "BUSINESS"
          },
          "country": {
            "type": "string",
            "description": "Recognised two-letter ISO country code; stored uppercase.",
            "example": "GB"
          }
        }
      },
      "PaymentTransparencyRequest": {
        "required": [
          "purpose",
          "relationship"
        ],
        "type": "object",
        "properties": {
          "purpose": {
            "type": "string",
            "description": "Meaningful payment purpose, at most 280 characters with no control characters. A bare Other is refused; describe the actual purpose. This does not replace the bank payment reference.",
            "example": "Settlement of invoice INV-123"
          },
          "relationship": {
            "enum": [
              "NotProvided",
              "OwnFunds",
              "DirectCustomer",
              "DownstreamCustomer"
            ],
            "type": "string",
            "description": "Whose payment this is: OwnFunds for the submitting customer's own funds, DirectCustomer for its end customer, or DownstreamCustomer for an end customer of another financial institution. NotProvided records only the payment reason without asserting a relationship, and is refused when the customer's policy requires disclosure. It is a declaration, not a verified finding. Accepted values: `NotProvided`, `OwnFunds`, `DirectCustomer`, `DownstreamCustomer`.",
            "example": "DirectCustomer"
          },
          "underlyingPayer": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PaymentTransparencyParty"
              }
            ],
            "description": "Required for DirectCustomer and DownstreamCustomer; forbidden for OwnFunds and NotProvided. Identifies the ultimate underlying payer using a stable reference from the submitting customer's records."
          },
          "downstreamInstitutionId": {
            "type": [
              "null",
              "string"
            ],
            "description": "Required only for DownstreamCustomer; forbidden for other relationships. Must exactly match an active permitted institution reference in this customer's tenant-scoped policy. The server resolves its legal name and country; client-supplied approval does not grant permission.",
            "example": "reviewed-msb-01"
          },
          "ultimateBeneficiary": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PaymentTransparencyParty"
              }
            ],
            "description": "Optional additional party ultimately receiving the funds; forbidden for NotProvided. Supply all party fields when present. The existing bank beneficiary remains the legal bank account holder."
          }
        }
      },
      "PaymentTransparencySnapshot": {
        "required": [
          "schemaVersion",
          "capturedAt",
          "purpose",
          "relationship",
          "underlyingPayer",
          "downstreamInstitution",
          "ultimateBeneficiary",
          "policyVersion"
        ],
        "type": "object",
        "properties": {
          "schemaVersion": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Stored disclosure schema version. Version 1 is currently supported.",
            "format": "int32"
          },
          "capturedAt": {
            "type": "string",
            "description": "When the immutable disclosure was first captured, UTC ISO-8601. Retained across status changes, approvals and replay.",
            "format": "date-time",
            "example": "2026-09-10T12:00:00Z"
          },
          "purpose": {
            "type": "string",
            "description": "Original normalized payment purpose."
          },
          "relationship": {
            "type": "string",
            "description": "Original declared relationship; NotProvided means only a payment reason was collected. Not a verification or breach finding."
          },
          "underlyingPayer": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PaymentTransparencyParty"
              }
            ],
            "description": "Original normalized underlying payer, null for OwnFunds or NotProvided."
          },
          "downstreamInstitution": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/ApprovedPaymentInstitution"
              }
            ],
            "description": "Original permitted institution identity resolved by the server, null for other relationships. Later policy changes do not rewrite this evidence."
          },
          "ultimateBeneficiary": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PaymentTransparencyParty"
              }
            ],
            "description": "Original additional ultimate beneficiary, if declared."
          },
          "policyVersion": {
            "type": [
              "null",
              "string"
            ],
            "description": "Version of the customer policy at initial capture, null if no explicit policy existed. A retained approved batch snapshot keeps this original version while execution checks current permission.",
            "format": "uuid"
          }
        }
      },
      "ProblemDetails": {
        "required": [
          "title",
          "status"
        ],
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "A URI reference for the problem class. The framework default for the status (an RFC 9110 section link or `about:blank`) — not a stable discriminator.",
            "example": "https://tools.ietf.org/html/rfc9110#section-15.5.2"
          },
          "title": {
            "type": "string",
            "description": "Short human-readable summary, fixed per emitter: `Public API key authentication failed.`, `API key is not authorized for this request.`, `Too many requests.`, `Invalid idempotency key.`, `Idempotency-Key conflict.`, `Invalid tenant header.`, `Invalid correlation ID.`. Not a contract.",
            "example": "Too many requests."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status, repeated.",
            "format": "int32",
            "example": 429
          },
          "detail": {
            "type": [
              "null",
              "string"
            ],
            "description": "Human-readable explanation. On 401 it is deliberately one fixed sentence for every refusal that consulted a stored key. On 429 it is the only field that separates the authentication-failure throttle (`Too many failed authentication attempts.`) from a volume limit (`Request rate limit exceeded.`).",
            "example": "Request rate limit exceeded."
          },
          "instance": {
            "type": [
              "null",
              "string"
            ],
            "description": "Not set by this API; present only if a proxy adds it."
          },
          "code": {
            "type": [
              "null",
              "string"
            ],
            "description": "Machine-readable reason. Present on the authentication, rate-limit, idempotency and correlation-id problems: `api_key_authentication_failed` (401), `api_key_not_authorized` (403), `rate_limited` (429), `idempotency_key_invalid` (400), `idempotency_key_reused` and `idempotency_key_in_progress` (409), `correlation_id_invalid` (400). ABSENT on the `X-Tenant-Id` refusal, the model-binding 400 and the 500. Authentication codes are intentionally one per status: the granular reason (expired, revoked, IP-denied, …) is recorded on our side and never returned.",
            "example": "rate_limited"
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Only on the model-binding 400: request field name to the messages explaining why it could not be bound."
          },
          "traceId": {
            "type": "string",
            "description": "Framework trace identifier for the request. Prefer the `X-Correlation-Id` response header when quoting a request to support — it is present on every response, including successes."
          }
        },
        "description": "RFC 7807 problem body (`application/problem+json`) written by the shared middleware for 400, 401, 403, 409 (idempotency) and 429, and by the framework for the model-binding 400 and for 500. Branch on `status` and `code`; `title`, `detail` and `type` are for humans. The codes and their remedies are listed in `/guides/errors`."
      },
      "PublicApiAccountData": {
        "required": [
          "account"
        ],
        "type": "object",
        "properties": {
          "account": {
            "description": "The fiat account. On the create route this is the account request as accepted; read `status` — `Completed` means bank details are present, anything else means keep polling or wait for the `viban.ready` webhook.",
            "$ref": "#/components/schemas/PublicApiAccountResponse"
          }
        }
      },
      "PublicApiAccountListData": {
        "required": [
          "accounts",
          "pagination"
        ],
        "type": "object",
        "properties": {
          "accounts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiAccountResponse"
            },
            "description": "The customer's fiat accounts for this page, newest change first. Includes every status — Failed and Closed rows are not filtered out — so filter on `status` (the `status` query parameter does this server-side)."
          },
          "pagination": {
            "description": "Paging metadata for this list. `page` and `limit` (1-500) on the request are honoured; `total` counts every matching record across all pages, not the rows in this response, and `last_page` is `ceil(total / per_page)` (never 0). Loop `page` from 1 to `last_page`. See the Pagination guide.",
            "$ref": "#/components/schemas/PublicApiPaginationResponse"
          }
        }
      },
      "PublicApiAccountResponse": {
        "required": [
          "id",
          "customer_id",
          "currency",
          "account_type",
          "status",
          "submitted_at",
          "completed_at",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Kwiikpay's id for this fiat account. Stable from the moment the request is accepted, so it is the same id before and after the provider issues the bank details. Use it in `GET .../banking/accounts/{accountId}`.",
            "format": "uuid",
            "example": "019ed130-7a2c-7def-8a01-aabbccddeeff"
          },
          "customer_id": {
            "type": "string",
            "description": "The customer this account belongs to — the `customerId` from the request path.",
            "format": "uuid",
            "example": "019ecfeb-c15b-760f-9e2b-8e50ce653573"
          },
          "currency": {
            "enum": [
              "EUR",
              "GBP",
              "USD"
            ],
            "type": "string",
            "description": "Currency the account is denominated in. A customer holds at most ONE active account per currency, at any provider. Accepted values: `EUR`, `GBP`, `USD`.",
            "example": "GBP"
          },
          "account_type": {
            "enum": [
              "Account",
              "VirtualAccount"
            ],
            "type": "string",
            "description": "Kind of fiat account. A virtual account is a vIBAN or sort-code/account-number pair issued under a pooled provider account. Accepted values: `Account`, `VirtualAccount`.",
            "example": "VirtualAccount"
          },
          "status": {
            "enum": [
              "Pending",
              "PendingProvider",
              "Completed",
              "Failed",
              "ManualReview",
              "Returned",
              "Closed"
            ],
            "type": "string",
            "description": "Where the account request stands. `Completed` is the only state in which the account can receive deposits and fund withdrawals — `bank_details` is populated and `provider_account_id` is set. `Pending` means recorded but not yet placed with the provider; `PendingProvider` means the provider accepted the request and the bank details are still being issued (poll this route, or wait for the `viban.ready` webhook); `ManualReview` means an operator has to act before it can proceed (often missing provider prerequisites — see the create route); `Failed` and `Closed` are terminal and free the currency slot for a new request. `Returned` belongs to payments and is not expected on an account. Accepted values: `Pending`, `PendingProvider`, `Completed`, `Failed`, `ManualReview`, `Returned`, `Closed`.",
            "example": "Completed"
          },
          "submitted_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "When the request was handed to the banking provider, UTC ISO-8601. Null until then, and always null on the list route, whose read model does not carry it — read the account by id if you need it.",
            "format": "date-time",
            "example": "2026-06-17T10:44:58.000Z"
          },
          "completed_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "When the account reached `Completed`, UTC ISO-8601. Null for every other status.",
            "format": "date-time",
            "example": "2026-06-17T10:45:00.000Z"
          },
          "created_at": {
            "type": "string",
            "description": "When Kwiikpay recorded the account request, UTC ISO-8601.",
            "format": "date-time",
            "example": "2026-06-17T10:44:57.000Z"
          },
          "updated_at": {
            "type": "string",
            "description": "Last change to the account row, UTC ISO-8601. The list route sorts by this field, newest first.",
            "format": "date-time",
            "example": "2026-06-17T10:45:00.000Z"
          },
          "provider": {
            "type": [
              "null",
              "string"
            ],
            "description": "Provider actually holding this account. FIAT_REPUBLIC or OPENPAYD on current accounts; rows created before 2026-08-12 may still report the decommissioned BCB.",
            "example": "OPENPAYD"
          },
          "provider_account_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "The banking provider's own reference for this account. Null until the provider has accepted the request. Its presence is what makes the account eligible to hold a beneficiary and to fund a withdrawal, so treat null as \"not usable yet\".",
            "example": "a1b2c3d4"
          },
          "iban": {
            "type": [
              "null",
              "string"
            ],
            "description": "IBAN of the issued account. Populated once `status` is `Completed` and the provider issued an IBAN; null before that and on accounts identified by sort code and account number only.",
            "example": "GB29NWBK60161331926819"
          },
          "bic": {
            "type": [
              "null",
              "string"
            ],
            "description": "BIC/SWIFT of the bank holding the issued account. Populated alongside `iban`; null otherwise.",
            "example": "NWBKGB2L"
          },
          "account_number": {
            "type": [
              "null",
              "string"
            ],
            "description": "Local account number of the issued account — the UK 8-digit account number for a GBP account. Null on IBAN-only accounts and until the account is `Completed`.",
            "example": "31926819"
          },
          "sort_code": {
            "type": [
              "null",
              "string"
            ],
            "description": "UK sort code of the issued GBP account, six digits. Null on IBAN-only accounts and until the account is `Completed`.",
            "example": "601613"
          },
          "account_holder_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Name the receiving bank holds the account under — what a payer must put on their transfer. Null until issued, and null where the provider did not report it.",
            "example": "Jane Doe"
          },
          "bank_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Name of the bank the account is held at, as reported by the provider. May be null even on a `Completed` account when the provider did not report one.",
            "example": "Example Bank"
          },
          "account_country": {
            "type": [
              "null",
              "string"
            ],
            "description": "ISO 3166-1 alpha-2 country of the issued account's bank details, as reported by the provider. Null until issued.",
            "example": "GB"
          },
          "bank_details": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiBankDetailsResponse"
              }
            ],
            "description": "The same coordinates as the top-level `iban`, `bic`, `account_number`, `sort_code`, `account_holder_name`, `bank_name` and `account_country`, plus `raw`: every provider-reported bank-detail key verbatim. Prefer the typed fields; use `raw` only when a provider reports something the typed fields do not carry."
          }
        }
      },
      "PublicApiAllowedDestinationsData": {
        "required": [
          "currencies"
        ],
        "type": "object",
        "properties": {
          "currencies": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Asset codes the exchange is configured to deliver for your tenant — fiat currencies AND crypto assets, despite the name. Derived from the tenant's enabled exchange fee configuration, not from the fiat account currency list, so it can be EMPTY when no exchange fee is configured (that reads as \"exchange unavailable\"). When `source_currency` is supplied the list is further filtered to the pairs that trade from that source; without it, it is the unfiltered set, so a pair drawn from it can still be refused with 422 `invalid_pair`.",
            "example": [
              "USDC"
            ]
          }
        }
      },
      "PublicApiAssociatedPartyRequest": {
        "required": [
          "external_party_reference",
          "full_name",
          "roles"
        ],
        "type": "object",
        "properties": {
          "external_party_reference": {
            "type": [
              "null",
              "string"
            ],
            "description": "Your own identifier for this person, so their later verification status can be matched back to your records.",
            "example": "party-1"
          },
          "full_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Full legal name of the person.",
            "example": "Jane Director"
          },
          "roles": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string"
            },
            "description": "The capacities this person holds. At least one required; accepted values: ubo, director, authorized_representative. Accepted values: `ubo`, `director`, `authorized_representative`."
          },
          "ownership_percentage": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Ownership share of the company, for UBOs. 0-100 when supplied.",
            "format": "double",
            "example": "55"
          },
          "country_code": {
            "pattern": "^[A-Za-z]{2}$",
            "type": [
              "null",
              "string"
            ],
            "description": "ISO 3166-1 alpha-2 country of the person. Optional.",
            "example": "GB"
          }
        }
      },
      "PublicApiBalanceListData": {
        "required": [
          "balances"
        ],
        "type": "object",
        "properties": {
          "balances": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiBalanceResponse"
            },
            "description": "One row per fiat currency, sorted by currency. Empty when the customer holds no fiat account and no fiat ledger balance. Crypto balances are not included — read them from the crypto wallet routes."
          }
        }
      },
      "PublicApiBalanceResponse": {
        "required": [
          "currency",
          "balance_minor",
          "balance_major",
          "wallet_id",
          "funded_by_account_ids",
          "last_updated",
          "account_id"
        ],
        "type": "object",
        "properties": {
          "currency": {
            "enum": [
              "EUR",
              "GBP",
              "USD"
            ],
            "type": "string",
            "description": "Fiat currency of this balance row. One row per currency in which the customer holds a `Completed` account or a ledger balance. Accepted values: `EUR`, `GBP`, `USD`.",
            "example": "GBP"
          },
          "balance_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "AVAILABLE balance in minor units (2000 is GBP 20.00) — the funds that can be withdrawn or converted right now. Money that has arrived but is still clearing, or is locked by an open withdrawal or conversion, is NOT included. Summed across every ledger pot the customer holds in this currency. A currency with a `Completed` account and no movement yet is reported at 0 rather than omitted.",
            "format": "double",
            "example": "2000"
          },
          "balance_major": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The same available balance in major units (20.00). Derived from `balance_minor`; branch on the minor value when comparing.",
            "format": "double",
            "example": "20.00"
          },
          "wallet_id": {
            "type": "string",
            "description": "Internal ledger account id the balance is presented under. Presentational only: with several pots in one currency it names the pooled pot while the amount is the sum across all of them. Identical to `account_id`. Not a fiat account id and not accepted by any other route.",
            "format": "uuid",
            "example": "6f1c2a4e-2b7d-4c8e-9a10-1b2c3d4e5f60"
          },
          "funded_by_account_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Ids of the customer's fiat accounts in this currency (the `id` on the accounts route), in any status. Empty when the balance exists without an account row.",
            "example": [
              "019ed130-7a2c-7def-8a01-aabbccddeeff"
            ]
          },
          "last_updated": {
            "type": "string",
            "description": "When the balance last moved, UTC ISO-8601: the newest ledger entry in this currency, or — for a zero row with no entries — the account's own last change. It does not re-stamp on every read.",
            "format": "date-time",
            "example": "2026-06-17T11:46:49.000Z"
          },
          "account_id": {
            "type": "string",
            "description": "Same value as `wallet_id` — the presentational ledger account id. Not a fiat account id; use `funded_by_account_ids` for those.",
            "format": "uuid",
            "example": "6f1c2a4e-2b7d-4c8e-9a10-1b2c3d4e5f60"
          }
        }
      },
      "PublicApiBankDetailsResponse": {
        "required": [
          "iban",
          "bic",
          "account_number",
          "sort_code",
          "account_holder_name",
          "bank_name",
          "account_country",
          "raw"
        ],
        "type": "object",
        "properties": {
          "iban": {
            "type": [
              "null",
              "string"
            ],
            "description": "IBAN of the issued account, or null when the account is identified by sort code and account number.",
            "example": "GB29NWBK60161331926819"
          },
          "bic": {
            "type": [
              "null",
              "string"
            ],
            "description": "BIC/SWIFT of the bank holding the issued account, or null.",
            "example": "NWBKGB2L"
          },
          "account_number": {
            "type": [
              "null",
              "string"
            ],
            "description": "Local account number of the issued account, or null on an IBAN-only account.",
            "example": "31926819"
          },
          "sort_code": {
            "type": [
              "null",
              "string"
            ],
            "description": "UK sort code of the issued GBP account, or null.",
            "example": "601613"
          },
          "account_holder_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Name the receiving bank holds the account under. Null when the provider did not report it.",
            "example": "Jane Doe"
          },
          "bank_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Name of the bank the account is held at. Null when the provider did not report it.",
            "example": "Example Bank"
          },
          "account_country": {
            "type": [
              "null",
              "string"
            ],
            "description": "ISO 3166-1 alpha-2 country of the bank details. Null when the provider did not report it.",
            "example": "GB"
          },
          "raw": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Every bank-detail key the provider reported, verbatim and provider-specific (for example `bankDetails.iban`, `virtualAccountDetails.sortCode`). Keys differ per provider and can change; the typed fields above are derived from this map and are the stable contract."
          }
        }
      },
      "PublicApiBankingAccountFormSchemaData": {
        "required": [
          "supported_currencies",
          "supported_providers",
          "fields",
          "providers"
        ],
        "type": "object",
        "properties": {
          "supported_currencies": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Currencies the platform can open an account in for this tenant — the union across every provider in `providers`. Carries the placeholder `[\"GBP\"]` with an empty `providers` array when account opening is switched off for the tenant; do not read that as a real offering.",
            "example": [
              "GBP"
            ]
          },
          "supported_providers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The provider codes the create route will accept as an EXPLICIT `provider` value — today `FIAT_REPUBLIC` only, or empty when Fiat Republic account opening is disabled. It is a selector, not a capability list: OpenPayd issues accounts but is reached only by omitting `provider`, so it never appears here.",
            "example": [
              "FIAT_REPUBLIC"
            ]
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiFormFieldResponse"
            },
            "description": "The three inputs of the create form, in order: `currency` (required, options = `supported_currencies`), `provider` (optional, options = `supported_providers`) and `rail` (optional, options = the union of every provider's `supported_rails`). Each carries a `description` with the help text to show."
          },
          "providers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiBankingProviderSchemaResponse"
            },
            "description": "One entry per provider that can issue accounts for this tenant, with its own currency list, rail list and provider-specific inputs. Describes what the platform can do; whether you may NAME a provider is decided by `supported_providers`."
          }
        }
      },
      "PublicApiBankingProviderSchemaResponse": {
        "required": [
          "provider",
          "display_name",
          "supported_currencies",
          "supported_rails",
          "provider_input_values"
        ],
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "description": "Provider code this entry describes. Today only `FIAT_REPUBLIC` is advertised; an OpenPayd-issued account is reached by omitting `provider` on the create route and is never listed here.",
            "example": "FIAT_REPUBLIC"
          },
          "display_name": {
            "type": "string",
            "description": "Human-readable provider name for a dropdown label.",
            "example": "Fiat Republic"
          },
          "supported_currencies": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Currencies this provider can open an account in for this tenant. Falls back to `[\"GBP\"]` when the tenant's configured rows name no currency the platform offers.",
            "example": [
              "GBP"
            ]
          },
          "supported_rails": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "A FLAT UNION of the rails this provider offers across all of its `supported_currencies`, in Fiat Republic's scheme vocabulary — for the default advertisement that is ACH, ACH_SAME_DAY, BACS, CHAPS, DOMESTIC_WIRE, FPS, INTERNATIONAL_WIRE and SCT. It carries no per-currency mapping, so it must not be paired freely: EUR takes SCT only, GBP takes FPS, CHAPS or BACS, USD takes the four US schemes. See the Currencies and rails guide for the table.",
            "example": [
              "FPS"
            ]
          },
          "provider_input_values": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiFormFieldResponse"
            },
            "description": "Provider-specific inputs you may send in `provider_input_values` on the create route, keyed by the exact string in `name`. Populated for Fiat Republic only; an EMPTY list for any other provider means \"not described here\", not \"no input needed\"."
          }
        }
      },
      "PublicApiBeneficiaryBatchData": {
        "required": [
          "beneficiaries"
        ],
        "type": "object",
        "properties": {
          "beneficiaries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiBeneficiaryResponse"
            },
            "description": "ONE beneficiary per provider the customer holds an eligible account with in the currency — normally exactly one, but the route fans out, so do not assume `beneficiaries[0]` is the only row. Each has its own `id`. Creation is not atomic across providers: a 409 `beneficiary_creation_failed` naming the second provider can arrive after the first provider's beneficiary was already created and kept. Recover by LISTING, not by retrying under a new key."
          }
        }
      },
      "PublicApiBeneficiaryData": {
        "required": [
          "beneficiary"
        ],
        "type": "object",
        "properties": {
          "beneficiary": {
            "description": "One beneficiary. On the delete route this is the row AFTER disabling: `status` `Failed`, `provider_status` `LOCAL_DISABLED`.",
            "$ref": "#/components/schemas/PublicApiBeneficiaryResponse"
          }
        }
      },
      "PublicApiBeneficiaryListData": {
        "required": [
          "beneficiaries",
          "pagination"
        ],
        "type": "object",
        "properties": {
          "beneficiaries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiBeneficiaryResponse"
            },
            "description": "The customer's beneficiaries for this page, newest first, in EVERY status — deleted and superseded rows are included (`status` `Failed`, `provider_status` `LOCAL_DISABLED`). `approval_status` and OpenPayd `bank_registration` are populated; requested OpenPayd registrations include their saved `supported_schemes`."
          },
          "pagination": {
            "description": "Paging metadata for this list. `page` and `limit` (1-500) on the request are honoured; `total` counts every matching record across all pages, not the rows in this response, and `last_page` is `ceil(total / per_page)` (never 0). Loop `page` from 1 to `last_page`. See the Pagination guide.",
            "$ref": "#/components/schemas/PublicApiPaginationResponse"
          }
        }
      },
      "PublicApiBeneficiaryResponse": {
        "required": [
          "id",
          "customer_id",
          "provider",
          "currency",
          "status",
          "provider_reference",
          "provider_status",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Kwiikpay's id for this beneficiary — the value to send as `beneficiary_id` on a withdrawal. Where PATCH is permitted, it creates a NEW beneficiary with a new id; the id in the PATCH URL is superseded, not updated. OpenPayd rows whose bank registration has started refuse PATCH with 409 `beneficiary_registration_immutable`.",
            "format": "uuid",
            "example": "1c9f3e6a-5d2b-4f7e-8a90-0b1c2d3e4f50"
          },
          "customer_id": {
            "type": "string",
            "description": "The customer this beneficiary belongs to — the `customerId` from the request path.",
            "format": "uuid",
            "example": "019ecfeb-c15b-760f-9e2b-8e50ce653573"
          },
          "provider": {
            "type": "string",
            "description": "Provider actually holding this beneficiary. FIAT_REPUBLIC or OPENPAYD; rows created before 2026-08-12 may still report the decommissioned BCB, and those remain readable and payable.",
            "example": "OPENPAYD"
          },
          "currency": {
            "enum": [
              "EUR",
              "GBP",
              "USD"
            ],
            "type": "string",
            "description": "Currency the beneficiary can be paid in. A withdrawal must name the same currency or it is refused with 422 `beneficiary_currency_mismatch`. Accepted values: `EUR`, `GBP`, `USD`.",
            "example": "GBP"
          },
          "status": {
            "enum": [
              "Pending",
              "PendingProvider",
              "Completed",
              "Failed",
              "ManualReview",
              "Returned",
              "Closed"
            ],
            "type": "string",
            "description": "Provisioning state at the provider. HTTP 200 on create or update does NOT mean the beneficiary is usable — read this field. `Completed` with a non-null `provider_reference` is the only payable state (a withdrawal against anything else is refused with 409 `beneficiary_not_ready`). `PendingProvider` means the provider accepted it but has not confirmed it yet. `ManualReview` means the provider call failed or was skipped and an operator must act — the request returned 200 all the same; the reason is not returned, so re-check the details and create a new beneficiary under a NEW `Idempotency-Key` (replaying the same key returns the parked row). `Failed` with `provider_status` `LOCAL_DISABLED` is a beneficiary you deleted or superseded. `Pending`, `Returned` and `Closed` are part of the shared vocabulary but not expected here. Payability also needs `approval_status` = `Approved` and, for requested OpenPayd registration, `bank_registration.status` = `Registered`. OpenPayd local `Completed` alone does not prove a bank record exists. Accepted values: `Pending`, `PendingProvider`, `Completed`, `Failed`, `ManualReview`, `Returned`, `Closed`.",
            "example": "Completed"
          },
          "provider_reference": {
            "type": [
              "null",
              "string"
            ],
            "description": "Provider/local reference retained for compatibility. For OpenPayd this is the original local reference, not the saved bank-beneficiary ID; inspect `bank_registration`. Withdrawals use Kwiikpay's beneficiary `id`.",
            "example": "pye_01j8x2y3z4"
          },
          "provider_status": {
            "type": [
              "null",
              "string"
            ],
            "description": "The provider's own status for the payee, passed through unchanged — an OPEN vocabulary that differs per provider, plus one Kwiikpay literal: `LOCAL_DISABLED`, written when you delete or supersede a beneficiary. Informational only; branch on `status`, `provider_reference` and `approval_status`, never on this field.",
            "example": "ACTIVE"
          },
          "created_at": {
            "type": "string",
            "description": "When Kwiikpay recorded the beneficiary, UTC ISO-8601. The list route sorts by this field, newest first.",
            "format": "date-time",
            "example": "2026-06-17T11:46:49.000Z"
          },
          "updated_at": {
            "type": "string",
            "description": "Last change to the beneficiary row, UTC ISO-8601 — including a delete, which sets `status` to `Failed`.",
            "format": "date-time",
            "example": "2026-06-17T11:46:50.000Z"
          },
          "supported_schemes": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string"
            },
            "description": "Saved payment rail for an OpenPayd beneficiary whose bank registration has started; returned on write/replay, get, list and delete. Use this rail for payment; SEPA and SEPA_INSTANT are distinct, and FASTER_PAYMENTS maps to FPS. Fiat Republic USD pre-flight schemes remain present on create/update. Null means UNKNOWN, never no schemes.",
            "example": [
              "SEPA_INSTANT"
            ]
          },
          "approval_status": {
            "enum": [
              "PendingApproval",
              "Approved",
              "Rejected"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Kwiikpay's staff-approval state. OpenPayd bank registration is an additional readiness gate. Every new beneficiary starts `PendingApproval` and is cleared by Kwiikpay staff; a withdrawal before that is refused with 409 `beneficiary_not_approved`, and `Rejected` is final. A payee can be `status` = `Completed` and still unpayable here. Populated on EVERY response — get, list, and the create, update and delete writes — so the answer to \"can I pay this yet\" is always in front of you. Rows created before the approval gate existed report `Approved`. Accepted values: `PendingApproval`, `Approved`, `Rejected`.",
            "example": "PendingApproval"
          },
          "bank_registration": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/BankBeneficiaryRegistrationResponse"
              }
            ],
            "description": "OpenPayd bank registration, separate from staff approval and local provisioning. New approvals queue registration under the beneficiary owner's linked business (the child for parent-initiated sub-customer requests). Poll until Registered before withdrawal. NotRegistered preserves the historical inline flow until staff queue backfill. Other providers return null. Customer responses exclude bank IDs and staff details."
          },
          "display_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Customer-safe display name captured at creation (company name for CORPORATE, first + last name for RETAIL), or recovered from the saved OpenPayd attributes for legacy rows. Null when neither is available — never fall back to a UUID to identify a payee.",
            "example": "Recipient Trading Ltd"
          },
          "masked_account_identifier": {
            "type": [
              "null",
              "string"
            ],
            "description": "The IBAN or account number masked: only the last 4 characters are revealed for identifiers shorter than 12 characters (account numbers); longer identifiers such as IBANs also keep their first 4. The full identifier is never returned on any customer- or partner-facing response.",
            "example": "DE89**************3000"
          },
          "beneficiary_type": {
            "enum": [
              "RETAIL",
              "CORPORATE"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "RETAIL or CORPORATE when known from the saved OpenPayd attributes; null for other providers or when not recorded. Accepted values: `RETAIL`, `CORPORATE`.",
            "example": "CORPORATE"
          },
          "country": {
            "type": [
              "null",
              "string"
            ],
            "description": "Two-letter recipient country: the saved `beneficiary_country` attribute, falling back to `address.country`. Null when neither is present.",
            "example": "DE"
          },
          "payment_rail": {
            "type": [
              "null",
              "string"
            ],
            "description": "The saved payment rail attribute (`paymentType`), upper-cased — SEPA, SEPA_INSTANT, FASTER_PAYMENTS or CHAPS for OpenPayd. Null for other providers or before a rail was recorded. See also `supported_schemes`, which reflects what the PROVIDER confirmed rather than what was saved.",
            "example": "SEPA"
          },
          "superseded_by_beneficiary_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "The id of the beneficiary that REPLACED this one — set only when this beneficiary is retired or otherwise no longer the active payee (for example after a staff correction, a recovery, or a self-service/partner replace). Null for the active payee. When set alongside `bank_registration.status` `NeedsReview`, `bank_registration.reason` names this same replacement instead of the generic \"contact support\" text — use the replacement's id, not this one, for withdrawals and further polling.",
            "format": "uuid"
          }
        }
      },
      "PublicApiBrandingResponse": {
        "required": [
          "brand_name",
          "primary_color",
          "logo_url",
          "favicon_url"
        ],
        "type": "object",
        "properties": {
          "brand_name": {
            "type": "string",
            "description": "The tenant's display name — the same value as `partner_name`.",
            "example": "Acme Payments Ltd"
          },
          "primary_color": {
            "type": "string",
            "description": "Always `#6366f1` today: a fixed platform default, not a per-tenant setting. Do not read a tenant's brand colour from it.",
            "example": "#6366f1"
          },
          "logo_url": {
            "type": [
              "null",
              "string"
            ],
            "description": "Always null today. Tenant logos are not served through this API."
          },
          "favicon_url": {
            "type": [
              "null",
              "string"
            ],
            "description": "Always null today. Tenant favicons are not served through this API."
          }
        }
      },
      "PublicApiBusinessAddressRequest": {
        "required": [
          "country_code"
        ],
        "type": "object",
        "properties": {
          "country_code": {
            "pattern": "^[A-Za-z]{2}$",
            "type": [
              "null",
              "string"
            ],
            "description": "ISO 3166-1 alpha-2 country of the address. Required.",
            "example": "GB"
          },
          "region": {
            "type": [
              "null",
              "string"
            ],
            "description": "State, region or county.",
            "example": "Greater Manchester"
          },
          "city": {
            "type": [
              "null",
              "string"
            ],
            "description": "Town or city.",
            "example": "Manchester"
          },
          "postal_code": {
            "type": [
              "null",
              "string"
            ],
            "description": "Postal code.",
            "example": "M1 1AA"
          },
          "address_line1": {
            "type": [
              "null",
              "string"
            ],
            "description": "First address line (building and street).",
            "example": "1 High Street"
          },
          "address_line2": {
            "type": [
              "null",
              "string"
            ],
            "description": "Second address line, if any.",
            "example": "Northern Quarter"
          }
        }
      },
      "PublicApiBusinessProfileRequest": {
        "required": [
          "company_name",
          "registration_number",
          "country_code",
          "registered_address",
          "associated_parties"
        ],
        "type": "object",
        "properties": {
          "company_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Registered legal name of the company.",
            "example": "Acme Trading Ltd"
          },
          "registration_number": {
            "type": [
              "null",
              "string"
            ],
            "description": "Company registration number.",
            "example": "12345678"
          },
          "country_code": {
            "pattern": "^[A-Za-z]{2}$",
            "type": [
              "null",
              "string"
            ],
            "description": "ISO 3166-1 alpha-2 country of registration.",
            "example": "GB"
          },
          "registered_address": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiBusinessAddressRequest"
              }
            ],
            "description": "Registered address of the company. Its country_code is required."
          },
          "associated_parties": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/PublicApiAssociatedPartyRequest"
            },
            "description": "The people behind the company: at least one UBO, director or authorized representative is required for KYB."
          }
        }
      },
      "PublicApiCommercialConfigData": {
        "required": [
          "fees"
        ],
        "type": "object",
        "properties": {
          "fees": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiFeeConfigurationResponse"
            },
            "description": "Every fee row currently in force for the tenant as a whole: enabled, effective now, and not customer-specific (rows negotiated for one customer are excluded here — read `GET customers/{customerId}/fees` for those). Both customer types are included; filter on `customer_type`. Ordered by `fee_type`, then `asset_code`, then `customer_type`. Rows are versioned per fee dimension and the newest effective version wins, so you get one row per dimension — but the dimension includes rail/network, geography and product, which this object does not expose, so two rows can look identical apart from `id` (for example USDT on Ethereum and USDT on Tron). Not paged."
          }
        }
      },
      "PublicApiConfigData": {
        "required": [
          "tenant_id",
          "partner_name",
          "domain",
          "branding",
          "public_key",
          "api_base_url",
          "features",
          "support"
        ],
        "type": "object",
        "properties": {
          "tenant_id": {
            "type": "string",
            "description": "Tenant (partner) id the key belongs to.",
            "format": "uuid",
            "example": "0198f3a0-0000-7000-8000-00000000c0de"
          },
          "partner_name": {
            "type": "string",
            "description": "Display name of the tenant as configured on the platform. Also returned as `branding.brand_name`.",
            "example": "Acme Payments Ltd"
          },
          "domain": {
            "type": "string",
            "description": "The host you reached this API on (the request's Host header, for example `api.kwiikpay.io`), falling back to the tenant's slug when no host was sent. It is NOT a partner-owned white-label domain, and it does not change per tenant.",
            "example": "api.kwiikpay.io"
          },
          "branding": {
            "description": "Branding block. Today only `brand_name` carries tenant data; `primary_color` is a fixed default and `logo_url`/`favicon_url` are always null — see the field descriptions.",
            "$ref": "#/components/schemas/PublicApiBrandingResponse"
          },
          "public_key": {
            "type": [
              "null",
              "string"
            ],
            "description": "The PREFIX of the API key you authenticated with (the part before the secret, for example `kp_live_a1b2c3d4`), or null when the key carries no prefix. It identifies the key in dashboards and logs. It is NOT a signing key, not a webhook secret and not a key you can hand to a browser. Webhook signature secrets are issued per subscription, not here.",
            "example": "kp_live_a1b2c3d4"
          },
          "api_base_url": {
            "type": "string",
            "description": "Base URL for the v1 partner routes, built from the scheme and host of this request: `{scheme}://{host}/api/v1/partner`. Note this route itself lives under `/api/public/partner`, not under the base it returns.",
            "example": "https://api.kwiikpay.io/api/v1/partner"
          },
          "features": {
            "description": "Which product families are enabled for this tenant, derived from its enabled provider capabilities. A flag being true means the platform has a provider configured for that product — not that a given customer has an account or wallet yet.",
            "$ref": "#/components/schemas/PublicApiFeaturesResponse"
          },
          "support": {
            "description": "Support contact block. `email` is always null today; there is no per-tenant support contact configured through this API.",
            "$ref": "#/components/schemas/PublicApiSupportResponse"
          }
        }
      },
      "PublicApiConversionRequestCreateRequest": {
        "required": [
          "from_currency",
          "to_currency",
          "amount_minor"
        ],
        "type": "object",
        "properties": {
          "from_currency": {
            "enum": [
              "GBP",
              "USD",
              "EUR",
              "BTC",
              "ETH",
              "USDC",
              "USDT"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Asset to sell, from the customer's balance. Not every pair of listed assets trades: the source and destination must differ, and a BTC or ETH source can only be converted to a fiat currency (BTC→USDC, BTC→ETH, ETH→USDT and the like are refused). A disallowed pair is refused with 422 `invalid_pair` (\"Pair must use different supported assets. BTC/ETH source trades are only enabled to fiat targets.\"). The customer must already hold a `Completed` fiat account (fiat source) or an active wallet (crypto source) for this asset — 409 `exchange_account_missing` / `exchange_wallet_missing` otherwise. A fiat source or USDC/USDT source has a 20 major-unit minimum (422 `below_minimum_trade_size`). Accepted values: `GBP`, `USD`, `EUR`, `BTC`, `ETH`, `USDC`, `USDT`.",
            "example": "GBP"
          },
          "to_currency": {
            "enum": [
              "GBP",
              "USD",
              "EUR",
              "BTC",
              "ETH",
              "USDC",
              "USDT"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Asset to buy. Must differ from `from_currency`; when the source is BTC or ETH this must be a fiat currency (GBP, USD or EUR) — anything else is refused with 422 `invalid_pair`. USDC, USDT and fiat sources may target any other listed asset. The customer must already hold the account or wallet for this asset — 409 `exchange_account_missing` / `exchange_wallet_missing` otherwise. Accepted values: `GBP`, `USD`, `EUR`, `BTC`, `ETH`, `USDC`, `USDT`.",
            "example": "USDC"
          },
          "amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Public API atomic/minor amount. GBP/EUR/USD use 2 decimals, USDC/USDT use 6, BTC uses 8, ETH uses 18. A conversion has a 20 major-unit floor when the source is GBP, USD, EUR, USDC or USDT, so the smallest accepted GBP conversion is 2000.",
            "format": "double",
            "example": "2000"
          }
        }
      },
      "PublicApiConversionRequestData": {
        "required": [
          "conversion_request"
        ],
        "type": "object",
        "properties": {
          "conversion_request": {
            "description": "The conversion as recorded. On the create route its `status` is `Submitted` (or `Reserved`) — read `destination_amount_minor` as the expected amount, and treat the conversion as complete only once `status` is `Settled`.",
            "$ref": "#/components/schemas/PublicApiConversionRequestResponse"
          }
        }
      },
      "PublicApiConversionRequestListData": {
        "required": [
          "conversion_requests",
          "pagination"
        ],
        "type": "object",
        "properties": {
          "conversion_requests": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiConversionRequestResponse"
            },
            "description": "The customer's conversions for this page, newest first, in every status."
          },
          "pagination": {
            "description": "Paging metadata for this list. `page` and `limit` (1-500) on the request are honoured; `total` counts every matching record across all pages, not the rows in this response, and `last_page` is `ceil(total / per_page)` (never 0). Loop `page` from 1 to `last_page`. See the Pagination guide.",
            "$ref": "#/components/schemas/PublicApiPaginationResponse"
          }
        }
      },
      "PublicApiConversionRequestResponse": {
        "required": [
          "id",
          "status",
          "from_currency",
          "to_currency",
          "amount_minor",
          "executed_rate",
          "destination_amount_minor",
          "fee_amount_minor",
          "fee_rate",
          "message",
          "created_at",
          "completed_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Kwiikpay's id for the conversion (the exchange order). Use it in `GET .../banking/conversion-requests/{conversionRequestId}` and match it against the `conversion.completed` webhook.",
            "format": "uuid",
            "example": "3b1e5d7f-9a2c-4e6b-8d0f-1a2b3c4d5e6f"
          },
          "status": {
            "enum": [
              "Reserved",
              "Submitted",
              "Settled",
              "Failed",
              "Released",
              "Parked",
              "Submitting"
            ],
            "type": "string",
            "description": "Order status. `Reserved` — funds held, not yet placed; `Submitting` / `Submitted` — placed with the liquidity provider, settlement follows provider finality; `Settled` — the destination balance is credited (`completed_at` is set); `Failed` and `Released` — the order did not execute and the held funds were returned; `Parked` — an ambiguous provider outcome, funds stay held until an operator reconciles it. The create route returns 200 with `Submitted` (or `Reserved`) — the conversion is not finished when the call returns; poll this resource or subscribe to `conversion.completed`. Accepted values: `Reserved`, `Submitted`, `Settled`, `Failed`, `Released`, `Parked`, `Submitting`.",
            "example": "Submitted"
          },
          "from_currency": {
            "enum": [
              "GBP",
              "USD",
              "EUR",
              "BTC",
              "ETH",
              "USDC",
              "USDT",
              "TRX"
            ],
            "type": "string",
            "description": "Public API source currency or asset for exchange. Accepted values: `GBP`, `USD`, `EUR`, `BTC`, `ETH`, `USDC`, `USDT`, `TRX`.",
            "example": "GBP"
          },
          "to_currency": {
            "enum": [
              "GBP",
              "USD",
              "EUR",
              "BTC",
              "ETH",
              "USDC",
              "USDT",
              "TRX"
            ],
            "type": "string",
            "description": "Public API destination currency or asset for exchange. Accepted values: `GBP`, `USD`, `EUR`, `BTC`, `ETH`, `USDC`, `USDT`, `TRX`.",
            "example": "USDC"
          },
          "amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Public API atomic/minor amount. GBP/EUR/USD use 2 decimals, USDC/USDT use 6, BTC uses 8, ETH uses 18. A conversion has a 20 major-unit floor when the source is GBP, USD, EUR, USDC or USDT, so the smallest accepted GBP conversion is 2000.",
            "format": "double",
            "example": "2000"
          },
          "executed_rate": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Destination major units per ONE source major unit, computed from `amount_minor` and `destination_amount_minor` on this row (for example GBP→USDC 1.26). Null when the destination amount is not yet known or the source amount is zero. Derived from the amounts — it is not a separately quoted market rate.",
            "format": "double",
            "example": "1.2634"
          },
          "destination_amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Public API destination amount in atomic/minor units.",
            "format": "double",
            "example": "24500000"
          },
          "fee_amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Public API fee amount in atomic/minor units.",
            "format": "double",
            "example": "50"
          },
          "fee_rate": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Public API fee divided by source amount when both use the same asset/scale.",
            "format": "double",
            "example": "0.005"
          },
          "message": {
            "type": [
              "null",
              "string"
            ],
            "description": "Human-readable note. While `Submitted` it is a fixed sentence saying the order awaits provider finality; on a `Failed`, `Released` or `Parked` order it carries the provider's or platform's reason when one was recorded; otherwise null. Not a machine code — branch on `status`.",
            "example": "Exchange order submitted to Hercle; settlement completes after provider finality."
          },
          "created_at": {
            "type": "string",
            "description": "When the order was created, UTC ISO-8601 (on the list and get routes, the time the quote it executed against was taken). The list route sorts by creation time, newest first.",
            "format": "date-time",
            "example": "2026-06-17T11:46:49.000Z"
          },
          "completed_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "When the order settled and the destination balance became available, UTC ISO-8601. Null until `status` is `Settled`.",
            "format": "date-time",
            "example": "2026-06-17T11:52:10.000Z"
          }
        }
      },
      "PublicApiCreateBankingAccountRequest": {
        "type": "object",
        "properties": {
          "currency": {
            "enum": [
              "GBP",
              "USD",
              "EUR"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Fiat currency code. Fiat accounts are supported in GBP, EUR and USD. Either currency or currency_code may be sent; currency_code wins when both are present, and a value outside the set is refused with 422 unsupported_currency. Accepted values: `GBP`, `USD`, `EUR`.",
            "example": "GBP"
          },
          "currency_code": {
            "enum": [
              "GBP",
              "USD",
              "EUR"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Public API fiat currency code. Fiat accounts are supported in GBP, EUR and USD. Either currency or currency_code may be sent; currency_code wins when both are present, and a value outside the set is refused with 422 unsupported_currency. Accepted values: `GBP`, `USD`, `EUR`.",
            "example": "GBP"
          },
          "rail": {
            "type": [
              "null",
              "string"
            ],
            "description": "Provider-ROUTING selector for issuing the account — not a payment scheme. Matched case-insensitively against the tenant's routing rules and capabilities, where a rule with no rail configured acts as a wildcard, then stored on the account. It is never validated against any rail allow-list and the account adapters ignore it. Omitting it is NOT the same as sending no rail: an omitted value is replaced by the currency's Fiat Republic default (FPS for GBP, SCT for EUR, ACH for USD) BEFORE routing matches, so a routing rule scoped to an OpenPayd rail code (FASTER_PAYMENTS, SEPA, SEPA_INSTANT) can only be reached by sending that code explicitly.",
            "example": "FPS"
          },
          "provider": {
            "enum": [
              "FIAT_REPUBLIC"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Provider to issue the account with. FIAT_REPUBLIC is the only value this endpoint accepts, with the aliases FIATREPUBLIC, FIAT-REPUBLIC and FR. Anything else, INCLUDING OPENPAYD, is refused with 422 unsupported_provider — omitting the field entirely is the only way to be routed to OpenPayd. provider, provider_code and banking_provider are three names for this one field. Accepted values: `FIAT_REPUBLIC`.",
            "example": "FIAT_REPUBLIC"
          },
          "provider_code": {
            "enum": [
              "FIAT_REPUBLIC"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Alias of provider on this request. FIAT_REPUBLIC only; OPENPAYD is refused. Omit the field to let routing choose. Accepted values: `FIAT_REPUBLIC`.",
            "example": "FIAT_REPUBLIC"
          },
          "banking_provider": {
            "enum": [
              "FIAT_REPUBLIC"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Public API banking provider selector. Accepted values: `FIAT_REPUBLIC`.",
            "example": "FIAT_REPUBLIC"
          },
          "provider_input_values": {
            "type": [
              "null",
              "object"
            ],
            "additionalProperties": {
              "type": "string"
            },
            "description": "Provider-specific inputs, keyed by the dotted names the form-schema route lists under `providers[].provider_input_values[].name` (for example `person.dob`, `business.companyName`, `sector`). Blank keys and blank values are dropped. Only the Fiat Republic path reads them; a key the provider does not read is never refused. If a business prerequisite such as `sector` is missing the request is not refused — it is accepted and PARKED (`status: ManualReview` or 422 `missing_provider_prerequisites`), so send what the schema names."
          }
        }
      },
      "PublicApiCreateBeneficiaryRequest": {
        "type": "object",
        "properties": {
          "currency": {
            "enum": [
              "GBP",
              "USD",
              "EUR"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Fiat currency code. Fiat accounts are supported in GBP, EUR and USD. Either currency or currency_code may be sent; currency_code wins when both are present, and a value outside the set is refused with 422 unsupported_currency. Accepted values: `GBP`, `USD`, `EUR`.",
            "example": "GBP"
          },
          "currency_code": {
            "enum": [
              "GBP",
              "USD",
              "EUR"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Public API fiat currency code. Fiat accounts are supported in GBP, EUR and USD. Either currency or currency_code may be sent; currency_code wins when both are present, and a value outside the set is refused with 422 unsupported_currency. Accepted values: `GBP`, `USD`, `EUR`.",
            "example": "GBP"
          },
          "rail": {
            "type": [
              "null",
              "string"
            ],
            "description": "Rail hint. This route does NOT validate it against the scheme policy. It has two uses: on USD it selects the payee shape and routing-code type (DOMESTIC_WIRE, WIRE or FEDWIRE give a WIRE routing code, anything else gives ABA), and it acts as a provider-routing criterion. For GBP and EUR, SWIFT explicitly selects OpenPayd international beneficiary validation and requires an enabled currency and destination; other values keep their existing routing behavior. To register a USD beneficiary for several rails use the schemes array, which is validated.",
            "example": "ACH"
          },
          "provider": {
            "enum": [
              "FIAT_REPUBLIC",
              "OPENPAYD"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Provider to register the beneficiary with. FIAT_REPUBLIC and OPENPAYD are both accepted; which one a given customer may use depends on the accounts they hold in that currency, so a provider they have no eligible account with is refused with 409 bank_account_required. BCB was decommissioned on 2026-08-12 and is no longer accepted. Omit the field to let the platform choose from the customer's eligible accounts. provider and provider_code are two names for this one field. Accepted values: `FIAT_REPUBLIC`, `OPENPAYD`.",
            "example": "OPENPAYD"
          },
          "provider_code": {
            "enum": [
              "FIAT_REPUBLIC",
              "OPENPAYD"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Alias of provider on this request. FIAT_REPUBLIC and OPENPAYD are both accepted; omit to let the platform choose. Accepted values: `FIAT_REPUBLIC`, `OPENPAYD`.",
            "example": "OPENPAYD"
          },
          "type": {
            "enum": [
              "PERSON",
              "BUSINESS"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Beneficiary party type, uppercase and exact. Optional — when omitted it is derived from the customer record, so a business subject becomes BUSINESS. Note that a value supplied in `attributes[\"type\"]` takes precedence over this field. For OpenPayd company-name mapping, explicitly send BUSINESS (or COMPANY / CORPORATE, case-insensitive) and a nonblank name; the customer's default type never classifies an external payee. Accepted values: `PERSON`, `BUSINESS`.",
            "example": "PERSON"
          },
          "name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Beneficiary account holder name, as the receiving bank holds it. The generic name defaults to the CUSTOMER's display name when omitted, so always send the payee's own name. For OpenPayd, an explicit business `type` plus this nonblank name supplies the company name unless provider identity attributes are already present; customer defaults never supply OpenPayd identity. Personal first and last names must be sent separately in `attributes`.",
            "example": "Acme LLC"
          },
          "iban": {
            "type": [
              "null",
              "string"
            ],
            "description": "Beneficiary IBAN. EUR SEPA routes use the IBAN directly. OpenPayd GBP Faster Payments/CHAPS also accept a valid UK IBAN: its format/checksum are checked and its sort code/account number extracted. Any explicit provider local bank details must match it; invalid, non-GB or conflicting domestic GBP instructions return 422 beneficiary_details_incomplete. This never selects SWIFT automatically. USD: supply exactly ONE of iban or account_number; INTERNATIONAL_WIRE accepts either, and a supplied IBAN's country prefix must agree with the receiving bank's country.",
            "example": "GB29NWBK60161331926819"
          },
          "bic": {
            "type": [
              "null",
              "string"
            ],
            "description": "Beneficiary bank BIC/SWIFT. Required for INTERNATIONAL_WIRE.",
            "example": "NWBKGB2L"
          },
          "account_number": {
            "type": [
              "null",
              "string"
            ],
            "description": "Beneficiary account number. USD: supply exactly ONE of account_number or iban; ACH, ACH_SAME_DAY and DOMESTIC_WIRE require account_number. OpenPayd GBP Faster Payments/CHAPS require sort_code and account_number unless a valid UK IBAN supplies them; any explicit local coordinates must match a supplied IBAN. EUR SEPA routes use iban.",
            "example": "000123456789"
          },
          "sort_code": {
            "type": [
              "null",
              "string"
            ],
            "description": "GBP: the six-digit UK sort code, sent with account_number. USD: this is NOT a sort code — it is the legacy single routing value, typed automatically as ABA (or WIRE when the only scheme is DOMESTIC_WIRE), so a six-digit UK sort code on a USD body is refused with \"a ABA routing number must be exactly 9 digits\". USD callers should use aba_routing_number or wire_routing_number, or set routing_code_type explicitly. Precedence: routing codes are collected in the order sort_code, aba_routing_number, wire_routing_number and DE-DUPLICATED BY TYPE, first wins — so a sort_code that resolves to type ABA silently DISCARDS an explicit aba_routing_number. Do not send both.",
            "example": "040075"
          },
          "address_line1": {
            "type": [
              "null",
              "string"
            ],
            "description": "Beneficiary street address. Required for EVERY currency, but NOT pre-checked by Kwiikpay: a payee sent without it is accepted with HTTP 200 and parked at `status` ManualReview — the provider adapter refuses inside the workflow and the refusal is swallowed. It does NOT surface as a 409 or 422.",
            "example": "1 Example Street"
          },
          "address_line2": {
            "type": [
              "null",
              "string"
            ],
            "description": "Optional second address line.",
            "example": "Floor 3"
          },
          "city": {
            "type": [
              "null",
              "string"
            ],
            "description": "Beneficiary city. Required for every currency, but not pre-checked: omitting it returns HTTP 200 with the payee parked at `status` ManualReview, never a 4xx.",
            "example": "London"
          },
          "state": {
            "type": [
              "null",
              "string"
            ],
            "description": "Beneficiary state or province. Optional in general, but REQUIRED on a USD payee whose country is US or CA. Note this is driven by the beneficiary's address country, not by the currency.",
            "example": "NY"
          },
          "postal_code": {
            "type": [
              "null",
              "string"
            ],
            "description": "Beneficiary postal or ZIP code. Required for every currency, but not pre-checked: omitting it returns HTTP 200 with the payee parked at `status` ManualReview, never a 4xx.",
            "example": "EC1A 1BB"
          },
          "country": {
            "type": [
              "null",
              "string"
            ],
            "description": "ISO 3166-1 country of the BENEFICIARY's address (alpha-2 canonical; alpha-3 and `UK` are accepted and converted). USD: required — refused with 422 `beneficiary_details_incomplete` when missing, and the beneficiary's own country is never guessed. GBP and EUR: optional, and SILENTLY DEFAULTED when omitted — first to the customer's own country, then to `GB` — so a UK customer's German payee is registered with a GB address unless you send this field. Send it for every currency.",
            "example": "GB"
          },
          "attributes": {
            "type": [
              "null",
              "object"
            ],
            "additionalProperties": {
              "type": "string"
            },
            "description": "Provider attributes sent with the payee, as flat string key/values. NOT free-form metadata: this map is applied FIRST and a key already present is never overwritten, so any key you put here SHADOWS the typed field that would have produced it — `name`, `type`, `address.line1`, `address.city`, `address.postalCode`, `address.country`, `bankDetails.iban`, `bankDetails.accountNumber`, `bankDetails.bic`, `bankDetails.routingCodes[0].value`, and the rest. The USD shape check runs on typed fields; OpenPayd identity and GBP domestic IBAN checks use the effective provider attributes. Do not put secrets here. OpenPayd requires `beneficiary_first_name` and `beneficiary_last_name` for a person, or `beneficiary_company_name` for a company. An explicit business `type` plus nonblank `name` also supplies the company name (including explicit generic attribute overrides); customer defaults never do. Explicit provider name keys, even blank or partial ones, and an explicit `beneficiary_type` remain authoritative; `RETAIL` is never converted to a company. Missing or invalid identity is refused with 422 `beneficiary_details_incomplete` on every currency. For GBP FPS/CHAPS, a supplied `beneficiary_iban` must be a valid UK IBAN; its sort code and account number are extracted. Explicit `beneficiary_sort_code` / `beneficiary_account_number` values must agree with it, including refusal of blanks."
          },
          "schemes": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string"
            },
            "description": "USD ONLY — silently ignored for GBP and EUR. The payment schemes to register this payee for: ACH, ACH_SAME_DAY, DOMESTIC_WIRE, INTERNATIONAL_WIRE. Case, hyphens and spaces are tolerated, and the aliases ACH_SAMEDAY, WIRE, FEDWIRE and SWIFT map to ACH_SAME_DAY, DOMESTIC_WIRE, DOMESTIC_WIRE and INTERNATIONAL_WIRE respectively. Unrecognised values are refused with 422 unsupported_payment_scheme. When omitted it is derived from `rail`, defaulting to [ACH] if that names no USD scheme. This — not `rail` — is how you register a USD payee for several schemes.",
            "example": [
              "ACH"
            ]
          },
          "account_type": {
            "enum": [
              "CHECKING",
              "SAVINGS"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "US bank account type, uppercased before use. REQUIRED for a USD beneficiary on ACH or ACH_SAME_DAY, and validated when supplied on DOMESTIC_WIRE or INTERNATIONAL_WIRE. Accepted on GBP and EUR but never validated and never transmitted. Accepted values: `CHECKING`, `SAVINGS`.",
            "example": "CHECKING"
          },
          "routing_code_type": {
            "enum": [
              "ABA",
              "BSB",
              "NCC",
              "SORT_CODE",
              "WIRE"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Type of the routing code supplied. Accepted: ABA, BSB, NCC, SORT_CODE, WIRE (case-insensitive). Optional — defaults to SORT_CODE for GBP and EUR, and for USD to WIRE when the only resolved scheme is DOMESTIC_WIRE, otherwise ABA. Accepted values: `ABA`, `BSB`, `NCC`, `SORT_CODE`, `WIRE`.",
            "example": "ABA"
          },
          "aba_routing_number": {
            "type": [
              "null",
              "string"
            ],
            "description": "US ABA routing number. Exactly 9 digits AND must pass the ABA check-digit test — a 9-digit value that fails the checksum is refused. Required for ACH and ACH_SAME_DAY, and for INTERNATIONAL_WIRE at least one routing code must be ABA-typed. At most two routing codes may be supplied in total.",
            "example": "021000021"
          },
          "wire_routing_number": {
            "type": [
              "null",
              "string"
            ],
            "description": "US wire routing number. Exactly 9 digits and must pass the ABA check-digit test. Required for DOMESTIC_WIRE. Never valid as the ABA-typed code that INTERNATIONAL_WIRE requires.",
            "example": "021000021"
          },
          "bank_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Receiving bank name. Required for DOMESTIC_WIRE and INTERNATIONAL_WIRE.",
            "example": "Example Bank"
          },
          "bank_address_line1": {
            "type": [
              "null",
              "string"
            ],
            "description": "Receiving bank street address. Required for INTERNATIONAL_WIRE.",
            "example": "500 Example Avenue"
          },
          "bank_address_city": {
            "type": [
              "null",
              "string"
            ],
            "description": "Receiving bank city. Required for INTERNATIONAL_WIRE.",
            "example": "New York"
          },
          "bank_address_postal_code": {
            "type": [
              "null",
              "string"
            ],
            "description": "Receiving bank postal or ZIP code. Required for INTERNATIONAL_WIRE.",
            "example": "10001"
          },
          "bank_country": {
            "type": [
              "null",
              "string"
            ],
            "description": "Country of the receiving bank. Required for DOMESTIC_WIRE and INTERNATIONAL_WIRE unless bank_address_country is supplied. When an IBAN is present its first two characters must agree with this country, or with a territory of it — GB accepts JE, GG and IM; FR accepts its overseas territories; FI accepts AX; NO accepts SJ.",
            "example": "US"
          },
          "bank_address_country": {
            "type": [
              "null",
              "string"
            ],
            "description": "Country of the receiving bank's address. Satisfies the bank_country requirement when that is omitted, and is subject to the same IBAN-prefix agreement rule.",
            "example": "US"
          },
          "intermediary_bic_swift": {
            "type": [
              "null",
              "string"
            ],
            "description": "Intermediary bank BIC/SWIFT. The whole intermediary block is optional — but supplying THIS field makes intermediary_bank_name, _address_line1, _address_city, _address_postal_code and _address_country all required.",
            "example": "EXAMPUS33"
          },
          "intermediary_bank_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Intermediary bank name. Required once intermediary_bic_swift is supplied.",
            "example": "Example Intermediary Bank"
          },
          "intermediary_bank_address_line1": {
            "type": [
              "null",
              "string"
            ],
            "description": "Intermediary bank street address. Required once intermediary_bic_swift is supplied.",
            "example": "200 Example Road"
          },
          "intermediary_bank_address_city": {
            "type": [
              "null",
              "string"
            ],
            "description": "Intermediary bank city. Required once intermediary_bic_swift is supplied.",
            "example": "New York"
          },
          "intermediary_bank_address_postal_code": {
            "type": [
              "null",
              "string"
            ],
            "description": "Intermediary bank postal or ZIP code. Required once intermediary_bic_swift is supplied.",
            "example": "10002"
          },
          "intermediary_bank_address_country": {
            "type": [
              "null",
              "string"
            ],
            "description": "Intermediary bank country. Required once intermediary_bic_swift is supplied.",
            "example": "US"
          }
        }
      },
      "PublicApiCreateCryptoWithdrawalAddressRequest": {
        "required": [
          "asset",
          "network",
          "address",
          "address_tag",
          "label"
        ],
        "type": "object",
        "properties": {
          "asset": {
            "enum": [
              "BTC",
              "ETH",
              "USDC",
              "USDT",
              "TRX"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Crypto asset code. Accepted values: `BTC`, `ETH`, `USDC`, `USDT`, `TRX`.",
            "example": "USDC"
          },
          "network": {
            "enum": [
              "BITCOIN",
              "ETHEREUM",
              "TRON"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Network to whitelist the address on. Optional: omitting it selects the asset's default (BTC → BITCOIN, ETH/USDC/USDT → ETHEREUM, TRX → TRON). Only USDT has a choice (ETHEREUM or TRON). The entry can only back a withdrawal sent on this same network. Accepted values: `BITCOIN`, `ETHEREUM`, `TRON`.",
            "example": "ETHEREUM"
          },
          "address": {
            "type": [
              "null",
              "string"
            ]
          },
          "address_tag": {
            "type": [
              "null",
              "string"
            ]
          },
          "label": {
            "type": [
              "null",
              "string"
            ],
            "description": "Required, free text (your own reference for the destination, e.g. a client name). Refused with 422 label_required when blank."
          }
        }
      },
      "PublicApiCreateCryptoWithdrawalRequest": {
        "required": [
          "asset",
          "amount"
        ],
        "type": "object",
        "properties": {
          "asset": {
            "enum": [
              "BTC",
              "ETH",
              "USDC",
              "USDT",
              "TRX"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Crypto asset to send. TRX is accepted here and is NOT part of the exchange vocabulary — you can hold and withdraw TRX without being able to convert to it. Pair with `network` when the asset has more than one: USDT accepts ETHEREUM and TRON. Accepted values: `BTC`, `ETH`, `USDC`, `USDT`, `TRX`.",
            "example": "USDC"
          },
          "amount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "MAJOR units — whole coins, fractional expected. This is the only request amount on the v1 surface that is not in minor units. Rounded away from zero at the asset's scale (BTC 8, ETH 18, USDC/USDT/TRX 6). An amount that rounds to zero is refused with 422 invalid_amount; one that rounds up to a single atomic unit is accepted.",
            "format": "double",
            "example": "0.0125"
          },
          "destination_address": {
            "type": [
              "null",
              "string"
            ],
            "description": "Raw on-chain destination, honoured ONLY when your tenant has raw addresses enabled (off by default) AND `whitelisted_address_id` is absent. In the default configuration it is ignored: send `whitelisted_address_id` instead. When honoured it is trimmed and must be 20–100 characters, else 422 invalid_destination_address; no checksum or network validation is applied.",
            "example": "0x0000000000000000000000000000000000000000"
          },
          "destination_tag": {
            "type": [
              "null",
              "string"
            ],
            "description": "Optional memo/tag for the raw-address path only; ignored whenever `whitelisted_address_id` is supplied (the whitelist entry's own tag is used). Never required. Trimmed; blank is treated as absent."
          },
          "description": {
            "type": [
              "null",
              "string"
            ],
            "description": "Free-form metadata for your own records. It is stored against the withdrawal and NEVER reaches the chain — it is not a payment reference and is not length- or character-validated. The on-chain memo or tag field is destination_tag.",
            "example": "Treasury sweep"
          },
          "whitelisted_address_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "Id of a whitelisted withdrawal address belonging to this customer. REQUIRED in the default configuration — omitting it (or sending the all-zero GUID) is refused with 422 whitelisted_address_required. The entry must be Active (control-verified and approved) and must match both the withdrawal `asset` and the resolved network, so paying a whitelisted USDT/TRON address also requires network: \"TRON\". Create one with POST .../crypto/withdrawal-addresses (or on the dashboard); either way it starts PendingVerification and still needs a proof-of-control deposit and staff approval before it is usable here — no API-key route can skip either of those two steps. When supplied, `destination_address` and `destination_tag` are ignored and taken from the whitelist entry.",
            "format": "uuid",
            "example": "018f6a2e-3b7c-7d2a-9e11-4c9f0a1b2c3d"
          },
          "network": {
            "enum": [
              "BITCOIN",
              "ETHEREUM",
              "TRON"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Network to send on. Optional: omitting it selects the asset's default (BTC → BITCOIN, ETH/USDC/USDT → ETHEREUM, TRX → TRON). Only USDT has a choice (ETHEREUM or TRON); for every other asset the sole accepted value is its default and anything else is 422 unsupported_asset with detail \"Network X is not supported for ASSET.\" Case-insensitive. The whitelisted address must be on this network. Accepted values: `BITCOIN`, `ETHEREUM`, `TRON`.",
            "example": "ETHEREUM"
          }
        }
      },
      "PublicApiCreateEndCustomerAccountRequest": {
        "required": [
          "currency"
        ],
        "type": "object",
        "properties": {
          "currency": {
            "enum": [
              "EUR",
              "GBP"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Currency of the virtual account to issue. EUR and GBP only on the end-customer surface — other currencies are refused with self_serve_currency_not_available. The account is always an OpenPayd virtual account; the provider is not selectable here. Accepted values: `EUR`, `GBP`.",
            "example": "EUR"
          }
        }
      },
      "PublicApiCreateEndCustomerRequest": {
        "required": [
          "subject_type",
          "external_reference",
          "country_code"
        ],
        "type": "object",
        "properties": {
          "subject_type": {
            "enum": [
              "Personal",
              "Business"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Whether this end customer is an individual or a company. Determines which verification runs: Personal takes KYC, Business takes KYB. Accepted values: `Personal`, `Business`.",
            "example": "Personal"
          },
          "external_reference": {
            "type": [
              "null",
              "string"
            ],
            "description": "Your own stable identifier for this end customer, scoped to the parent and subject type. Posting it again returns the existing child without updating it. The HTTP Idempotency-Key header is also required; keep both unchanged when retrying the same create request.",
            "example": "customer-1042"
          },
          "display_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Name to show for the end customer. Optional; for a Personal end customer it is the name presented on their record, for a Business the legal name is `legal_name`.",
            "example": "Jane Doe"
          },
          "country_code": {
            "pattern": "^[A-Za-z]{2}$",
            "type": [
              "null",
              "string"
            ],
            "description": "ISO 3166-1 alpha-2 country of the end customer. Selects the verification level applied, so it must be the country you want them verified in.",
            "example": "GB"
          },
          "legal_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Registered company name. Required when subject_type is Business, ignored otherwise. Held on the record and PREFILLED into the KYB form — a name retyped later that diverges from this one is what the applicant identity binding refuses.",
            "example": "Acme Trading Ltd"
          },
          "registration_number": {
            "type": [
              "null",
              "string"
            ],
            "description": "Company registration number. Business end customers only; prefilled into the KYB form alongside legal_name.",
            "example": "12345678"
          }
        }
      },
      "PublicApiCreateFiatWithdrawalRequest": {
        "required": [
          "beneficiary_id",
          "amount",
          "currency"
        ],
        "type": "object",
        "properties": {
          "beneficiary_id": {
            "type": "string",
            "description": "The `id` of a beneficiary belonging to this customer. It must be in the same currency as the withdrawal (422 `beneficiary_currency_mismatch`), be `Completed` with a provider reference (409 `beneficiary_not_ready`) and be `approval_status` `Approved` (409 `beneficiary_not_approved`). A beneficiary id that does not exist for this customer is refused with 422 `beneficiary_not_found`, not 404. The beneficiary's provider also decides which of the customer's accounts funds the payment — see 409 `source_account_not_ready`.",
            "format": "uuid",
            "example": "1c9f3e6a-5d2b-4f7e-8a90-0b1c2d3e4f50"
          },
          "amount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "MINOR units, as a positive whole number — 2000 is GBP 20.00. Fiat currencies here (GBP, EUR, USD) all use a scale of 2. A value of zero or below, or one with a fractional part, is refused with 422 invalid_amount; there is no platform minimum beyond that. One per-rail CEILING exists: a GBP Faster Payments withdrawal to a beneficiary held at OpenPayd is capped at GBP 1,000,000 per payment, and an amount above it is NOT refused — the call returns 200 with the withdrawal at status Failed and the funds released. Use rail CHAPS for larger GBP payments; it has no cap. Note that a decimal such as 20.00 is accepted as TWENTY MINOR UNITS (GBP 0.20), not GBP 20.00 — send an integer.",
            "format": "double",
            "example": "2000"
          },
          "currency": {
            "enum": [
              "EUR",
              "GBP",
              "USD"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Fiat currency of the withdrawal. REQUIRED and unconditionally checked: omitted, blank or outside the platform set is refused with 422 `unsupported_currency`, and a value that differs from the beneficiary's currency stops at 422 `beneficiary_currency_mismatch`. This request has NO alternate spelling — there is no `currency_code` member on it, so the alias wording on other currency fields does not apply here. Accepted values: `EUR`, `GBP`, `USD`.",
            "example": "GBP"
          },
          "rail": {
            "type": [
              "null",
              "string"
            ],
            "description": "Payment rail, validated against the currency when the beneficiary is held at Fiat Republic: GBP takes FPS, CHAPS or BACS; EUR takes SCT; USD takes ACH, ACH_SAME_DAY, DOMESTIC_WIRE or INTERNATIONAL_WIRE. Optional — omitting it selects FPS for GBP, SCT for EUR and ACH for USD, except that a USD beneficiary whose own schemes are known picks the first of ACH, ACH_SAME_DAY, DOMESTIC_WIRE, INTERNATIONAL_WIRE that it supports. Input is normalised before matching, so case, hyphens and spaces are tolerated and the deprecated spellings FASTER_PAYMENTS, SEPA, SEPA_INSTANT, WIRE, FEDWIRE and SWIFT are accepted as aliases — only the canonical value is ever stored or returned. A rail outside the currency's set is refused with 422 unsupported_rail before any ledger hold is taken. IMPORTANT — all of the above applies ONLY when the beneficiary is held at Fiat Republic. For a beneficiary at any other provider the rail is translated into that provider's own vocabulary: an OpenPayd beneficiary accepts FASTER_PAYMENTS, CHAPS, SEPA or SEPA_INSTANT (SCT is accepted as an alias for SEPA), and omitting the rail on a EUR withdrawal to an OpenPayd beneficiary defaults to SEPA_INSTANT. SWIFT for EUR and GBP is an explicit OpenPayd route, independently enabled per currency and verified recipient bank destination. A SWIFT beneficiary requires exactly one IBAN or account number, a BIC and the recipient's own address. Use the v2 customer banking swift-options and swift-quote endpoints to discover availability and obtain the fee. Send expected_swift_fee_minor and swift_fee_configuration_version_id with the withdrawal. Reuse the original key and entire payload when retrying an unconfirmed request. SWIFT beneficiaries cannot be paid by another rail or by payout batch. Reference limit: 35 ASCII characters. purpose_code is not exposed by this API. Separately, and more important for your error handling: a provider refusal that happens AFTER a hold has been taken does NOT return an error status. The hold is released, the payment is marked Failed, and the call returns 200 with the message \"Withdrawal created successfully\" — so branch on the withdrawal's own status field, never on the HTTP code. One more Fiat Republic gate: a payment scheme STORED on the beneficiary (attribute `paymentScheme`) is copied into the payment and validated independently of the rail you send, so it can produce a 422 unsupported_rail, rail_not_supported_by_beneficiary or invalid_payment_reference naming a rail you never supplied.",
            "example": "FPS"
          },
          "description": {
            "type": [
              "null",
              "string"
            ],
            "description": "The PAYMENT REFERENCE shown on the receiving statement, not free-form metadata. Trimmed; defaults to \"Withdrawal\". Validation is provider- and currency-gated. Fiat Republic USD is checked BEFORE the ledger hold: letters only (space, hyphen, full stop, ampersand and slash are permitted but not counted), at most 17 counted characters, at least 6 alphanumerics, and not every alphanumeric the same character — otherwise 422 invalid_payment_reference. Fiat Republic GBP and EUR are deliberately NOT pre-checked here, so a refusal arrives from the provider AFTER the hold is taken. Supported OpenPayd payment types are checked BEFORE the hold: ASCII-only and a BYTE cap of 18 for Faster Payments, 35 for CHAPS/SWIFT and 140 for SEPA; invalid references return 422 invalid_payment_reference.",
            "example": "Invoice ABC"
          },
          "transparency": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PaymentTransparencyRequest"
              }
            ],
            "description": "Structured payment disclosure, separate from the payee-visible reference and bank account holder. Optional only while this customer's policy allows omission. Supplied incomplete or contradictory data is refused before a new financial workflow. Nested property names use camelCase. Identical replay retains the original disclosure; changing it cannot rewrite the payment."
          },
          "expected_swift_fee_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Required for an OpenPayd EUR or GBP SWIFT withdrawal: the feeMinorString from the customer's v2 banking swift-quote response, expressed in minor units of the withdrawal currency. A fresh request is refused before claim/hold if the current fee differs. On an unconfirmed retry, reuse the original value, fee version, idempotency key and entire original payload.",
            "format": "double",
            "example": "500"
          },
          "swift_fee_configuration_version_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "Required for an OpenPayd EUR or GBP SWIFT withdrawal: the feeConfigurationVersionId from the same swift-quote response as expected_swift_fee_minor. This identifies the fee schedule the customer accepted. Keep it unchanged when retrying an unresolved payment, even if a newer fee version is available.",
            "format": "uuid",
            "example": "1c9f3e6a-5d2b-4f7e-8a90-0b1c2d3e4f50"
          }
        }
      },
      "PublicApiCreatePayoutBatchItemRequest": {
        "required": [
          "beneficiary_id",
          "amount"
        ],
        "type": "object",
        "properties": {
          "beneficiary_id": {
            "type": "string",
            "description": "UUID of a beneficiary stored for this customer (POST .../banking/beneficiaries) in the batch currency, provisioned at the provider and approved. A beneficiary_id that is omitted or all zeros, names no beneficiary of this customer's, is in another currency, is still provisioning, or is not yet approved makes the item Skipped with the reason — one bad item never fails the request.",
            "format": "uuid",
            "example": "0198e2f4-6b7c-7d3a-9f1e-2c4a5b6d7e8f"
          },
          "amount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "MINOR units at a scale of 2 for every currency (2000 = 20.00), as a positive whole number. Zero, negative or fractional values do not fail the request — the item is recorded as Skipped with the reason and the rest of the batch proceeds. Omitting the field is the same as sending 0.",
            "format": "double",
            "example": "2000"
          },
          "reference": {
            "type": [
              "null",
              "string"
            ],
            "description": "Payment reference for this item — the text on the recipient's statement. Optional: omit it or send null and the item is paid with \"Batch payout\". Do NOT send an empty string: on FPS, CHAPS, SCT and every USD rail it is refused (\"a payment reference is required.\") and the item is Skipped. Checked at intake against the batch's stored rail: FPS at most 18 and CHAPS at most 35 ASCII bytes, SCT at most 140, all ASCII-only. USD rails are held to Fiat Republic's rule: letters a-z A-Z only (space - . & / permitted and not counted), at most 17 counted characters, at least 6 alphanumerics, not all the same. BACS, DE and NPP get NO local check — the value goes to the provider as sent. A ceiling of 256 characters (after trimming) applies on every rail. Any failure Skips only this item.",
            "example": "Invoice ABC"
          },
          "transparency": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PaymentTransparencyRequest"
              }
            ],
            "description": "This item's payment disclosure. Required when enabled for the customer. An invalid declaration skips this item without rejecting otherwise valid items. The approved snapshot is retained through checker approval and execution; downstream institution permission is rechecked at execution. Nested property names use camelCase."
          }
        }
      },
      "PublicApiCreatePayoutBatchRequest": {
        "required": [
          "currency",
          "items"
        ],
        "type": "object",
        "properties": {
          "currency": {
            "enum": [
              "EUR",
              "GBP",
              "USD"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Currency for every item in the batch, matched against each beneficiary's own currency. Must be a platform fiat currency — GBP, EUR or USD; anything else (AUD included) is refused with 422 `unsupported_currency`, and a rail the currency does not support is refused with 422 `unsupported_rail`. Case-insensitive; stored and returned uppercased. Accepted values: `EUR`, `GBP`, `USD`.",
            "example": "GBP"
          },
          "rail": {
            "type": [
              "null",
              "string"
            ],
            "description": "Payment rail for every item, validated against Fiat Republic's scheme table for the batch currency BEFORE anything is stored, whichever provider holds the beneficiaries. GBP takes FPS, CHAPS or BACS; EUR takes SCT only; USD takes ACH, ACH_SAME_DAY, DOMESTIC_WIRE or INTERNATIONAL_WIRE; AUD takes DE or NPP. Optional: omitted, it defaults to FPS for GBP, SCT for EUR and ACH for USD (AUD has no default and is refused without one). Case, hyphens and spaces are tolerated and the aliases FASTER_PAYMENTS, SEPA, SEPA_INSTANT, SCT_INST, WIRE, FEDWIRE and SWIFT are accepted — but only the canonical scheme is stored and returned, so FASTER_PAYMENTS comes back as FPS and SEPA_INSTANT as SCT (there is no instant SEPA on a batch). Anything else, including EAGLE_NET_TRANSFER, is refused with 422 unsupported_rail. At execution the stored scheme is handed to each beneficiary's provider unchanged: OpenPayd accepts FPS and CHAPS but not SCT or BACS, so a EUR batch, or a GBP batch on BACS, paying a beneficiary held at OpenPayd fails item by item with failure_code withdrawal_failed after approval.",
            "example": "FPS"
          },
          "items": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/PublicApiCreatePayoutBatchItemRequest"
            },
            "description": "The payouts, in the order you want them executed: 1 to 500 entries. Missing or empty is refused with 422 `items_required`; more than 500 with 422 `too_many_items`. Every entry is validated on its own — an invalid entry becomes a Skipped item with its reason and never blocks the others. When EVERY entry is invalid the batch is still created, in status Rejected, and the call still returns HTTP 200 — read `payout_batch.status` and each item's `status` and `failure_reason`."
          }
        }
      },
      "PublicApiCryptoWithdrawalAddressData": {
        "required": [
          "withdrawal_address"
        ],
        "type": "object",
        "properties": {
          "withdrawal_address": {
            "$ref": "#/components/schemas/PublicApiCryptoWithdrawalAddressResponse"
          }
        }
      },
      "PublicApiCryptoWithdrawalAddressListData": {
        "required": [
          "withdrawal_addresses"
        ],
        "type": "object",
        "properties": {
          "withdrawal_addresses": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiCryptoWithdrawalAddressResponse"
            }
          }
        }
      },
      "PublicApiCryptoWithdrawalAddressResponse": {
        "required": [
          "id",
          "asset_code",
          "network",
          "address",
          "address_tag",
          "label",
          "status",
          "rejected_reason",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "asset_code": {
            "enum": [
              "BTC",
              "ETH",
              "USDC",
              "USDT",
              "TRX"
            ],
            "type": "string",
            "description": "Public API crypto asset code. Accepted values: `BTC`, `ETH`, `USDC`, `USDT`, `TRX`.",
            "example": "USDC"
          },
          "network": {
            "enum": [
              "BITCOIN",
              "ETHEREUM",
              "TRON"
            ],
            "type": "string",
            "description": "Blockchain network code. Accepted values: `BITCOIN`, `ETHEREUM`, `TRON`.",
            "example": "ETHEREUM"
          },
          "address": {
            "type": "string"
          },
          "address_tag": {
            "type": [
              "null",
              "string"
            ]
          },
          "label": {
            "type": "string"
          },
          "status": {
            "enum": [
              "PendingVerification",
              "ControlVerified",
              "Active",
              "Rejected",
              "Disabled"
            ],
            "type": "string",
            "description": "Lifecycle state, from PendingVerification, ControlVerified, Active, Rejected, Disabled. This route only ever CREATES a PendingVerification entry — every other value is reached later, by a proof-of-control deposit or by staff on the dashboard. Only Active can be used as whitelisted_address_id on a withdrawal. Accepted values: `PendingVerification`, `ControlVerified`, `Active`, `Rejected`, `Disabled`.",
            "example": "PendingVerification"
          },
          "rejected_reason": {
            "type": [
              "null",
              "string"
            ],
            "description": "Set only when status is Rejected: the reason staff gave for refusing the submission. Null for every other status — including PendingVerification, ControlVerified, and Disabled, where an internal reason may exist (a periodic sanctions re-screen can disable an already-Active address) but is deliberately withheld here: it can name a sanctions-screening outcome about the address."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PublicApiCustomerData": {
        "required": [
          "customer"
        ],
        "type": "object",
        "properties": {
          "customer": {
            "description": "The customer as it now stands — on a create, update or read this is the full record, not a delta.",
            "$ref": "#/components/schemas/PublicApiCustomerResponse"
          }
        }
      },
      "PublicApiCustomerFeesData": {
        "required": [
          "customer_id",
          "fees"
        ],
        "type": "object",
        "properties": {
          "customer_id": {
            "type": "string",
            "description": "The customer id from the path, echoed back.",
            "format": "uuid",
            "example": "0198f3a0-4b5c-7d6e-8f90-a1b2c3d4e5f6"
          },
          "fees": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiFeeConfigurationResponse"
            },
            "description": "The fee rows that apply to THIS customer: enabled, effective now, filtered to the customer's type (`Consumer` for a personal customer, `Business` for a company), and with any row negotiated for this specific customer replacing the tenant default for the same dimension. Ordered by `fee_type`, then `asset_code`, then `customer_type`. As on commercial-config, rail/network, geography and product are part of the dimension but not exposed, so rows can repeat apart from `id`. Not paged."
          }
        }
      },
      "PublicApiCustomerListData": {
        "required": [
          "customers",
          "pagination"
        ],
        "type": "object",
        "properties": {
          "customers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiCustomerResponse"
            },
            "description": "The page of customers and businesses, newest first. People and companies share one list; tell them apart by `user_type`/`record_type`."
          },
          "pagination": {
            "description": "Paging metadata for this list. `page` and `limit` (1-500) on the request are honoured; `total` counts every matching record across all pages, not the rows in this response, and `last_page` is `ceil(total / per_page)` (never 0). Loop `page` from 1 to `last_page`. See the Pagination guide.",
            "$ref": "#/components/schemas/PublicApiPaginationResponse"
          }
        }
      },
      "PublicApiCustomerResponse": {
        "required": [
          "id",
          "external_reference",
          "display_name",
          "user_type",
          "jurisdiction",
          "status",
          "record_type",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The customer's identifier. This is the `customerId` every customer-scoped route takes in its path.",
            "format": "uuid",
            "example": "0198f3a0-1234-7000-8000-0000000000c1"
          },
          "external_reference": {
            "type": "string",
            "description": "The customer's email address as supplied when the record was created — it doubles as your stable reference, is unique per customer within your account, and is the key the email-keyed onboarding routes resolve on. Change it with PATCH `email`/`external_reference`.",
            "example": "jane.doe@example.com"
          },
          "display_name": {
            "type": "string",
            "description": "Name to show for the customer. For a person it is the supplied `name`, falling back to the email when none was given; for a business it is the registered legal name.",
            "example": "Jane Doe"
          },
          "user_type": {
            "enum": [
              "Personal",
              "Business"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Whether the customer is a person (`Personal`, verified by KYC) or a company (`Business`, verified by KYB). PascalCase, exactly as shown. Accepted values: `Personal`, `Business`.",
            "example": "Personal"
          },
          "jurisdiction": {
            "type": [
              "null",
              "string"
            ],
            "description": "Country the customer is registered in, as stored: ISO 3166-1 alpha-2 (alpha-3 and `UK` supplied at creation were converted). This is the input to sanctions and eligibility checks. Null only on records created before it became mandatory.",
            "example": "GB"
          },
          "status": {
            "type": "string",
            "description": "Lifecycle status. `Draft` (created, verification not started), `Onboarding` (verification session opened; all money movement blocked), `PendingCompliance` (awaiting a decision), `Active` (verified; money movement allowed — the only status most money routes accept), `Rejected`, `Suspended`, `Blocked`, `Closed`. Only `Active` can be reached by verification; `Suspended`/`Blocked`/`Closed` can be set by PATCH and are not reversible on this API. The list is what the code writes today; treat any other value as not-Active.",
            "example": "Active"
          },
          "record_type": {
            "enum": [
              "Customer",
              "Business"
            ],
            "type": "string",
            "description": "Public API storage record type returned by list endpoints. Accepted values: `Customer`, `Business`.",
            "example": "Customer"
          },
          "created_at": {
            "type": "string",
            "description": "When the record was created, ISO-8601 UTC. Lists are ordered by this, newest first.",
            "format": "date-time",
            "example": "2026-09-01T09:30:00.000000+00:00"
          },
          "updated_at": {
            "type": "string",
            "description": "When the record last changed, ISO-8601 UTC. Moves on PATCH, on verification start, and when a read by id activates the customer after Sumsub approval.",
            "format": "date-time",
            "example": "2026-09-02T10:15:30.000000+00:00"
          }
        }
      },
      "PublicApiCustomerUpdateRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": [
              "null",
              "string"
            ],
            "description": "New email/reference for the customer. Alias of `external_reference` — that one wins when both are sent. Must contain `@` (422 invalid_email). Refused with 409 external_reference_in_use if another customer in your account already holds it. Omit to leave unchanged.",
            "example": "jane.doe@example.com"
          },
          "external_reference": {
            "type": [
              "null",
              "string"
            ],
            "description": "New reference for the customer; the same field as `email` and takes precedence over it. Must contain `@` (422 invalid_email); 409 external_reference_in_use if another customer holds it. Omit to leave unchanged.",
            "example": "jane.doe@example.com"
          },
          "name": {
            "type": [
              "null",
              "string"
            ],
            "description": "New display name (person) or legal name (business). Omit or send blank to leave unchanged.",
            "example": "Jane Doe"
          },
          "jurisdiction": {
            "type": [
              "null",
              "string"
            ],
            "description": "New country for the customer. ISO 3166-1 alpha-2 canonical; alpha-3 and `UK` are accepted and converted; anything unrecognised is refused with 422 jurisdiction_invalid, never coerced. Note that an already-opened verification case keeps the country it was opened with. Omit to leave unchanged.",
            "example": "GB"
          },
          "status": {
            "enum": [
              "Suspended",
              "Blocked",
              "Closed"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Customer lifecycle status. This endpoint only performs de-escalation, so the sole writable values are Suspended, Blocked and Closed (case-insensitive). A customer cannot be returned to Draft or Active here; anything else is refused with 422 unsupported_status. This is ONE-WAY: no route on this API restores a Suspended, Blocked or Closed customer to Active — not even a later approved verification — so treat it as permanent and contact KwiikPay support to reverse it. Accepted values: `Suspended`, `Blocked`, `Closed`.",
            "example": "Suspended"
          }
        }
      },
      "PublicApiDepositData": {
        "required": [
          "deposit"
        ],
        "type": "object",
        "properties": {
          "deposit": {
            "description": "One deposit owned by the customer in the route. Read it at `GET customers/{customerId}/banking/deposits/{depositId}` or `GET customers/{customerId}/crypto/deposits/{depositId}`. Both require only `deposits:read`; a parent's key may read its own authorized sub-customer by using the child's customerId. Missing, wrong-type and out-of-scope deposits return 404.",
            "$ref": "#/components/schemas/PublicApiDepositResponse"
          }
        }
      },
      "PublicApiDepositListData": {
        "required": [
          "deposits",
          "pagination"
        ],
        "type": "object",
        "properties": {
          "deposits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiDepositResponse"
            },
            "description": "Deposits for this customer, newest first (by `created_at`, then by id). No status filter is applied: rows that are still pending, held for review, failed or returned are all included, so read `status` (and `reason_code`) before treating a row as money received. The fiat route lists only fiat deposits and the crypto route only crypto deposits — call both to see everything that arrived. Page with `page` and `limit`; `pagination.total` is the real count."
          },
          "pagination": {
            "description": "Paging metadata for this list. `page` and `limit` (1-500) on the request are honoured; `total` counts every matching record across all pages, not the rows in this response, and `last_page` is `ceil(total / per_page)` (never 0). Loop `page` from 1 to `last_page`. See the Pagination guide.",
            "$ref": "#/components/schemas/PublicApiPaginationResponse"
          }
        }
      },
      "PublicApiDepositResponse": {
        "required": [
          "id",
          "type",
          "status",
          "amount",
          "currency",
          "asset_code",
          "asset_scale",
          "network_code",
          "provider_reference",
          "transaction_hash",
          "reason_code",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Deposit id (UUID). Stable across reads; deduplicate on it. For a fiat deposit it is also the `source_command_id` carried in `metadata` on the account transaction feed, which links the deposit to its ledger journals.",
            "format": "uuid",
            "example": "0198f3a0-4b5c-7d6e-8f90-a1b2c3d4e5f6"
          },
          "type": {
            "enum": [
              "fiat",
              "crypto"
            ],
            "type": "string",
            "description": "Which route produced the row: `fiat` from `.../banking/deposits`, `crypto` from `.../crypto/deposits`. Tells you which half of this object is populated: a `fiat` row carries `currency` and `provider_reference` and has `asset_code`, `network_code` and `transaction_hash` null; a `crypto` row carries `asset_code`, `network_code`, `transaction_hash` and `provider_reference` and has `currency` null. Accepted values: `fiat`, `crypto`.",
            "example": "fiat"
          },
          "status": {
            "type": "string",
            "description": "Lifecycle state, as stored. FIAT rows use the fiat workflow vocabulary: `Pending` and `PendingProvider` (recorded, not yet credited), `Completed` (credited to the account's available balance), `ManualReview` (held for review — `reason_code` is set), and the terminal `Failed`, `Returned` and `Closed` (nothing spendable). CRYPTO rows use the crypto deposit vocabulary: `Pending` (recorded, not yet confirmed), `Confirmed` (credited), `Failed`, `Reorged` (the on-chain transaction was reorganised out — do not treat as received) and `Quarantined` (received on-chain but held after screening — `reason_code` is set, nothing credited). Only `Completed` (fiat) and `Confirmed` (crypto) mean the customer can spend the money. Case is as shown.",
            "example": "Completed"
          },
          "amount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "ATOMIC/minor units, for both fiat and crypto deposits. Do NOT infer the scale from the currency: read asset_scale on this same object, which is the scale the platform actually stored for the deposit. A fiat GBP deposit of 4480 with asset_scale 2 is GBP 44.80; a USDC deposit of 5000000 with asset_scale 6 is 5 USDC.",
            "format": "double",
            "example": "4480"
          },
          "currency": {
            "enum": [
              "GBP",
              "USD",
              "EUR"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Fiat currency of the deposit — the currency of the account it landed on (GBP, EUR or USD). Null on a `crypto` row, whose asset is in `asset_code`. Accepted values: `GBP`, `USD`, `EUR`.",
            "example": "GBP"
          },
          "asset_code": {
            "enum": [
              "BTC",
              "ETH",
              "USDC",
              "USDT",
              "TRX"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Crypto asset of a `crypto` deposit; always null on a `fiat` row, whose currency is in `currency`. Includes TRX, which arrives as the native asset on TRON — a strict client built from the exchange asset list will fail to deserialise those rows. Accepted values: `BTC`, `ETH`, `USDC`, `USDT`, `TRX`.",
            "example": "USDC"
          },
          "asset_scale": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "null",
              "integer",
              "string"
            ],
            "description": "Number of decimal places `amount` is expressed in, as stored for this deposit. Divide `amount` by 10^asset_scale for the human amount: 2 for GBP/EUR/USD, 8 for BTC, 18 for ETH, 6 for USDC/USDT/TRX. Read it from the row rather than inferring it from the currency.",
            "format": "int32",
            "example": "2"
          },
          "network_code": {
            "enum": [
              "BITCOIN",
              "ETHEREUM",
              "TRON"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Public API blockchain network code. Accepted values: `BITCOIN`, `ETHEREUM`, `TRON`.",
            "example": "ETHEREUM"
          },
          "provider_reference": {
            "type": [
              "null",
              "string"
            ],
            "description": "The provider's own reference for this deposit — the banking provider's payment reference on a `fiat` row, the custody provider's transaction reference on a `crypto` row. Null until the provider has assigned one. On a fiat row it is the same value as `metadata.provider_reference` on the account transaction feed, which is how a deposit is matched to its ledger journal. This identifies the provider transaction. The separate `reference` carries the incoming payment reference, and `sender` carries the provider-reported payer name when captured.",
            "example": "pmt_01J9X0Y5Z6A7B8C9D0E1F2G3H4"
          },
          "transaction_hash": {
            "type": [
              "null",
              "string"
            ],
            "description": "On-chain transaction hash of a `crypto` deposit, for matching against your own node or a block explorer. Always null on a `fiat` row.",
            "example": "0x9c2f6a1e0b3d4c5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6"
          },
          "reason_code": {
            "type": [
              "null",
              "string"
            ],
            "description": "Why the deposit is held, when it is. Non-null only when `status` is `ManualReview` (fiat) or `Quarantined` (crypto). Values: `pending_review` (held for a review; also every held fiat deposit, and any crypto hold not listed below), `compliance_hold` (held on a compliance or screening outcome) and `tier_limit_hold` (a crypto deposit that exceeded a configured limit). The internal reason text is never returned. A held deposit fires no webhook; poll this route to see it clear. Release is a manual action by the platform — there is nothing for you to call.",
            "example": "pending_review"
          },
          "created_at": {
            "type": "string",
            "description": "When the platform first recorded the deposit (ISO 8601, UTC). Rows are ordered by this field, newest first. It is not the on-chain block time or the provider's value date.",
            "format": "date-time",
            "example": "2026-08-31T10:15:30.1234567+00:00"
          },
          "updated_at": {
            "type": "string",
            "description": "When the row last changed (ISO 8601, UTC), for example on a status change. Compare it with the value you stored to detect a change on an already-seen row.",
            "format": "date-time",
            "example": "2026-08-31T10:16:02.9876543+00:00"
          },
          "sender": {
            "type": [
              "null",
              "string"
            ],
            "description": "Provider-reported payer name on a fiat deposit, or null when not captured. It is a display name supplied by the banking provider, not independently verified originator identity. Always null for crypto deposits. Historical values may become available after provider reconciliation; read the deposit again rather than replaying an old webhook.",
            "example": "Example Payer Ltd"
          },
          "reference": {
            "type": [
              "null",
              "string"
            ],
            "description": "Incoming payment/remittance reference on a fiat deposit, or null when not captured; at most 512 characters. Separate from `provider_reference` (the provider transaction identifier) and any internal hold reference. For a crypto deposit, this is the on-chain transaction hash, also available as `transaction_hash`.",
            "example": "INV-1042"
          }
        }
      },
      "PublicApiEndCustomerAssetBalanceResponse": {
        "required": [
          "asset_code",
          "asset_scale",
          "total_amount"
        ],
        "type": "object",
        "properties": {
          "asset_code": {
            "type": "string",
            "description": "Currency or asset code of this balance — a fiat code such as `GBP`/`EUR`/`USD` or a crypto asset such as `USDC`. Open vocabulary.",
            "example": "GBP"
          },
          "asset_scale": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Number of decimal places for this asset, shipped alongside every amount so the pair is self-describing and no per-currency assumption has to be hardcoded.",
            "format": "int32",
            "example": "2"
          },
          "total_amount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Balance in ATOMIC (minor) units, not a decimal amount. Read it with the asset_scale in the same object: 1234 at scale 2 is 12.34.",
            "format": "double",
            "example": "1234"
          }
        }
      },
      "PublicApiEndCustomerBalanceData": {
        "required": [
          "end_customer_id",
          "balances"
        ],
        "type": "object",
        "properties": {
          "end_customer_id": {
            "type": "string",
            "description": "The end customer these balances belong to.",
            "format": "uuid",
            "example": "0198f3a0-1234-7000-8000-0000000000e1"
          },
          "balances": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiEndCustomerAssetBalanceResponse"
            },
            "description": "One entry per currency or asset the end customer holds, each as an atomic amount plus its scale."
          }
        }
      },
      "PublicApiEndCustomerConsolidationData": {
        "required": [
          "consolidation"
        ],
        "type": "object",
        "properties": {
          "consolidation": {
            "$ref": "#/components/schemas/PublicApiEndCustomerConsolidationResponse"
          }
        }
      },
      "PublicApiEndCustomerConsolidationRequest": {
        "required": [
          "currency",
          "amount_minor"
        ],
        "type": "object",
        "properties": {
          "currency": {
            "type": [
              "null",
              "string"
            ],
            "description": "Fiat currency of the balance to move up to the parent. Both the end customer and the parent must hold a completed account in it.",
            "example": "EUR"
          },
          "amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Amount to move, in MINOR units of `currency` as an integer (12.34 EUR is 1234). Must not exceed the end customer's available balance.",
            "format": "int64",
            "example": "1234"
          }
        }
      },
      "PublicApiEndCustomerConsolidationResponse": {
        "required": [
          "end_customer_id",
          "parent_customer_id",
          "currency",
          "amount_minor",
          "journal_entry_id"
        ],
        "type": "object",
        "properties": {
          "end_customer_id": {
            "type": "string",
            "description": "The end customer whose balance was reduced.",
            "format": "uuid",
            "example": "0198f3a0-1234-7000-8000-0000000000e1"
          },
          "parent_customer_id": {
            "type": "string",
            "description": "The parent customer whose balance was credited.",
            "format": "uuid",
            "example": "0198f3a0-1234-7000-8000-0000000000c1"
          },
          "currency": {
            "type": "string",
            "description": "Currency moved.",
            "example": "EUR"
          },
          "amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Amount moved, in MINOR units of `currency`.",
            "format": "int64",
            "example": "1234"
          },
          "journal_entry_id": {
            "type": "string",
            "description": "Identifier of the single balanced ledger entry that recorded the move. Stable across an idempotent replay.",
            "format": "uuid",
            "example": "0198f3a0-1234-7000-8000-0000000000f1"
          }
        }
      },
      "PublicApiEndCustomerData": {
        "required": [
          "end_customer"
        ],
        "type": "object",
        "properties": {
          "end_customer": {
            "$ref": "#/components/schemas/PublicApiEndCustomerResponse"
          }
        }
      },
      "PublicApiEndCustomerDocumentListData": {
        "required": [
          "documents"
        ],
        "type": "object",
        "properties": {
          "documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiEndCustomerDocumentResponse"
            },
            "description": "Every document the provider holds for this end customer's verification. Download each by `document_id`; the download returns raw bytes with the provider's content type, not JSON."
          }
        }
      },
      "PublicApiEndCustomerDocumentResponse": {
        "required": [
          "document_id",
          "category",
          "document_type",
          "country_code",
          "added_at"
        ],
        "type": "object",
        "properties": {
          "document_id": {
            "type": "string",
            "description": "The provider's identifier for this document image. Pass it as `documentId` to the download route.",
            "example": "1234567890"
          },
          "category": {
            "enum": [
              "COMPANY_DOC",
              "SELFIE",
              "FILE_ATTACHMENT",
              "UNKNOWN",
              "IDENTITY"
            ],
            "type": "string",
            "description": "Coarse kind of document, derived from the provider's document type so you can decide what an audience may see: `IDENTITY` (passport, ID card, licence, residence permit, proof of address and any other typed document), `SELFIE`, `COMPANY_DOC`, `FILE_ATTACHMENT`, or `UNKNOWN` when the provider gave no type. Read `document_type` for the precise kind. Accepted values: `COMPANY_DOC`, `SELFIE`, `FILE_ATTACHMENT`, `UNKNOWN`, `IDENTITY`.",
            "example": "IDENTITY"
          },
          "document_type": {
            "type": [
              "null",
              "string"
            ],
            "description": "The provider's precise document type (for example `PASSPORT`, `ID_CARD`, `UTILITY_BILL`, `SELFIE`, `COMPANY_DOC`). Open vocabulary; null when the provider gave none.",
            "example": "PASSPORT"
          },
          "country_code": {
            "type": [
              "null",
              "string"
            ],
            "description": "Issuing country of the document as reported by the provider (three-letter code as Sumsub stores it), or null.",
            "example": "GBR"
          },
          "added_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "When the document was added. NOT POPULATED today — always null — because the provider listing does not carry a timestamp. Do not sort on it.",
            "format": "date-time"
          }
        }
      },
      "PublicApiEndCustomerListData": {
        "required": [
          "end_customers",
          "pagination"
        ],
        "type": "object",
        "properties": {
          "end_customers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiEndCustomerResponse"
            },
            "description": "The page of end customers under this parent."
          },
          "pagination": {
            "$ref": "#/components/schemas/PublicApiPaginationResponse"
          }
        }
      },
      "PublicApiEndCustomerResponse": {
        "required": [
          "id",
          "parent_customer_id",
          "subject_type",
          "external_reference",
          "display_name",
          "country_code",
          "legal_name",
          "registration_number",
          "status",
          "created_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The end customer's identifier — the `endCustomerId` in every end-customer route's path.",
            "format": "uuid",
            "example": "0198f3a0-1234-7000-8000-0000000000e1"
          },
          "parent_customer_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "The customer this end customer belongs to (the `customerId` in the path).",
            "format": "uuid",
            "example": "0198f3a0-1234-7000-8000-0000000000c1"
          },
          "subject_type": {
            "enum": [
              "Personal",
              "Business"
            ],
            "type": "string",
            "description": "`Personal` (verified by KYC) or `Business` (verified by KYB), as supplied at creation. Accepted values: `Personal`, `Business`.",
            "example": "Personal"
          },
          "external_reference": {
            "type": "string",
            "description": "Your identifier for the end customer, as supplied at creation. Unique within the parent and subject type; creating again with the same value and subject type returns this record.",
            "example": "customer-1042"
          },
          "display_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Display name as supplied at creation, or null.",
            "example": "Jane Doe"
          },
          "country_code": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2 country the end customer is verified in.",
            "example": "GB"
          },
          "legal_name": {
            "type": [
              "null",
              "string"
            ],
            "description": "Registered company name. Business end customers only; null for a person.",
            "example": "Acme Trading Ltd"
          },
          "registration_number": {
            "type": [
              "null",
              "string"
            ],
            "description": "Company registration number. Business end customers only; null when not supplied.",
            "example": "12345678"
          },
          "status": {
            "type": "string",
            "description": "Lifecycle status of the end customer. A newly created end customer is Onboarding and only becomes Active once verification completes.",
            "example": "Onboarding"
          },
          "created_at": {
            "type": "string",
            "description": "When the end customer was created, ISO-8601 UTC.",
            "format": "date-time",
            "example": "2026-09-01T09:30:00.000000+00:00"
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiAccountData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiAccountData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiAccountListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiAccountListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiAllowedDestinationsData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiAllowedDestinationsData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiBalanceListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiBalanceListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiBankingAccountFormSchemaData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiBankingAccountFormSchemaData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiBeneficiaryBatchData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiBeneficiaryBatchData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiBeneficiaryData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiBeneficiaryData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiBeneficiaryListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiBeneficiaryListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiCommercialConfigData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiCommercialConfigData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiConfigData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiConfigData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiConversionRequestData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiConversionRequestData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiConversionRequestListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiConversionRequestListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiCryptoWithdrawalAddressData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiCryptoWithdrawalAddressData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiCryptoWithdrawalAddressListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiCryptoWithdrawalAddressListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiCustomerData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiCustomerData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiCustomerFeesData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiCustomerFeesData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiCustomerListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiCustomerListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiDepositData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiDepositData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiDepositListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiDepositListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiEndCustomerBalanceData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiEndCustomerBalanceData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiEndCustomerConsolidationData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiEndCustomerConsolidationData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiEndCustomerData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiEndCustomerData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiEndCustomerDocumentListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiEndCustomerDocumentListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiEndCustomerListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiEndCustomerListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiIdentityData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiIdentityData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiInternalTransferData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiInternalTransferData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiInternalTransferListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiInternalTransferListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiKybStatusData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiKybStatusData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiKycStatusData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiKycStatusData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiOnboardingRequirementsData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiOnboardingRequirementsData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiOnboardingSessionData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiOnboardingSessionData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiOnboardingStatusData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiOnboardingStatusData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiPayoutBatchData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiPayoutBatchData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiPayoutBatchListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiPayoutBatchListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiTransactionListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiTransactionListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiTransferPriceData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiTransferPriceData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiValidationData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiValidationData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiVerificationCallbackData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiVerificationCallbackData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiWalletAddressData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiWalletAddressData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiWalletListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiWalletListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiWithdrawalData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiWithdrawalData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiEnvelopeOfPublicApiWithdrawalListData": {
        "required": [
          "success",
          "status_code",
          "message",
          "data"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Redundant with the HTTP status: `true` on 2xx, `false` on the 409/422 refusals this envelope carries. Branch on the HTTP status, not on this flag — 400, 401, 403, 429 and 500 never use this envelope at all."
          },
          "status_code": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The HTTP status of this response, repeated in the body as an integer. Always equal to the transport status.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable summary of what happened, in English (for example `Customers retrieved successfully`). Not stable, not localised, and not a contract: it can be reworded without notice. Never branch on it — use the HTTP status, and on a refusal `data.code`.",
            "example": "Customers retrieved successfully"
          },
          "data": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiWithdrawalListData"
              }
            ],
            "description": "The payload. On success, the operation's data object; on a 409 or 422 refusal raised by the operation, `{ code, detail }` (see `PublicApiValidationData`). Always present on the responses that use this envelope — the schema marks it nullable only because the wrapper is generic."
          }
        }
      },
      "PublicApiFeaturesResponse": {
        "required": [
          "exchange_enabled",
          "crypto_enabled",
          "fiat_enabled"
        ],
        "type": "object",
        "properties": {
          "exchange_enabled": {
            "type": "boolean",
            "description": "True when the tenant has an enabled provider capability for the Exchange product — currency and crypto conversion. False means conversion requests are not available on this tenant.",
            "example": "true"
          },
          "crypto_enabled": {
            "type": "boolean",
            "description": "True when the tenant has an enabled provider capability for ANY of the crypto products (wallets, crypto deposits, crypto withdrawals). It does not say which of the three; a tenant with wallets but no withdrawals still reads true.",
            "example": "true"
          },
          "fiat_enabled": {
            "type": "boolean",
            "description": "True when the tenant has an enabled provider capability for ANY of the fiat products (accounts, fiat deposits, fiat withdrawals). It does not say which; read `GET .../banking/accounts/form-schema` for what accounts can actually be issued.",
            "example": "true"
          }
        }
      },
      "PublicApiFeeConfigurationResponse": {
        "required": [
          "id",
          "fee_type",
          "provider_category",
          "provider",
          "asset_code",
          "asset_scale",
          "customer_type",
          "end_user_percentage_rate",
          "end_user_fixed_minor",
          "end_user_minimum_minor",
          "end_user_maximum_minor",
          "enabled",
          "effective_from",
          "effective_until"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Id of the fee VERSION row (UUID). It changes whenever the fee is re-issued (a new version with a later `effective_from`), so it is not a stable id for \"the deposit fee\" — key your cache on the dimension fields instead.",
            "format": "uuid",
            "example": "0198f3a0-7f6e-7d5c-8b4a-392817160504"
          },
          "fee_type": {
            "enum": [
              "Deposit",
              "Withdrawal",
              "Exchange",
              "MonthlyPersonalAccount",
              "MonthlyBusinessAccount",
              "MonthlyWallet",
              "MonthlyBankAccount",
              "MonthlySubscription",
              "FxFiatToFiat",
              "ExchangeCryptoToFiat",
              "ExchangeFiatToCrypto",
              "MinimumPersonalAccount",
              "MinimumBusinessAccount"
            ],
            "type": "string",
            "description": "What the fee is charged on. `Deposit`, `Withdrawal` and `Exchange` are the per-transaction fees on fiat and crypto movements and conversions; the `Monthly*` values are recurring account/wallet/subscription charges; `FxFiatToFiat`, `ExchangeCryptoToFiat` and `ExchangeFiatToCrypto` are direction-specific conversion fees. Which one a given conversion uses is decided by the platform's routing, not by you. Accepted values: `Deposit`, `Withdrawal`, `Exchange`, `MonthlyPersonalAccount`, `MonthlyBusinessAccount`, `MonthlyWallet`, `MonthlyBankAccount`, `MonthlySubscription`, `FxFiatToFiat`, `ExchangeCryptoToFiat`, `ExchangeFiatToCrypto`, `MinimumPersonalAccount`, `MinimumBusinessAccount`.",
            "example": "Deposit"
          },
          "provider_category": {
            "enum": [
              "Banking",
              "WalletCustody",
              "Liquidity",
              "Compliance",
              "Platform"
            ],
            "type": "string",
            "description": "Which class of provider the fee is attached to: `Banking` (fiat accounts, deposits and withdrawals), `WalletCustody` (crypto wallets, deposits and withdrawals), `Liquidity` (conversions), `Compliance`, `Platform` (recurring platform charges). This is the FEE vocabulary and it differs from the provider-configuration `category` used elsewhere on this document. Accepted values: `Banking`, `WalletCustody`, `Liquidity`, `Compliance`, `Platform`.",
            "example": "Banking"
          },
          "provider": {
            "type": [
              "null",
              "string"
            ],
            "description": "Provider code the fee row is attached to, upper-case — for example `FIAT_REPUBLIC` or `OPENPAYD` for banking, `UTILA` for custody, `HERCLE` for liquidity — or null for a row that applies regardless of provider. Rows created before 2026-08-12 may still name the decommissioned `BCB`. Open vocabulary: the platform adds providers without changing this document.",
            "example": "OPENPAYD"
          },
          "asset_code": {
            "enum": [
              "GBP",
              "EUR",
              "USD",
              "BTC",
              "ETH",
              "USDC",
              "USDT"
            ],
            "type": "string",
            "description": "Currency or crypto asset the fee row applies to. Fee rows exist for fiat as well as crypto. Accepted values: `GBP`, `EUR`, `USD`, `BTC`, `ETH`, `USDC`, `USDT`.",
            "example": "GBP"
          },
          "asset_scale": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Decimal places the three `*_minor` amounts on this row are expressed in (2 for GBP/EUR/USD, 8 for BTC, 18 for ETH, 6 for USDC/USDT). Read it from the row rather than inferring it from `asset_code`.",
            "format": "int32",
            "example": "2"
          },
          "customer_type": {
            "enum": [
              "Consumer",
              "Business"
            ],
            "type": "string",
            "description": "Which customer type the row prices: `Consumer` (a personal customer) or `Business` (a company). `GET customers/{customerId}/fees` returns only the customer's own type; commercial-config returns both. Accepted values: `Consumer`, `Business`.",
            "example": "Consumer"
          },
          "end_user_percentage_rate": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Percentage part of what the customer pays, as a FRACTION of the transaction amount: 0.01 means 1%, 0.005 means 0.5%, 0 means no percentage component. Applied before `end_user_fixed_minor` is added and before the minimum/maximum are enforced.",
            "format": "double",
            "example": "0.005"
          },
          "end_user_fixed_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Fixed part of what the customer pays, in minor units of `asset_code` at `asset_scale` — 100 with scale 2 is 1.00. Added to the percentage part. 0 means no fixed component.",
            "format": "double",
            "example": "100"
          },
          "end_user_minimum_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Floor on the total fee, in minor units at `asset_scale`; null when there is no floor. When the percentage-plus-fixed total is below this, this is charged instead.",
            "format": "double",
            "example": "50"
          },
          "end_user_maximum_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Cap on the total fee, in minor units at `asset_scale`; null when there is no cap. When the percentage-plus-fixed total is above this, this is charged instead.",
            "format": "double",
            "example": "5000"
          },
          "enabled": {
            "type": "boolean",
            "description": "Always true on these two routes: disabled rows are filtered out before the response is built. Carried for schema compatibility only.",
            "example": "true"
          },
          "effective_from": {
            "type": "string",
            "description": "When this version came into force (ISO 8601, UTC). Always in the past on these routes — only rows effective at the time of the call are returned, so a scheduled future price is not visible until it starts.",
            "format": "date-time",
            "example": "2026-08-01T00:00:00+00:00"
          },
          "effective_until": {
            "type": [
              "null",
              "string"
            ],
            "description": "When this version stops applying (ISO 8601, UTC), or null for open-ended. When set it is always in the future on these routes; an expired version is not returned.",
            "format": "date-time"
          }
        }
      },
      "PublicApiFormFieldResponse": {
        "required": [
          "name",
          "label",
          "type",
          "required",
          "options",
          "description"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Field key. For the three top-level fields it is the request property (`currency`, `provider`, `rail`). For provider inputs it is the full path, for example `provider_input_values.sector`: send the part after the prefix (`sector`) as the key inside `provider_input_values`.",
            "example": "currency"
          },
          "label": {
            "type": "string",
            "description": "Human-readable label for the field.",
            "example": "Currency"
          },
          "type": {
            "enum": [
              "select",
              "text",
              "date"
            ],
            "type": "string",
            "description": "Widget hint: `select` (choose from `options`), `text` or `date`. Accepted values: `select`, `text`, `date`.",
            "example": "select"
          },
          "required": {
            "type": "boolean",
            "description": "Whether the create route refuses a request without this field. Only `currency` is required; every provider input is optional at request time — but note some are needed for the account to progress (their `description` says when).",
            "example": "true"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The accepted values for a `select` field; empty for free-text and date fields.",
            "example": [
              "GBP"
            ]
          },
          "description": {
            "type": [
              "null",
              "string"
            ],
            "description": "Help text for the field, including when a provider input is required for the request to progress rather than merely accepted.",
            "example": "Accepted account currency."
          }
        }
      },
      "PublicApiIdentityData": {
        "required": [
          "customer_id",
          "subject_binding",
          "tenant_id",
          "api_key_prefix",
          "scopes",
          "delegated_customer_ids"
        ],
        "type": "object",
        "properties": {
          "customer_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "The customer this key is bound to. Use this parent id on nested end-customers routes; use an owned child's id on that child's banking, beneficiary and withdrawal routes. See [Sub-customers & parent access](/guides/sub-customers). Null for a tenant-wide key (it acts for every customer on the tenant and must name one explicitly) and null for an `unbound` key, which authenticates but resolves nothing.",
            "format": "uuid",
            "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
          },
          "subject_binding": {
            "enum": [
              "unbound",
              "subject",
              "tenant"
            ],
            "type": "string",
            "description": "What the key can reach. `subject`: one customer or business, plus the direct end-customers it onboarded (`delegated_customer_ids`). `tenant`: every customer on the tenant; lists are unfiltered. `unbound`: a legacy key with unknown provenance — it authenticates, but every list comes back empty and every by-id read is 404; ask Kwiikpay to reissue it. Accepted values: `unbound`, `subject`, `tenant`.",
            "example": "subject"
          },
          "tenant_id": {
            "type": "string",
            "description": "Your tenant's UUID, resolved from the key. Informational: never send it back as `X-Tenant-Id` — that header is refused with 400 on this surface.",
            "format": "uuid",
            "example": "9c1b2d3e-4f50-4a6b-8c7d-0e1f2a3b4c5d"
          },
          "api_key_prefix": {
            "type": [
              "null",
              "string"
            ],
            "description": "The public prefix of the key that made this call — the part before the dot, `kp_<environment>_<16 hex>` (for example `kp_live_0123456789abcdef`). Safe to log and to quote to support; it identifies the key without revealing the secret.",
            "example": "kp_live_0123456789abcdef"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The scopes this key holds, sorted, in the same colon vocabulary the reference uses — compare with the `Requires the … scope` line on an operation to diagnose a 403 without a ticket. A key stores the expanded set, so the umbrella names `transactions:read` / `transactions:write` never appear here. Every value the field can carry: `accounts:read`, `accounts:write`, `balances:read`, `compliance:read`, `crm-leads:write`, `deposits:read`, `end-customers:read`, `end-customers:write`, `end-user-fees:write`, `exchanges:read`, `exchanges:write`, `fees:read`, `onboarding:read`, `onboarding:write`, `open-banking:read`, `open-banking:write`, `payout-batches:read`, `payout-batches:write`, `wallets:read`, `wallets:write`, `webhooks:read`, `webhooks:write`, `withdrawal-addresses:read`, `withdrawal-addresses:write`, `withdrawals:read`, `withdrawals:write`.",
            "example": [
              "withdrawals:write"
            ]
          },
          "delegated_customer_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Additional customer ids this key may act for: the DIRECT end-customers the bound customer onboarded, one level deep, resolved on every request. Empty for `tenant` and `unbound` keys. This is why an id other than `customer_id` can sometimes resolve; a grandchild never does."
          }
        }
      },
      "PublicApiInternalTransferData": {
        "required": [
          "internal_transfer"
        ],
        "type": "object",
        "properties": {
          "internal_transfer": {
            "$ref": "#/components/schemas/PublicApiInternalTransferResponse"
          }
        }
      },
      "PublicApiInternalTransferListData": {
        "required": [
          "internal_transfers"
        ],
        "type": "object",
        "properties": {
          "internal_transfers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiInternalTransferResponse"
            }
          }
        }
      },
      "PublicApiInternalTransferRequest": {
        "required": [
          "direction",
          "currency",
          "amount_minor",
          "client_reference"
        ],
        "type": "object",
        "properties": {
          "direction": {
            "enum": [
              "parent_to_end_customer",
              "end_customer_to_parent"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "parent_to_end_customer debits the parent's Available fiat and credits the child by the same amount. end_customer_to_parent reverses that direction. Accepted values: `parent_to_end_customer`, `end_customer_to_parent`.",
            "example": "parent_to_end_customer"
          },
          "currency": {
            "enum": [
              "GBP",
              "EUR"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Fiat currency held by both eligible accounts. No currency conversion occurs in this call. Accepted values: `GBP`, `EUR`.",
            "example": "GBP"
          },
          "amount_minor": {
            "type": [
              "integer",
              "string"
            ],
            "description": "Positive whole number of pence/cents. Accepts a JSON number or quoted integer; use a string for exact large amounts. 25000 means GBP 250.00 or EUR 250.00.",
            "example": "25000"
          },
          "client_reference": {
            "type": "string",
            "description": "Non-empty UUID matching the Idempotency-Key header. Generate once per intended transfer; preserve it with the same request for retries and durable recovery.",
            "format": "uuid",
            "example": "785a23b2-a916-4f29-b8ed-0dcb4412fb09"
          },
          "origin_exchange_order_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "Optional audit link to a settled exchange owned by this parent whose fiat target matches currency. Only allowed for parent_to_end_customer. This does not convert funds or reserve that order's proceeds.",
            "format": "uuid"
          }
        }
      },
      "PublicApiInternalTransferResponse": {
        "required": [
          "id",
          "journal_entry_id",
          "direction",
          "currency",
          "amount_minor",
          "parent_customer_id",
          "end_customer_id",
          "client_reference",
          "origin_exchange_order_id",
          "created_at",
          "completed_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "journal_entry_id": {
            "type": "string",
            "format": "uuid"
          },
          "direction": {
            "type": "string"
          },
          "currency": {
            "enum": [
              "GBP",
              "EUR"
            ],
            "type": "string",
            "description": "Fiat currency transferred between the parent and sub-customer accounts. No conversion occurs. Accepted values: `GBP`, `EUR`.",
            "example": "GBP"
          },
          "amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Transferred whole pence/cents. 25000 means GBP 250.00 or EUR 250.00. Read amount_minor_string for exact large values.",
            "format": "double",
            "example": "25000"
          },
          "parent_customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "end_customer_id": {
            "type": "string",
            "format": "uuid"
          },
          "client_reference": {
            "type": "string",
            "format": "uuid"
          },
          "origin_exchange_order_id": {
            "type": [
              "null",
              "string"
            ],
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "enum": [
              "completed"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "completed means the balanced ledger posting and durable transfer record committed together. Accepted values: `completed`.",
            "example": "completed"
          },
          "amount_minor_string": {
            "type": [
              "null",
              "string"
            ],
            "description": "Exact decimal-string representation of the transferred minor units; safe for JavaScript without rounding.",
            "example": "25000"
          }
        }
      },
      "PublicApiKybStatusData": {
        "required": [
          "kyb_status",
          "customer_status"
        ],
        "type": "object",
        "properties": {
          "kyb_status": {
            "type": [
              "null",
              "string"
            ],
            "description": "Business verification state mapped from the lifecycle status: `pending`, `in_progress`, `approved`, `rejected`, `blocked`, `closed`, or an unmapped lifecycle status passed through. Null when the customer is a person (call the KYC route instead) — that is a 200, not an error.",
            "example": "in_progress"
          },
          "customer_status": {
            "type": "string",
            "description": "The raw lifecycle status the mapped value was derived from: `Draft`, `Onboarding`, `PendingCompliance`, `Active`, `Rejected`, `Suspended`, `Blocked` or `Closed`.",
            "example": "Onboarding"
          }
        }
      },
      "PublicApiKycStatusData": {
        "required": [
          "kyc_status",
          "customer_status"
        ],
        "type": "object",
        "properties": {
          "kyc_status": {
            "type": [
              "null",
              "string"
            ],
            "description": "Individual verification state mapped from the lifecycle status: `pending`, `in_progress`, `approved`, `rejected`, `blocked`, `closed`, or an unmapped lifecycle status passed through. Null when the customer is a business (call the KYB route instead) — that is a 200, not an error.",
            "example": "approved"
          },
          "customer_status": {
            "type": "string",
            "description": "The raw lifecycle status the mapped value was derived from: `Draft`, `Onboarding`, `PendingCompliance`, `Active`, `Rejected`, `Suspended`, `Blocked` or `Closed`.",
            "example": "Active"
          }
        }
      },
      "PublicApiOnboardingRequirementsData": {
        "required": [
          "level",
          "applicant_type",
          "required_fields",
          "required_documents",
          "questionnaire_required"
        ],
        "type": "object",
        "properties": {
          "level": {
            "enum": [
              "individual-onboarding",
              "uk-individual-kyc-level",
              "non-uk-individual-kyc-level",
              "kwiikpay-v2-business-unified-risk-profile"
            ],
            "type": "string",
            "description": "The Sumsub verification level these requirements describe, as resolved from `level`, `type` and `jurisdiction`. One of `individual-onboarding`, `uk-individual-kyc-level`, `non-uk-individual-kyc-level` or `kwiikpay-v2-business-unified-risk-profile`. When you submit, either omit `level` or send exactly this value. Accepted values: `individual-onboarding`, `uk-individual-kyc-level`, `non-uk-individual-kyc-level`, `kwiikpay-v2-business-unified-risk-profile`.",
            "example": "individual-onboarding"
          },
          "applicant_type": {
            "enum": [
              "individual",
              "company"
            ],
            "type": "string",
            "description": "Which submit route the level belongs to: `individual` means `POST /onboarding/kyc`, `company` means `POST /onboarding/kyb`. Accepted values: `individual`, `company`.",
            "example": "individual"
          },
          "required_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "JSON paths on the submit request body that this level requires, spelled exactly as you send them (for example `person.first_name`, `person.dob`, `company.address.post_code`). A submission missing any of them is refused with 422 missing_required_fields. That refusal names the same fields in an internal camelCase spelling (`person.firstName`, `person.dateOfBirth` for `dob`, `company.address.postCode`); map them back to the paths listed here.",
            "example": [
              "person.first_name"
            ]
          },
          "required_documents": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Human-readable list of the documents the level expects, for building an upload form — NOT machine-readable values. For individuals: one identity document (`type` PASSPORT, ID_CARD, RESIDENCE_PERMIT or DRIVERS, front and back where the document has two sides), a SELFIE and a UTILITY_BILL. For companies: COMPANY_DOC uploads and each beneficiary's identity, selfie and proof of residence. Send them as `documents` (KYC) or `company_documents` plus per-beneficiary `documents` (KYB).",
            "example": [
              "SELFIE"
            ]
          },
          "questionnaire_required": {
            "type": "boolean",
            "description": "Whether the level expects a `questionnaire` object in the submission. True for every level except `non-uk-individual-kyc-level`.",
            "example": "true"
          },
          "business_sectors": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "type": "string"
            },
            "description": "The banking provider's accepted `sector` values for a business, for rendering a dropdown — the KYB submit route validates `sector` against exactly this list (422 invalid_sector otherwise). Present only for the company level; null for individual levels.",
            "example": [
              "ACCOUNTING"
            ]
          }
        }
      },
      "PublicApiOnboardingSessionData": {
        "required": [
          "verification_link",
          "token",
          "applicant_id",
          "onboarding_case_id",
          "status"
        ],
        "type": "object",
        "properties": {
          "verification_link": {
            "type": [
              "null",
              "string"
            ],
            "description": "Hosted Sumsub verification URL to send the customer to (a browser flow; no API key involved). Null in two cases that are both 200s: the customer is already verified (`status: approved`), or the case was parked for manual review before Sumsub was called. Null means do not send anyone anywhere.",
            "example": "https://in.sumsub.com/websdk/p/..."
          },
          "token": {
            "type": [
              "null",
              "string"
            ],
            "description": "Sumsub Web SDK access token for embedding verification in your own UI, valid for 14 days from minting. Every call mints a fresh one, including a replay of an existing session. Null when the customer is already verified or the case is held for review — never launch the SDK on a null. An expired token fails inside the Sumsub SDK, not as an API error: call this route again.",
            "example": "_act-sbx-jwt-..."
          },
          "applicant_id": {
            "type": "string",
            "description": "Sumsub applicant identifier for this customer. Stable across calls for the same customer and level; useful when correlating with the Sumsub dashboard or support.",
            "example": "66b3f2a1c0ffee0001a1b2c3"
          },
          "onboarding_case_id": {
            "type": "string",
            "description": "KwiikPay's identifier for the verification case behind this session. One case per customer and verification kind; a replay returns the same id.",
            "format": "uuid",
            "example": "0198f3a0-1234-7000-8000-0000000000aa"
          },
          "status": {
            "type": "string",
            "description": "Where the customer stands in verification at the moment the session was minted — NOT the state of the session itself. Known values: `pending` (never started), `in_progress` (submitted, awaiting a decision), `approved` (verified; a session was still minted, so this is not an error), `rejected`, `blocked` (suspended or blocked), `closed`. Any other value is a lifecycle state passed through unmapped — treat an unrecognised value as not-yet-approved rather than failing on it. The end-customer verification route uses a narrower pair on this same field: `verified` when the end customer was already approved (no token is minted) and `pending` otherwise.",
            "example": "in_progress"
          }
        }
      },
      "PublicApiOnboardingStatusData": {
        "required": [
          "current_step",
          "steps",
          "kyc_status",
          "kyb_status",
          "can_proceed_to_kyb",
          "customer_status"
        ],
        "type": "object",
        "properties": {
          "current_step": {
            "enum": [
              "kyc",
              "kyb",
              "completed"
            ],
            "type": "string",
            "description": "Which verification step the customer is on. `kyc` for a person and `kyb` for a business while they are anything other than Active; `completed` once the customer is `Active`. It does not distinguish not-started from in-progress — read `kyc_status`/`kyb_status` for that (`steps` repeats them side by side, with the literal `not_applicable` for the kind that does not apply). Accepted values: `kyc`, `kyb`, `completed`.",
            "example": "kyc"
          },
          "steps": {
            "description": "Per-step verification state: `kyc` for the individual check and `kyb` for the business check. The step that does not apply to this customer type reads `not_applicable`.",
            "$ref": "#/components/schemas/PublicApiOnboardingStepsResponse"
          },
          "kyc_status": {
            "type": [
              "null",
              "string"
            ],
            "description": "Individual verification state, mapped from the customer's lifecycle status. Null for a BUSINESS customer. Known values: `pending` (created, verification never started), `in_progress` (session opened or decision pending), `approved` (Active — money movement allowed), `rejected`, `blocked` (Suspended or Blocked), `closed`. Any other value is a lifecycle status passed through unmapped; treat it as not-yet-approved.",
            "example": "in_progress"
          },
          "kyb_status": {
            "type": [
              "null",
              "string"
            ],
            "description": "Business verification state, mapped from the business's lifecycle status. Null for an INDIVIDUAL customer. Known values: `pending` (created, verification never started), `in_progress` (session opened or decision pending), `approved` (Active — money movement allowed), `rejected`, `blocked` (Suspended or Blocked), `closed`. Any other value is a lifecycle status passed through unmapped; treat it as not-yet-approved.",
            "example": "in_progress"
          },
          "can_proceed_to_kyb": {
            "type": "boolean",
            "description": "True only when the customer is a PERSON and is `Active`. It is always false for a business — including a business that is mid-KYB — so it cannot be used to gate KYB. It exists for a flow in which an approved individual goes on to register a company; it is not that company's state.",
            "example": "false"
          },
          "customer_status": {
            "type": "string",
            "description": "The customer's raw lifecycle status — the same value as `status` on the customer object and a DIFFERENT vocabulary from `kyc_status`/`kyb_status`. Values: `Draft`, `Onboarding`, `PendingCompliance`, `Active`, `Rejected`, `Suspended`, `Blocked`, `Closed`. Only `Active` may move money.",
            "example": "Onboarding"
          }
        }
      },
      "PublicApiOnboardingStepsResponse": {
        "required": [
          "kyc",
          "kyb"
        ],
        "type": "object",
        "properties": {
          "kyc": {
            "type": "string",
            "description": "Individual verification state (`pending`, `in_progress`, `approved`, `rejected`, `blocked`, `closed`, or an unmapped lifecycle status), or the literal `not_applicable` for a business.",
            "example": "in_progress"
          },
          "kyb": {
            "type": "string",
            "description": "Business verification state (`pending`, `in_progress`, `approved`, `rejected`, `blocked`, `closed`, or an unmapped lifecycle status), or the literal `not_applicable` for a person.",
            "example": "not_applicable"
          }
        }
      },
      "PublicApiPaginationResponse": {
        "required": [
          "current_page",
          "last_page",
          "per_page",
          "total"
        ],
        "type": "object",
        "properties": {
          "current_page": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The 1-based page this response holds: the `page` query parameter you sent, or 1 when it was omitted or below 1.",
            "format": "int32"
          },
          "last_page": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "How many pages exist at this `per_page` for the full matching set — `ceil(total / per_page)`, never 0 (an empty set reports 1, so `current_page <= last_page` always holds). Loop `while current_page < last_page`. Before 2026-09-01 this was always 1 regardless of how much data existed.",
            "format": "int32"
          },
          "per_page": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The page size actually applied: your `limit` clamped to 1–500 (default 100). Fewer rows than this on the last page is normal; fewer on an earlier page is not.",
            "format": "int32"
          },
          "total": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "The number of matching records across ALL pages — a real count taken in a separate query before the page was read — not the number of rows in this response. Before 2026-09-01 it reported the row count of the page just returned, so a caller that treated it as \"rows received\" will now see a larger number.",
            "format": "int32"
          }
        }
      },
      "PublicApiPayoutBatchData": {
        "required": [
          "payout_batch"
        ],
        "type": "object",
        "properties": {
          "payout_batch": {
            "description": "The payout batch, with `items` populated.",
            "$ref": "#/components/schemas/PublicApiPayoutBatchResponse"
          }
        }
      },
      "PublicApiPayoutBatchItemResponse": {
        "required": [
          "id",
          "sequence",
          "beneficiary_id",
          "amount",
          "reference",
          "status",
          "fiat_payment_id",
          "failure_code",
          "failure_reason"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Payout batch item UUID.",
            "format": "uuid",
            "example": "0198e2f4-6b7c-7d3a-9f1e-2c4a5b6d7e8f"
          },
          "sequence": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "1-based position of this item in the `items` array you sent. Skipped items keep their slot and items are always returned in sequence order, so this is the ONLY reliable key for matching a response item to your own row — on a Skipped item `amount` and `reference` can differ from what you sent.",
            "format": "int32",
            "example": "1"
          },
          "beneficiary_id": {
            "type": "string",
            "description": "UUID of the stored beneficiary this item pays, echoed from the request.",
            "format": "uuid",
            "example": "0198e2f4-6b7c-7d3a-9f1e-2c4a5b6d7e8f"
          },
          "amount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "MINOR units at a scale of 2. On a Skipped item this echoes the value as supplied, even a malformed one — with ONE exception: an item skipped for a reference over 256 characters whose amount was ALSO invalid is recorded with amount 1, and its failure_reason names only the reference. Match response items to your rows on `sequence`, never on amount.",
            "format": "double",
            "example": "2000"
          },
          "reference": {
            "type": [
              "null",
              "string"
            ],
            "description": "Payment reference recorded on the item, trimmed. Null when you sent none, null or a blank string (the item is then paid with \"Batch payout\"), and null when the value exceeded 256 characters — it is dropped rather than truncated, so this field never carries text you did not send. A reference that broke a rail rule is kept as sent, on a Skipped item.",
            "example": "Invoice ABC"
          },
          "status": {
            "enum": [
              "Pending",
              "Executing",
              "Completed",
              "Failed",
              "Skipped"
            ],
            "type": "string",
            "description": "Item status. Pending: valid at intake, waiting for the batch to be approved and executed. Skipped: refused at intake (`failure_code` = validation); terminal, never executes. Completed: a fiat withdrawal was CREATED for this item — it does NOT mean the money settled; the withdrawal may still be pending at the provider or under manual review, and the item never changes again. Follow `fiat_payment_id` for settlement. Failed: the withdrawal was refused or errored, see `failure_code`. Executing: a submission was in flight when it was interrupted, or a concurrent run owns it; it does not resolve on its own and a batch holding one never reaches a terminal status — contact support. Accepted values: `Pending`, `Executing`, `Completed`, `Failed`, `Skipped`.",
            "example": "Pending"
          },
          "fiat_payment_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "UUID of the fiat withdrawal created for this item. Set once, when the item becomes Completed; null otherwise. It is the `id` of a row in GET .../banking/withdrawals — that row, not the batch item, carries settlement status and any failure after submission.",
            "format": "uuid",
            "example": "0198e2f4-6b7c-7d3a-9f1e-2c4a5b6d7e8f"
          },
          "failure_code": {
            "type": [
              "null",
              "string"
            ],
            "description": "Machine-readable reason the item is Skipped or Failed; null on Pending, Executing and Completed items. Known values: `validation` (refused at intake — bad amount, bad reference, beneficiary missing/unapproved/wrong currency); at execution `subject_unavailable`, `subject_inactive` (the customer is no longer Active), `beneficiary_not_payable` (the beneficiary changed since intake), `configuration` (the customer's account or ledger is not ready for this currency and provider), `source_account_pot_missing` (a linked-business account that has never received a deposit — pay into it first), `withdrawal_failed` (the provider refused the payment; no funds are held) and `withdrawal_error` (the submission threw: compliance block, limit, provider fault). Treat the list as open. Nothing is retried automatically: fix the cause and create a new batch for those items.",
            "example": "validation"
          },
          "failure_reason": {
            "type": [
              "null",
              "string"
            ],
            "description": "Human-readable sentence behind `failure_code`, at most 500 characters. For `withdrawal_error` it is the raw error message and its wording is not stable — branch on `failure_code`, never on this text. Null whenever `failure_code` is null.",
            "example": "amount must be a positive whole minor-unit value."
          },
          "transparency": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PaymentTransparencySnapshot"
              }
            ],
            "description": "Immutable disclosure captured for this item, including the institution identity resolved from the customer's policy. Null means no disclosure was captured, not an own-funds declaration or verification. A skipped invalid declaration has no accepted snapshot."
          }
        }
      },
      "PublicApiPayoutBatchListData": {
        "required": [
          "payout_batches",
          "pagination"
        ],
        "type": "object",
        "properties": {
          "payout_batches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiPayoutBatchResponse"
            },
            "description": "One page of this customer's batches, newest first (`created_at` then `id` descending), each with `items` set to null. There is no status or date filter on this route; `pagination.total` is the real number of batches for the customer."
          },
          "pagination": {
            "description": "Paging metadata for this list. `page` and `limit` (1-500) on the request are honoured; `total` counts every matching record across all pages, not the rows in this response, and `last_page` is `ceil(total / per_page)` (never 0). Loop `page` from 1 to `last_page`. See the Pagination guide.",
            "$ref": "#/components/schemas/PublicApiPaginationResponse"
          }
        }
      },
      "PublicApiPayoutBatchResponse": {
        "required": [
          "id",
          "customer_id",
          "currency",
          "rail",
          "status",
          "item_count",
          "success_count",
          "failure_count",
          "total_amount",
          "approved_at",
          "completed_at",
          "created_at",
          "updated_at",
          "items"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Payout batch UUID. Poll GET .../payout-batches/{batchId} with it to follow the batch and its per-item outcomes — no webhook fires for any batch transition.",
            "format": "uuid",
            "example": "0198e2f4-6b7c-7d3a-9f1e-2c4a5b6d7e8f"
          },
          "customer_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "UUID of the customer the batch pays out for. On this API it is always the customerId from the request path.",
            "format": "uuid",
            "example": "0198e2f4-6b7c-7d3a-9f1e-2c4a5b6d7e8f"
          },
          "currency": {
            "type": "string",
            "description": "Currency the batch and every item are denominated in, uppercased. GBP, EUR or USD — a batch in any other currency has been refused at intake with 422 `unsupported_currency` since 2026-09-02 (before that, an AUD batch could be accepted by the rail gate and was always Rejected).",
            "example": "GBP"
          },
          "rail": {
            "type": [
              "null",
              "string"
            ],
            "description": "The canonical Fiat Republic scheme the batch was stored with — the RESOLVED value, not what you sent: FASTER_PAYMENTS is returned as FPS, SEPA and SEPA_INSTANT as SCT. One of FPS, CHAPS, BACS, SCT, ACH, ACH_SAME_DAY, DOMESTIC_WIRE, INTERNATIONAL_WIRE, DE or NPP. Compare against this vocabulary, never against your own input. Never null on a batch created through this API.",
            "example": "FPS"
          },
          "status": {
            "enum": [
              "Draft",
              "PendingApproval",
              "Approved",
              "Rejected",
              "Executing",
              "Completed",
              "PartiallyCompleted",
              "Failed"
            ],
            "type": "string",
            "description": "Batch lifecycle status. A batch you create is born Draft — or Rejected when every item was Skipped at intake (the call still returns HTTP 200, so branch on this field, never on the status code). No partner API route advances a batch: a Kwiikpay operator submits it for approval (PendingApproval), a DIFFERENT operator approves (Approved) or rejects (Rejected), and execution (Executing) settles into Completed when every non-skipped item completed, Failed when none did, or PartiallyCompleted for a mix. Draft, PendingApproval, Approved and Executing are in flight; Rejected, Completed, PartiallyCompleted and Failed are terminal. Poll GET to observe transitions. Accepted values: `Draft`, `PendingApproval`, `Approved`, `Rejected`, `Executing`, `Completed`, `PartiallyCompleted`, `Failed`.",
            "example": "Draft"
          },
          "item_count": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Number of items in the batch INCLUDING items Skipped at intake — the length of the items array you sent, at most 500.",
            "format": "int32",
            "example": "3"
          },
          "success_count": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Number of items in status Completed, i.e. items for which a fiat withdrawal was created. Always 0 until an operator executes the batch, so 0 on a fresh batch means nothing has run yet, not that nothing succeeded.",
            "format": "int32",
            "example": "0"
          },
          "failure_count": {
            "pattern": "^-?(?:0|[1-9]\\d*)$",
            "type": [
              "integer",
              "string"
            ],
            "description": "Number of items in status Failed OR Skipped, counted together. On a freshly created batch it is exactly the number of items refused at intake — nothing has been executed, so do not report these as failed payments. Recomputed from the items on every transition, never set on its own.",
            "format": "int32",
            "example": "1"
          },
          "total_amount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "MINOR units at a scale of 2. The sum of ALL item amounts — Skipped and Failed items included, and any substituted amount of 1 (see the item amount) — so it is NOT the amount that will be paid. Compute the payable figure yourself from the items in status Pending (before execution) or Completed (after).",
            "format": "double",
            "example": "6000"
          },
          "approved_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "UTC ISO-8601 timestamp of the operator's approval. Null until the batch is Approved; stays null on a Rejected batch.",
            "format": "date-time",
            "example": "2026-09-02T10:15:00+00:00"
          },
          "completed_at": {
            "type": [
              "null",
              "string"
            ],
            "description": "UTC ISO-8601 timestamp at which execution reached a terminal status (Completed, PartiallyCompleted or Failed). Null before that, and null forever on a Rejected batch.",
            "format": "date-time",
            "example": "2026-09-02T10:20:00+00:00"
          },
          "created_at": {
            "type": "string",
            "description": "UTC ISO-8601 timestamp the batch was created. Lists are ordered by this, newest first.",
            "format": "date-time",
            "example": "2026-09-02T10:00:00+00:00"
          },
          "updated_at": {
            "type": "string",
            "description": "UTC ISO-8601 timestamp of the last change to the batch row (status, counters or timestamps).",
            "format": "date-time",
            "example": "2026-09-02T10:20:00+00:00"
          },
          "items": {
            "type": [
              "null",
              "array"
            ],
            "items": {
              "$ref": "#/components/schemas/PublicApiPayoutBatchItemResponse"
            },
            "description": "Per-item outcomes, ordered by `sequence`. Populated on create and on GET by id; ALWAYS null in the list response — read a single batch to see its items."
          }
        }
      },
      "PublicApiStartEndCustomerOnboardingRequest": {
        "required": [
          "contact_email"
        ],
        "type": "object",
        "properties": {
          "return_url": {
            "type": [
              "null",
              "string"
            ],
            "description": "Absolute URL the end customer returns to after the hosted verification flow. Optional; must be absolute if supplied.",
            "example": "https://partner.example.com/verification/complete"
          },
          "contact_email": {
            "type": [
              "null",
              "string"
            ],
            "description": "Contact email for the end customer. REQUIRED for individuals and businesses alike — the banking provider demands one for every account holder, and an omission is refused with contact_email_required.",
            "example": "jane.doe@example.com"
          },
          "business_profile": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "$ref": "#/components/schemas/PublicApiBusinessProfileRequest"
              }
            ],
            "description": "Company details for KYB. REQUIRED when the end customer is a Business (refused with business_profile_required when absent) and refused on a Personal end customer."
          }
        }
      },
      "PublicApiSupportResponse": {
        "required": [
          "email"
        ],
        "type": "object",
        "properties": {
          "email": {
            "type": [
              "null",
              "string"
            ],
            "description": "Always null today. There is no per-tenant support address configured through this API; use the contact your account manager gave you."
          }
        }
      },
      "PublicApiTestResponse": {
        "required": [
          "success",
          "message",
          "tenant_id"
        ],
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true. This body is served only when the key authenticated; a failed key never reaches it (401 with the flat `ErrorResponse` shape instead). Note this response is NOT wrapped in the `{success, status_code, message, data}` envelope every other v1 200 uses — it is the flat object.",
            "example": "true"
          },
          "message": {
            "type": "string",
            "description": "Fixed text, currently \"API key authentication working!\". Not a status vocabulary; do not branch on it.",
            "example": "API key authentication working!"
          },
          "tenant_id": {
            "type": "string",
            "description": "Tenant (partner) id the key belongs to. The same value `GET /api/v1/partner/me` returns as `tenant_id`.",
            "format": "uuid",
            "example": "0198f3a0-0000-7000-8000-00000000c0de"
          }
        }
      },
      "PublicApiTransactionListData": {
        "required": [
          "transactions",
          "pagination"
        ],
        "type": "object",
        "properties": {
          "transactions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiTransactionResponse"
            },
            "description": "One row per ledger journal on this fiat account, newest first (by the journal's occurred-at time, then by id). A journal is one accounting event — a deposit arriving, a deposit moving from pending to available, a withdrawal being reserved or settled — so rows are not one-to-one with deposits and withdrawals. Rows whose legs net to zero on this account (for example a hold reserving funds from available into reserved) are included with `amount` 0. Page with `page` and `limit`; `pagination.total` is the real journal count."
          },
          "pagination": {
            "description": "Paging metadata for this list. `page` and `limit` (1-500) on the request are honoured; `total` counts every matching record across all pages, not the rows in this response, and `last_page` is `ceil(total / per_page)` (never 0). Loop `page` from 1 to `last_page`. See the Pagination guide.",
            "$ref": "#/components/schemas/PublicApiPaginationResponse"
          }
        }
      },
      "PublicApiTransactionResponse": {
        "required": [
          "id",
          "amount",
          "currency",
          "description",
          "status",
          "fee_minor",
          "net_amount_minor",
          "spot_rate",
          "fee_rate",
          "effective_rate",
          "sender",
          "created_at",
          "metadata"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Ledger journal id (UUID). Stable: the same accounting event has the same id on every read, so it is the key to deduplicate on. It is NOT the deposit or withdrawal id — one deposit can produce more than one journal (pending, then available), each with its own id.",
            "format": "uuid",
            "example": "0198f3a0-1c2d-7e4f-8a9b-0c1d2e3f4a5b"
          },
          "amount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "MINOR units of the account's currency, SIGNED: positive is money INTO the account (a deposit landing, a withdrawal returned), negative is money OUT (a withdrawal settling, a fee charged), and 0 for a journal that only moves money between the account's own buckets (a withdrawal hold being reserved, a deposit moving from pending to available). It is the net of the journal's legs on this account, so summing every row gives the account's balance across all buckets — not just the spendable one. Expect negative and zero values.",
            "format": "double",
            "example": "-2000"
          },
          "currency": {
            "enum": [
              "GBP",
              "USD",
              "EUR"
            ],
            "type": "string",
            "description": "Currency of the account, and therefore of `amount`, `fee_minor` and `net_amount_minor`. This is a response field: the fiat account was issued in GBP, EUR or USD and every journal on it is in that currency. Accepted values: `GBP`, `USD`, `EUR`.",
            "example": "GBP"
          },
          "description": {
            "type": [
              "null",
              "string"
            ],
            "description": "Internal ledger memo of the journal, for example \"Confirm fiat deposit to customer available balance.\" Free text, not a stable vocabulary and not the payer's reference — do not parse it. May be null.",
            "example": "Confirm fiat deposit to customer available balance."
          },
          "status": {
            "enum": [
              "completed"
            ],
            "type": "string",
            "description": "Always `completed`. Only posted ledger journals appear here, and a posted journal is final; a pending deposit shows as a journal on the pending bucket, not as a non-completed row. Read deposit and withdrawal lifecycle state from the deposits and withdrawals lists instead. Accepted values: `completed`.",
            "example": "completed"
          },
          "fee_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Always null on this route. Fees are not broken out per journal here; a fee charged on a deposit or withdrawal appears as its own journal row with a negative `amount`. Null means \"not reported\", not \"no fee\".",
            "format": "double"
          },
          "net_amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Always equal to `amount` on this route (same sign, same minor units). It is not an amount net of fees — `fee_minor` is never populated here.",
            "format": "double",
            "example": "-2000"
          },
          "spot_rate": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Always null on this route. Exchange rates are not recorded on fiat ledger journals; read `executed_rate` on the conversion request instead.",
            "format": "double"
          },
          "fee_rate": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Always null on this route. A fee rate is reported on conversion requests and on fee configuration rows, not on ledger journals.",
            "format": "double"
          },
          "effective_rate": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Always null on this route. Read `executed_rate` on the conversion request for the rate a conversion actually filled at.",
            "format": "double"
          },
          "sender": {
            "type": [
              "null",
              "string"
            ],
            "description": "Provider code recorded on the journal — the banking provider (for example `FIAT_REPUBLIC` or `OPENPAYD`) whose payment produced it — or null when the journal was not tied to a provider payment (an internal move between balance buckets, for example). It is NOT the name of the person or business who paid. Read `sender` on the fiat deposit list/detail for the provider-reported payer name when captured.",
            "example": "OPENPAYD"
          },
          "created_at": {
            "type": "string",
            "description": "When the journal was posted (ISO 8601, UTC). This is the posting time, not the time the provider received the payment; rows are ordered by this field, newest first.",
            "format": "date-time",
            "example": "2026-08-31T10:15:30.1234567+00:00"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Exactly four string keys, always present, empty string when unknown: `ledger_account_id` (the internal ledger pot this row is on — not the fiat account id in the path), `source_command_id` (the internal money-movement command that posted the journal; the deposit or withdrawal id when the journal came from one), `provider` (same value as `sender`) and `provider_reference` (the provider's own reference for the payment, when it was tied to one). `provider_reference` matches `provider_reference` on the deposits and withdrawals lists, which is how a journal is linked back to the payment that caused it."
          }
        }
      },
      "PublicApiTransferPriceData": {
        "required": [
          "pair",
          "price",
          "all_in_price",
          "timestamp",
          "expires_at",
          "source_amount_minor",
          "destination_amount_minor",
          "fee_amount_minor",
          "fee_currency",
          "fee_rate",
          "net_destination_amount_minor"
        ],
        "type": "object",
        "properties": {
          "pair": {
            "type": "string",
            "description": "The pair quoted, echoed as SOURCE/TARGET in upper case exactly as the platform resolved it (for example `GBP/USDC`).",
            "example": "GBP/USDC"
          },
          "price": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "INDICATIVE rate: destination major units per one source major unit, BEFORE the customer-facing fee (compare `all_in_price`). Not executable and not locked — creating a conversion re-prices at the live rate.",
            "format": "double",
            "example": "1.2701"
          },
          "all_in_price": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Public API customer-facing destination-per-source rate after fee impact when fee is charged from source.",
            "format": "double",
            "example": "1.2345"
          },
          "timestamp": {
            "type": "string",
            "description": "When this price was computed, UTC ISO-8601. Pair it with `expires_at` to know how long the provider considers it current.",
            "format": "date-time",
            "example": "2026-06-17T11:46:49.000Z"
          },
          "expires_at": {
            "type": "string",
            "description": "Public API UTC timestamp when the provider quote expires.",
            "format": "date-time",
            "example": "2026-05-22T12:15:30Z"
          },
          "source_amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Public API source amount in atomic/minor units.",
            "format": "double",
            "example": "100"
          },
          "destination_amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Public API destination amount in atomic/minor units.",
            "format": "double",
            "example": "24500000"
          },
          "fee_amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "Public API fee amount in atomic/minor units.",
            "format": "double",
            "example": "50"
          },
          "fee_currency": {
            "type": "string",
            "description": "Public API currency or asset code of the customer-facing fee.",
            "example": "GBP"
          },
          "fee_rate": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Public API fee divided by source amount when both use the same asset/scale.",
            "format": "double",
            "example": "0.005"
          },
          "net_destination_amount_minor": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "null",
              "number",
              "string"
            ],
            "description": "Public API destination amount after any destination-side fee impact.",
            "format": "double",
            "example": "24500000"
          }
        }
      },
      "PublicApiValidationData": {
        "required": [
          "code",
          "detail"
        ],
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable reason for a refusal raised by the operation itself (409 or 422). Always present, snake_case, stable. The vocabulary is OPEN: every code an operation can raise is listed per operation in the Errors guide (`/guides/errors`), but the two crypto operations pass workflow decision codes through verbatim (`insufficient_funds`, `crypto_withdrawals_disabled`, …) and new ones ship without notice. Branch on the codes you handle and treat an unknown code as a refusal that will not clear by retrying unchanged. This is a different namespace from `ErrorResponse.code`.",
            "example": "invalid_amount"
          },
          "detail": {
            "type": "string",
            "description": "Human-readable explanation of `code`, for an operator or a log line. It may embed values from your request or a message from the banking or custody provider, so it is not stable — show it, do not parse it.",
            "example": "amount_minor must be a positive integer in minor units."
          }
        }
      },
      "PublicApiVerificationCallbackData": {
        "required": [
          "status",
          "received_at",
          "received_parameters"
        ],
        "type": "object",
        "properties": {
          "status": {
            "enum": [
              "received"
            ],
            "type": "string",
            "description": "Always the literal `received`. It says the request arrived, nothing about any verification. Accepted values: `received`.",
            "example": "received"
          },
          "received_at": {
            "type": "string",
            "description": "Server time the request was received, ISO-8601 UTC.",
            "format": "date-time",
            "example": "2026-09-02T10:15:30.000000+00:00"
          },
          "received_parameters": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "YOUR OWN query string, echoed back as a flat string map. Any key whose name contains `token`, `secret`, `signature` or `key` is returned as `[redacted]`. Nothing here comes from Sumsub."
          }
        }
      },
      "PublicApiWalletAddressData": {
        "required": [
          "wallet",
          "address",
          "tag"
        ],
        "type": "object",
        "properties": {
          "wallet": {
            "description": "The wallet record the address belongs to. On this response the three balance figures are always 0 regardless of what the wallet holds — read `GET .../crypto/wallets` or the balance route for live balances.",
            "$ref": "#/components/schemas/PublicApiWalletResponse"
          },
          "address": {
            "type": [
              "null",
              "string"
            ],
            "description": "The deposit address to give the customer, or null. Identical to `wallet.address`. Null when the address has not been issued yet (create-address accepted but pending) or when the customer has no address for the asset and network (balance route). A null here with a 200 is NOT an address — do not display it.",
            "example": "0x0000000000000000000000000000000000000000"
          },
          "tag": {
            "type": [
              "null",
              "string"
            ],
            "description": "Always null on this API. Deposit addresses are issued one per customer per asset and network; there is no shared-address-plus-memo deposit model, so never ask a depositor for a tag."
          }
        }
      },
      "PublicApiWalletListData": {
        "required": [
          "wallets"
        ],
        "type": "object",
        "properties": {
          "wallets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiWalletResponse"
            },
            "description": "Every issued (Active) deposit address the customer holds, newest first, one row per asset and network. Unbounded and unpaginated — there is no `limit` on this route. Rows for the same asset on different networks repeat the same pooled balance; do not sum them."
          }
        }
      },
      "PublicApiWalletResponse": {
        "required": [
          "id",
          "asset_code",
          "network",
          "address",
          "balance",
          "reserved_balance",
          "available_balance",
          "balance_atomic",
          "reserved_balance_atomic",
          "available_balance_atomic",
          "status",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identifier of the deposit-address record. On GET crypto/wallets and on the create-address response it is always a real deposit-address id. On GET crypto/wallets/{asset}/balance it FALLS BACK when the customer has no address for that asset on its default network: first to the customer's pooled ledger account id, then to 00000000-0000-0000-0000-000000000000. Treat it as an address identifier only when `address` is non-null.",
            "format": "uuid",
            "example": "018f6a2e-3b7c-7d2a-9e11-4c9f0a1b2c3d"
          },
          "asset_code": {
            "enum": [
              "BTC",
              "ETH",
              "USDC",
              "USDT",
              "TRX"
            ],
            "type": "string",
            "description": "Crypto asset held in this wallet. Includes TRX. Accepted values: `BTC`, `ETH`, `USDC`, `USDT`, `TRX`.",
            "example": "USDC"
          },
          "network": {
            "enum": [
              "BITCOIN",
              "ETHEREUM",
              "TRON"
            ],
            "type": "string",
            "description": "Blockchain network the address lives on. Per asset: BTC is BITCOIN; ETH and USDC are ETHEREUM; TRX is TRON; USDT may be ETHEREUM or TRON, one address per network. GET crypto/wallets/{asset}/balance always reports the asset's DEFAULT network (USDT → ETHEREUM) and never returns a TRON USDT address. Funds sent on a different network from the one shown here are lost. Accepted values: `BITCOIN`, `ETHEREUM`, `TRON`.",
            "example": "ETHEREUM"
          },
          "address": {
            "type": [
              "null",
              "string"
            ],
            "description": "The on-chain deposit address, or null. Null on the create-address response when the request was accepted but the custody provider has not yet issued the address (see the operation description), and on GET crypto/wallets/{asset}/balance when the customer has no address for that asset and network. Never null on GET crypto/wallets, which lists issued addresses only. Branch on this field, not on the HTTP status.",
            "example": "0x0000000000000000000000000000000000000000"
          },
          "balance": {
            "type": "string",
            "description": "Total ledger balance of the ASSET in major units, rendered with exactly the asset's scale of decimals (BTC 8, ETH 18, USDC/USDT/TRX 6). It is keyed on the asset, not the address: a customer holding USDT on both ETHEREUM and TRON sees the same pooled USDT total on both wallet rows, so do not sum rows. It includes amounts currently reserved by open withdrawals and, on GET crypto/wallets, every ledger bucket including pending postings. The create-address response hardcodes this to 0 regardless of holdings.",
            "example": "40.000000"
          },
          "reserved_balance": {
            "type": "string",
            "description": "Amount of the asset currently held by active ledger reservations (withdrawals that are submitted or awaiting review), in major units at the asset's scale. Keyed on the asset, not the address. Hardcoded to 0 on the create-address response.",
            "example": "31.090000"
          },
          "available_balance": {
            "type": "string",
            "description": "max(0, balance − reserved_balance), in major units at the asset's scale. This is a books figure, not a per-network one: a withdrawal on a specific network can still be refused with insufficient_network_funds when the asset is spread across networks. Hardcoded to 0 on the create-address response.",
            "example": "8.910000"
          },
          "balance_atomic": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The same total as `balance`, in atomic units (satoshi, wei, 10⁻⁶ USDC/USDT/TRX) as a JSON number. At ETH's scale of 18 this exceeds JavaScript's Number.MAX_SAFE_INTEGER (0.01 ETH is 10000000000000000): JavaScript clients must read the string form `balance` instead of parsing this.",
            "format": "double",
            "example": "40000000"
          },
          "reserved_balance_atomic": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The same figure as `reserved_balance`, in atomic units as a JSON number. Exceeds JavaScript's safe-integer range for ETH — read the string form in JavaScript.",
            "format": "double",
            "example": "31090000"
          },
          "available_balance_atomic": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "The same figure as `available_balance`, in atomic units as a JSON number. Exceeds JavaScript's safe-integer range for ETH — read the string form in JavaScript.",
            "format": "double",
            "example": "8910000"
          },
          "status": {
            "enum": [
              "Requested",
              "SubmittedToProvider",
              "Active",
              "ProviderPending",
              "ManualReviewRequired",
              "Blocked",
              "Failed",
              "Settled",
              "Unknown"
            ],
            "type": "string",
            "description": "Lifecycle state of the deposit-address request, from the vocabulary Requested, SubmittedToProvider, Active, ProviderPending, ManualReviewRequired, Blocked, Failed, Settled — plus the literal Unknown. What you will actually see depends on the route: GET crypto/wallets lists only issued addresses, so it is always Active; the create-address response is Active when `address` is present and otherwise Requested or ManualReviewRequired; GET crypto/wallets/{asset}/balance returns the newest request's state with no filter (any value above) or Unknown when the customer has no address for that asset and network. Only Active means the address can receive funds. Accepted values: `Requested`, `SubmittedToProvider`, `Active`, `ProviderPending`, `ManualReviewRequired`, `Blocked`, `Failed`, `Settled`, `Unknown`.",
            "example": "Active"
          },
          "created_at": {
            "type": "string",
            "description": "When the deposit-address request was created, ISO-8601 with offset. On GET crypto/wallets/{asset}/balance with no address it is the time of your request, not a creation time.",
            "format": "date-time",
            "example": "2026-08-20T10:15:30+00:00"
          },
          "updated_at": {
            "type": "string",
            "description": "When the deposit-address request last changed state, ISO-8601 with offset. On GET crypto/wallets/{asset}/balance with no address it is the time of your request.",
            "format": "date-time",
            "example": "2026-08-20T10:15:30+00:00"
          }
        }
      },
      "PublicApiWithdrawalData": {
        "required": [
          "withdrawal"
        ],
        "type": "object",
        "properties": {
          "withdrawal": {
            "description": "The withdrawal as recorded. Read its `status`: the route returns 200 whether the payment was submitted, refused by the provider (`Failed`, funds released) or held (`ManualReview`).",
            "$ref": "#/components/schemas/PublicApiWithdrawalResponse"
          }
        }
      },
      "PublicApiWithdrawalListData": {
        "required": [
          "withdrawals",
          "pagination"
        ],
        "type": "object",
        "properties": {
          "withdrawals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicApiWithdrawalResponse"
            },
            "description": "The customer's fiat withdrawals for this page, newest first, in every status including `Failed`. Crypto withdrawals are not included; deposits are on the deposits route."
          },
          "pagination": {
            "description": "Paging metadata for this list. `page` and `limit` (1-500) on the request are honoured; `total` counts every matching record across all pages, not the rows in this response, and `last_page` is `ceil(total / per_page)` (never 0). Loop `page` from 1 to `last_page`. See the Pagination guide.",
            "$ref": "#/components/schemas/PublicApiPaginationResponse"
          }
        }
      },
      "PublicApiWithdrawalResponse": {
        "required": [
          "id",
          "type",
          "status",
          "amount",
          "currency",
          "asset_code",
          "destination",
          "created_at",
          "updated_at"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Kwiikpay's id for the withdrawal (the payment row). Stable across status changes; it is the id a `withdrawal.completed` webhook refers to.",
            "format": "uuid",
            "example": "7a2c9e10-1b3d-4e5f-8a6b-0c1d2e3f4a5b"
          },
          "type": {
            "enum": [
              "fiat",
              "crypto"
            ],
            "type": "string",
            "description": "Which kind of withdrawal this row is. The fiat routes always return `fiat`; `crypto` appears only on the crypto withdrawal routes, which share this shape. Accepted values: `fiat`, `crypto`.",
            "example": "fiat"
          },
          "status": {
            "type": "string",
            "description": "Status of the withdrawal. On a FIAT row the vocabulary is `Pending`, `PendingProvider`, `Completed`, `Failed`, `ManualReview`, `Returned`, `Closed`. The create route returns 200 for every outcome, so branch here, never on the HTTP code: `PendingProvider` means submitted to the bank (final outcome arrives as `withdrawal.completed` or on the list route); `Failed` means the provider refused before anything left and the reserved funds were released; `ManualReview` means either a transaction-monitoring hold or an ambiguous provider outcome — funds stay reserved until an operator resolves it; `Completed` means paid. Crypto rows carry the crypto withdrawal vocabulary instead, documented on those routes. No closed list is published because the two vocabularies share this field.",
            "example": "PendingProvider"
          },
          "amount": {
            "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$",
            "type": [
              "number",
              "string"
            ],
            "description": "ATOMIC/minor units, for both fiat and crypto. A crypto withdrawal therefore takes major units on the request and reports atomic units here, on the same resource.",
            "format": "double",
            "example": "2000"
          },
          "currency": {
            "enum": [
              "EUR",
              "GBP",
              "USD"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Fiat currency of the withdrawal — always set on a fiat row, null on a crypto row (which uses `asset_code`). Accepted values: `EUR`, `GBP`, `USD`.",
            "example": "GBP"
          },
          "asset_code": {
            "enum": [
              "BTC",
              "ETH",
              "USDC",
              "USDT",
              "TRX"
            ],
            "type": [
              "null",
              "string"
            ],
            "description": "Crypto asset of the withdrawal. Includes TRX. Accepted values: `BTC`, `ETH`, `USDC`, `USDT`, `TRX`.",
            "example": "USDC"
          },
          "destination": {
            "type": [
              "null",
              "string"
            ],
            "description": "On a fiat row: the provider's reference for the outgoing payment, null until the provider has accepted it — it is NOT the beneficiary id. On a crypto row: the destination address.",
            "example": "pay_01j8x2y3z4"
          },
          "created_at": {
            "type": "string",
            "description": "When Kwiikpay recorded the withdrawal, UTC ISO-8601. The list route sorts by this field, newest first.",
            "format": "date-time",
            "example": "2026-06-17T11:46:49.000Z"
          },
          "updated_at": {
            "type": "string",
            "description": "Last status change, UTC ISO-8601.",
            "format": "date-time",
            "example": "2026-06-17T11:47:02.000Z"
          },
          "transaction_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "Fiat only, null on a crypto row. The banking provider's own transaction id for the payout (e.g. OpenPayd's transaction UUID) — an identifier support can use to look the payment up with the provider directly, distinct from `destination`. Null until the provider has accepted the payment, and null for a provider that does not report one.",
            "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
          },
          "end_to_end_reference": {
            "type": [
              "null",
              "string"
            ],
            "description": "Fiat only, null on a crypto row. The SEPA/SWIFT end-to-end reference quoted to the receiving bank on this payout — the identifier a customer's own bank can look the incoming payment up by. Null on rails/providers that do not support one and on rows predating this field.",
            "example": "3FA85F6457174562B3FC2C963F66AFA6"
          },
          "provider_short_id": {
            "type": [
              "null",
              "string"
            ],
            "description": "Fiat only, null on a crypto row. A human-readable, bank-recognisable reference the provider generates for the payout (e.g. OpenPayd's `shortId`, formatted like a date plus a short code) — the identifier to read out over the phone to a bank, in place of the opaque `transaction_id`. Null until the provider reports one.",
            "example": "20220922-K3QY65"
          }
        }
      }
    },
    "securitySchemes": {
      "PublicApiKey": {
        "type": "apiKey",
        "description": "Your Kwiikpay API key. Send it as X-Api-Key, or as Authorization: Bearer.",
        "name": "X-Api-Key",
        "in": "header"
      }
    }
  },
  "tags": [
    {
      "name": "Customers",
      "description": "Read and manage your customer record."
    },
    {
      "name": "Sub-customers",
      "description": "Operate your own direct sub-customers using the parent's API key. The routes call them end-customers. Creating a child, starting verification and issuing accounts require onboarding eligibility for both tenant and parent. See [Sub-customers & parent access](/guides/sub-customers) for beneficiaries, fiat withdrawals and the complete request sequence."
    },
    {
      "name": "Onboarding",
      "description": "KYC/KYB verification: initiate flows, issue SDK tokens, read status."
    },
    {
      "name": "Exchange",
      "description": "Price quotes and conversion requests between fiat and crypto."
    },
    {
      "name": "Fiat Accounts",
      "description": "Virtual bank accounts (vIBANs), balances, and account transactions."
    },
    {
      "name": "Fiat Deposits",
      "description": "What arrived: fiat received by a customer, including anything still held for review."
    },
    {
      "name": "Fiat Withdrawals",
      "description": "Pay fiat out to a beneficiary."
    },
    {
      "name": "Fiat Beneficiaries",
      "description": "Saved payout destinations for fiat withdrawals."
    },
    {
      "name": "Fees",
      "description": "Commercial configuration and per-customer fee overrides."
    },
    {
      "name": "Crypto Wallets",
      "description": "Per-asset deposit addresses and wallet balances."
    },
    {
      "name": "Crypto Deposits",
      "description": "What arrived on-chain, with the transaction hash for each deposit."
    },
    {
      "name": "Crypto Withdrawals",
      "description": "Send crypto to an external address."
    },
    {
      "name": "API Configuration",
      "description": "Connectivity check and your tenant's public API configuration."
    }
  ]
}