{
  "openapi": "3.1.0",
  "info": {
    "title": "OZAV API",
    "version": "2.0.0",
    "summary": "Canonical OZAV platform API (v2)",
    "description": "The OZAV API v2 is the canonical, single interface to OZAV's full-stack\ncrypto and banking-as-a-service platform. New here? Read **What is OZAV**\nand **Choose your integration** below, then check **What's available\ntoday** - applications are open, endpoints are enabled per organization,\nand not every resource is callable yet.\n\n## What is OZAV\n\nOZAV is a full-stack crypto and banking-as-a-service platform. You\nintegrate once and get identity onboarding and compliance, cross-border FX,\nand wallets/custody - behind one API and one settlement layer, instead of\nwiring up a different counterparty for each capability.\n\nOne API serves four integration profiles from the **same** canonical\nendpoints: a full-stack neobank, a liquidity-only integration, a\ncompliance-only integration, or a wallets integration. You never call a\ndifferent API for a different shape of business - access is governed by\n**entitlements** (the products enabled for your organization) and **key\nscopes** (what a given key may do). Call `GET /capabilities` first; it\nreturns exactly which entitlements, scopes, and features your key has.\n\nWhat you get, grouped:\n\n- **Onboarding & compliance** - create customers, run KYC/KYB\n  verifications, and screen subjects against designated lists.\n- **FX & cross-border** - price a transfer with transparent exchange\n  conditions (a spread, in basis points) and move money across corridors.\n- **Accounts** - expose customer-owned named fiat receiving instructions\n  under an OZAV-owned `ozav_acc_` account id. BRL/Pix is the first\n  sandbox-active rail; future fiat rails use the same resource.\n- **Wallets & custody** - read managed and self-custody wallets and, for\n  self-custody, build unsigned transactions the end-user signs.\n- **Cards** - issue and manage OZAV virtual cards through one canonical\n  v2 surface, with sandbox issuance available and live programs enabled per\n  organization.\n\n**The machine-readable contract lives at a stable URL.** Fetch\n`https://ozav.io/openapi.yaml` (or `.json`) and generate a client. It is the same\nfile this page renders, copied at build time, so the URL and the page never drift.\n\n```\nnpx openapi-typescript https://ozav.io/openapi.json -o ozav.d.ts\n```\n\nOZAV is provider-neutral: pricing is always expressed as **exchange\nconditions** (a spread in integer basis points), amounts are decimal\nstrings (never floats), identifiers are opaque and prefixed (`ozav_cus_…`,\n`ozav_q_…`, `ozav_tr_…`, `ozav_acc_…`), and the environment (test vs live)\nis derived from your key.\n\n## Changelog\n\nBehavior changes that a generated client cannot see. The spec version is\nunchanged because nothing here removes or renames a field, but the wire\nshape you receive can differ, so read this before assuming a field is\nalways present.\n\n- **2026-09-10 - `account_status` says whether the account can operate, and\n  the refusal has its own code.** Two additions and one renamed error, all from\n  the same gap: an approved customer whose account is not operable had no field\n  saying so. `Customer.account_status` (`active` | `pending`) is that field.\n  `requirements.due_codes` carries the same list as `due` in stable English\n  tokens, using the `missing_fields` vocabulary; `due` itself is unchanged.\n\n  The refusal from `POST /accounts` for a customer whose verification is\n  `approved` but whose account is not active now returns `422\n  ACCOUNT_NOT_ACTIVATED` instead of `422 KYC_NOT_APPROVED`, and its message\n  points at `requirements.due`. The old code said the customer must be approved\n  while `status` already read `approved`.\n\n  What this means for you. Both fields are additive - ignore them and nothing\n  changes. If you switch exhaustively on error codes, add\n  `ACCOUNT_NOT_ACTIVATED`; `KYC_NOT_APPROVED` still fires for a customer whose\n  verification really has not been approved. And stop inferring operability from\n  `status`: read `account_status`.\n- **2026-09-10 - An approved customer can still be waiting on you, and\n  `requirements.owner` now says so.** A customer whose verification came back\n  `approved` is not necessarily able to move money: the account also has to\n  carry the data a withdrawal requires (holder name, a valid tax id, and a\n  street + city address). When it does not, `status` is `approved`,\n  `requirements.due` names exactly what is missing, and the account stays\n  inactive - `Wallet.status` reads `inactive`, and `POST /accounts` and\n  `POST /cards` refuse.\n\n  What changed on this date is the `owner` field. Before, that state returned\n  `owner: none`, which this spec documents as \"the decision is final\" - and\n  integrators read it as \"nothing to do\", so the customer sat there. It now\n  returns `integrator` whenever the decision is `approved` and `due` is\n  non-empty. A `rejected` decision still returns `none`: no address reverses a\n  verdict, and sending you to collect one would cost your customer a trip for\n  nothing.\n\n  What this means for you. Read `requirements.due` even when\n  `decision.status` is `approved` - `documents_due` and `decision.status`\n  alone no longer tell you everything that is owed. Send the named field with\n  `PATCH /customers/{customer_id}`: the check re-runs on that call, and when\n  it passes, the account activates and `due` comes back empty in the same\n  response. Until then the refusal from `POST /accounts` and `POST /cards`\n  surfaces as `KYC_NOT_APPROVED`, whose message says the customer must be\n  approved; when `status` is already `approved`, the real reason is\n  `requirements.due`, not the verification.\n- **2026-09-08 - The decision, the clock and the document verdict are on the\n  customer.** `Customer.decision` says whether the customer was approved and,\n  if not, why (closed `reason_code`) and whose move it is\n  (`requirements.owner`); `Customer.sla` is OZAV's response clock (48 business\n  hours to decide once the record is complete; 30 days for a document and 15\n  for a correction on your side); `requirements.documents_due[]` now names the\n  `doc_type` to upload; and `CustomerDocument.review` is OZAV's own verdict on\n  a supporting document, with a closed `reason_code` when rejected. New\n  webhook `document.reviewed`. `customer.updated` is now also emitted on a\n  manual approval, not only on rejection. Nothing was removed or renamed.\n- **2026-09-05 - A verification can be born already decided when OZAV has\n  verified that person before.** `POST /verifications` for an individual\n  whose tax identity OZAV has already verified in a hosted session (for any\n  organization, within the validity window) no longer starts a new hosted\n  session: `session_url` is `null`, and `status`/`checks` for the hosted\n  identity capabilities arrive with the reused outcome instead of\n  `pending`/`not_started`. Nothing about the other organization is\n  disclosed. Treat `session_url: null` together with a non-`pending`\n  status as \"no user step needed\". Off by default; enabled per\n  environment by OZAV.\n- **2026-08-31 - The Travel Rule now follows the customer's country instead of\n  requiring a Brazilian tax document from everyone.** An external crypto send\n  previously demanded `cpf` or `cnpj` from every originator, in both\n  environments, which made the call impossible for a customer outside Brazil.\n  Accepted documents are now resolved per country: `BR` keeps `cpf`/`cnpj`,\n  `NG` accepts `bvn`/`nin`/`passport`/`national_id`, and any other served\n  country accepts `passport`/`national_id`.\n\n  Why: the requirement was one hardcoded predicate with no country parameter,\n  on an API that serves customers in several countries.\n\n  What this means for you. Nothing breaks for a Brazilian customer - that path\n  is unchanged. The country comes from the customer record - what you declared\n  when creating the customer, not the country inside your `travel_rule` payload -\n  so it is not selectable per request. Correcting it later via\n  `PATCH /customers/{id}` is allowed and now passes the same jurisdiction check as\n  creation. Two smaller changes ship with it:\n  `beneficiary.wallet_attested_ownership_at` must now be a real ISO-8601\n  timestamp that is not in the future (any non-empty string was accepted\n  before), and the `missing_fields` entry for the document now names the types\n  accepted for that country rather than always `[cpf|cnpj]`.\n\n- **2026-08-30 - Webhook deliveries are now scoped to the environment of\n  the key that produced the event.** An endpoint registered with a live key\n  no longer receives sandbox events, and a sandbox endpoint no longer\n  receives live ones.\n\n  Why: a single endpoint receiving both meant test traffic reached\n  production handlers, and there was no way to tell the two apart from the\n  payload alone.\n\n  What this means for you. If you registered one endpoint and relied on it\n  catching both environments, register one per environment - otherwise half\n  your deliveries stop arriving. Endpoints registered through the dashboard\n  before this change carry no environment and still receive both; that is\n  being addressed separately.\n\n- **2026-08-30 - `POST /wallets` refuses self-custody registration on\n  networks with no way out.** `tron` and `stellar` now return `422\n  CHAIN_HAS_NO_SELF_CUSTODY_CAPABILITY` instead of `201`.\n\n  Why: registration succeeded on all nine networks, and the dead end only\n  appeared on the next call - `POST /transfers` does not accept a\n  self-custody source, and `transaction-intents` builds EVM transactions\n  only. A wallet that cannot move is worse than a refusal, because it looks\n  like it worked.\n\n  What this means for you. Use managed custody on those two networks. Note\n  `POST /customers` still accepts them in `self_custody.chain`, since they\n  remain valid networks elsewhere in the API. `solana` remains registrable\n  but is not served by `transaction-intents` either - it unlocks the\n  bring-your-own-wallet yield leg.\n\n- **2026-08-30 - Card transactions and authorizations carry `presentment`,\n  and `400` responses name the failing field.** `presentment` holds the\n  merchant's own amount and currency; an international purchase is\n  `presentment.currency != currency`. The key is ABSENT when the issuer did\n  not report it and is never back-filled with the card currency, so treat\n  absent as unknown, not as domestic. `error.param` now names the failing\n  field on body validation, across the whole API. The card activity shape is\n  also typed in the spec now (`CardActivity`); it was previously untyped.\n\n  Why: the issuer's currency was being discarded, so there was no way to\n  identify an international purchase from the API. Comparing\n  `merchant.country` answers a different question and gets it wrong for a\n  merchant registered in one country and billing in another.\n\n  What this means for you. Nothing breaks: `presentment` is additive and\n  optional. `simulate/purchase` accepts it so you can exercise the handling\n  in sandbox first.\n\n- **2026-08-25 - `requirements.missing_fields` reports seven more fields on\n  every customer.** The closed `MissingField` enum went from 19 to 26\n  values. For an `individual` the list now also reports\n  `monthly_income_usd`, `occupation`, `nationality`, `is_pep` and\n  `account_purpose`; for a `business`, `annual_revenue_usd`. Two more,\n  `regulator` and `regulated_activity_details`, are CONDITIONAL and appear\n  only while `business.is_regulated_activity` is `true`.\n\n  Why: the economic-capacity assessment compares the expected volume\n  against a declared capacity, and the API asked only for the volume. In the\n  30 days before this change, 322 of 328 assessments returned no verdict\n  because the capacity was never collected.\n\n  What this means for you. Nothing server-side is gated on this list, so no\n  existing customer is newly blocked by OZAV and no request that worked\n  yesterday fails today. What changes is the CONTENT of an informational\n  array: a customer that returned `missing_fields: []` yesterday can return\n  up to nine tokens now. Two integration patterns are affected. If you\n  switch exhaustively on the enum, add the seven values. If you treat a\n  non-empty `missing_fields` as \"block the user\", read the next entry first,\n  because that pattern has a second problem that predates this change.\n\n- **2026-08-31 - `owners` now clears, and `representative` joins it.**\n  Supersedes the 2026-08-25 entry below. `GET /customers/{customer_id}`\n  resolves the associated persons and reports both tokens truthfully, so an\n  empty `missing_fields` is reachable again for a Brazilian business. The\n  LIST endpoint still does not resolve them (it would cost one query per\n  row), so treat `owners` and `representative` on a list page as unresolved\n  rather than outstanding. `representative` is a SEPARATE requirement from\n  `owners`: a company whose only person is `roles: [\"ubo\"]` at 100% has an\n  owner but nobody who can sign, and an account cannot be opened for it in\n  any currency.\n\n- **2026-08-25 - `owners` is reported for every business, including one\n  whose UBOs are already on file.** Superseded on 2026-08-31 by the entry\n  above; kept because integrations written against that behaviour need to\n  know it changed. It described a real limitation: neither read path\n  supplied the qualifying-owner count, so the token stayed in the list for a\n  Brazilian business no matter how many associated persons you created.\n\n- **2026-08-23 - `/wallets/{wallet_id}/onchain-balances` lists only the\n  networks offered to your organization.** It used to list every network on\n  which OZAV happened to hold an address for you, drawn from the full\n  `Network` enum. It now lists the same set `/deposit-addresses` serves:\n  the proven-withdrawal ceiling intersected with your configuration. No\n  field was removed or renamed, so a generated client cannot see this - what\n  changes is that entries for networks you were never offered stop arriving.\n  If you key a map by `network` or iterate `data` expecting one entry per\n  address, expect fewer entries. Balances already sitting at an address on a\n  withdrawn network are unaffected on-chain. Read\n  `x-ozav-network-availability` on the `Network` schema for which networks\n  apply to you.\n\n- **2026-08-19 - `approved` no longer implies the customer can operate.**\n  OZAV now activates a customer only when it also holds the holder name, a\n  valid tax document for the declared country, and an address (a Brazilian\n  company with a valid CNPJ is exempt). A customer can sit at\n  `verification.status: approved` and still be refused with `422` on\n  `POST /accounts` and the money paths. The wire shape does not change, so a\n  generated client cannot see this: read `CustomerStatus` for what to send.\n\n- **2026-08-23 - `failure_reason` on an account is now a code list, and is absent more\n\n  often.** It used to be whatever the underlying institution wrote, verbatim. It is now a\n\n  comma-separated list of the codes documented on the `Account` schema, or absent. If you\n\n  displayed the string to an end user it will read differently; if you parsed it, parse the\n\n  codes instead. Account webhooks carry the same object, so payloads persisted before this\n\n  date hold the old shape.\n\n- **2026-08-19 - `ach` is no longer served, in any environment.** The rail\n  moved from `sandbox only` to **not offered**: `POST /accounts` with\n  `currency: USD, rail: ach` now answers `422 ACCOUNT_RAIL_NOT_AVAILABLE`\n  with a `test` key too, where it previously succeeded. This is a\n  commercial decision, not an outage, and no deprecation window preceded\n  it. **The enum value stays published** so records already carrying it keep\n  reading; only new requests are refused. `POST /payout-destinations` with\n  `rail: ach` answers `422 RAIL_NOT_AVAILABLE` from the provider registry,\n  which is a different code from a different source of truth.\n\n- **2026-08-19 - a new organization no longer receives every product.**\n  Organizations are created entitled to customer management, KYC/KYB,\n  liquidity and managed custody. **`cards`, `kyc_reliance` and\n  `self_custody` are no longer granted at creation**, so a fresh sandbox key\n  calling any `/cards` route now answers `403 ENTITLEMENT_REQUIRED` where it\n  previously succeeded. Ask OZAV to enable them; they then appear in\n  `GET /capabilities`. Organizations created before this date keep what they\n  already hold.\n\n- **2026-08-19 - `AssociatedPerson.verification.mode` can now be absent.**\n  It used to be populated on every associated person. It is now inherited\n  from the customer, and omitted when the customer has no recorded\n  verification posture. Absent is not `full`: it means OZAV has no posture\n  on record, and `status` must not be read as an attestation while `mode` is\n  absent. Affects `POST`, `GET` and `PATCH` on\n  `/customers/{customer_id}/associated-persons`. Same semantics the\n  2026-08-18 entry below describes for `CustomerVerification.mode`.\n\n- **2026-08-18 - incomplete customer records no longer fail the call.**\n  `GET /customers` and `GET /customers/{customer_id}` used to answer\n  `500 CUSTOMER_MAPPING_FAILED` when any record lacked a canonical\n  verification block, and on the list endpoint that failed the entire page.\n  Such records now return `200` with `verification.mode`, `individual`,\n  `business` and `metadata` omitted, and `requirements.missing_fields`\n  naming what is still needed. **`verification.mode` was always populated in\n  practice before this date**, so code that reads it without a presence\n  check (`verification.mode.toUpperCase()`, `d[\"mode\"]`) can now fail on\n  records it never used to receive. The field was already optional in the\n  schema; only the practice changed. See `CustomerVerification.mode`.\n\n## Choose your integration\n\nPick the profile that matches what you are building. Each lists what it is\nfor, the resources it uses, and - honestly - what is callable **today**\nversus what is on the roadmap. Availability is per-organization; confirm\nyours with `GET /capabilities`.\n\n### Full-stack neobank\n\n*Embed onboarding, compliance, FX, and wallets end-to-end.* Resources:\nCustomers, Verifications, Screenings, Quotes, Transfers, Accounts, Payout\nDestinations, Wallets, Cards.\n\n**Today:** customer and verification **records** persist and read back, and\nscreening and quoting are live. Sandbox verification simulation approves\nKYC and creates simulated wallet and BRL/Pix account projections for\napproved customers with complete BR data. BRL/Pix accounts, payout\ndestinations, cards, and transfer paths are served. **Gated in\nlive:** automatic wallet provisioning, live named-account opening, live card\nissuance, and full end-to-end settlement for every transfer path.\n\n### Liquidity-only\n\n*Price and move money for customers you onboarded elsewhere.* Resources:\nQuotes, Transfers, Accounts, Payout Destinations.\n\nQuotes price supported corridors with real exchange conditions. Named fiat\naccounts and payout destinations are created through the same contract in\nevery corridor OZAV serves; the sandbox returns deterministic instructions so\nyou can build and test the full flow before moving value.\n\n### Compliance-only\n\n*Screening and verification as a standalone service - no settlement.*\nResources: Screenings, Verifications.\n\n**Today:** screenings and verifications are both live and complete\nend-to-end. Screen a person, business, or existing customer against\ndesignated lists and get a real result; start an identity verification and\nreceive a hosted session URL to hand to the account holder, then poll the\nverification or receive the outcome on a webhook.\n\n### Wallets\n\n*Read wallets and build unsigned transactions for custody use cases.*\nResources: Wallets, Transaction Intents.\n\nWallet read routes are tenant-scoped. They return real\nwallet records when custody provisioning already exists, but provisioning\nitself is still gated. **On the roadmap:** balances read empty until custody\naccounts exist, and unsigned-transaction building (transaction intents) is\nnot yet callable.\n\n## What's available today\n\nEndpoints are enabled per organization, and `GET /capabilities` is the\nsource of truth for what **your** key can call.\n\n### Getting a key\n\nApplications are open - you do not need an introduction to start.\n\n1. **Apply** at [ozav.io/caas/apply](https://ozav.io/caas/apply). No key\n   required, and no sales call to get through the door.\n2. **We review it.** This step is a human at OZAV, not an automated\n   approval - it is the compliance review, and it is deliberate.\n3. **Sandbox opens with the core products entitled.** Every organization is\n   created with customer management, KYC/KYB, liquidity, and managed\n   custody. You receive a portal invite, and from *Developers -> API Keys*\n   you mint your own `ozav_sk_test_` keys, scoped how you choose, up to ten\n   per environment. No one at OZAV is in that loop.\n\n   Cards, KYC reliance, and self-custody wallets are **not** granted at\n   creation. They are commercial products: ask OZAV to enable them and they\n   appear in `GET /capabilities` for your organization.\n4. **Production is a separate, explicit activation.** Live `ozav_sk_live_`\n   keys are issued by OZAV when your organization is activated. That gate\n   is a risk decision and stays manual on purpose.\n\nSo sandbox key management is fully self-serve once you are approved; only\nthe initial review and the production activation involve us.\n\n| Tier | Resources | What it means |\n|---|---|---|\n| **Live** | Capabilities, Quotes, Screenings, Evidence Packs, Verifications, API Keys, Webhook Endpoints, Events, Customers, Documents, Transaction Intents, Wallet Transfers, Transfer Batches, Wallet Policy, Approvals, Accounts, Wallets, Self-custody Wallets, Deposit Addresses, Cards, Transfers, Unified Transactions, Payout Destinations, KYC Reliance, Yield, Swap | Serves real records against your entitlements. Where a specific corridor, rail or program is outside your entitlements, the request answers a named error rather than a partial result. |\n\n| **Coming soon** | Network Transfers, Contract Calls, Typed Messages, Client Tokens, Client Nft Collections | Defined and partly served: the read route returns real state, but the route that moves value is unreachable by construction. See below. |\n\n**Network Transfers** moves your own USDC between networks. The read route\n(`GET /network-transfers/{transfer_id}`) serves real state today. The route\nthat performs the transfer is deliberately not callable: the write scope is\nabsent from the only scope bootstrap, so no API key can hold it, and pricing\nis unconfigured - an absent price refuses rather than charging zero. Both\nare opened by a decision, not by a deploy.\n\nFrom 2026-08-16 to 2026-08-24 there was no \"coming soon\" row at all. A\nsurface that is defined but not yet served gets its own row, with the\nreason - which is what Network Transfers does above.\n\nThis table lists every surface `GET /capabilities` reports, and nothing\nelse. A resource absent from all three tiers does not exist in v2.\n\nEvery operation carries an `x-ozav-availability` marker (`live`, `beta`, or\n`coming_soon`) that matches its routed feature in `GET /capabilities`.\n\n## Accounts and wallets are different things\n\nThey are asked for separately, they hold different value, and they demand\ndifferent evidence. Integrators conflate them constantly, so here is the\ncontrast in one place.\n\nAn **account** (`ozav_acc_`) is a named fiat receiving instruction. \"Named\"\nis the load-bearing word: the instruction carries your end customer's\nidentity rather than being a shared OZAV address. That is why it needs\nidentity - a bank credits a person, and the person has to be established\nbefore the instruction exists.\n\nHow literal \"named\" is depends on the rail. On Brazilian rails the account\nis held in your end customer's own name at the institution. On the\nforeign-currency rails the holder of record is OZAV, and the account is\nsegregated and attributed to your end customer - the same money separation,\na different legal holder. If who holds the account at the institution\nmatters to your contracts, treat those two as different products.\n\nAccounts are created with `POST /accounts`, which requires an `approved`\ncustomer and can still refuse: activation for the money paths is a further\ninternal step with no public field (see \"`approved` is not the finish\nline\").\n\nA **wallet** (`ozav_wal_`) holds crypto. It comes in two kinds, and only one\nof them is ever created through the API:\n\n- **managed** - OZAV holds the key. It is provisioned as part of the\n  customer when your organization has the custody product; you never request\n  it directly.\n- **self-custody** - your end user holds the key and the address already\n  exists. `POST /wallets` **records** it. Nothing is created, and OZAV never\n  holds those funds.\n\n| | account | wallet |\n|---|---|---|\n| what it holds | fiat, at a bank | crypto, on a chain |\n| created by | `POST /accounts` | managed: with the customer. self-custody: `POST /wallets` |\n| needs identity? | yes - customer `approved`, and activation can still refuse | no - self-custody records an address |\n| needs an address? | no | self-custody: yes, and it must match the chain |\n\nRead `POST /wallets` for what recording an address does and does not prove.\n\n⚠️ Recording a self-custody address needs no identity, but it does need the\n`self_custody` entitlement on your organization - a newly created\norganization does not get it by default and receives `403\nENTITLEMENT_REQUIRED`. A customer that already holds a managed wallet is\nrefused with `409 WALLET_ALREADY_MANAGED`: a customer is one or the other,\nnever both.\n\n## What each customer type must provide\n\nThe required set is driven by the **country on the tax identity**, not by\nnationality. A Brazilian tax id (`cpf`, or `country: BR`) puts the customer\non the Brazilian profile; anything else puts them on the base profile. Send\n`nationality` if you have it - it feeds risk assessment and will not change\na single required field.\n\n`POST /customers` always requires `type`, `email`, `legal_name` and the tax\nidentity for the type - and a tax identity is three parts, not one:\n`value`, `type` and `country`. Every address also carries `country`.\n\nThe tables below are the **request fields**: send all of them and the\ncustomer is created and complete. They are not a transcription of\n`requirements.missing_fields`, which reports one flat token per concept from\na closed list (`phone`, `residential_address`, `tax_id`, ...) and never a\nsub-path like `phone.country_code`. Read the tables to build the request;\nread `missing_fields` to find out what a customer you already created is\nstill missing.\n\n#### Individual, tax id issued in Brazil\n\n| field | rule |\n|---|---|\n| `individual.legal_name` | full legal name |\n| `individual.tax_id.value` | the CPF; its check digit is verified |\n| `individual.mother_name` | full name |\n| `individual.date_of_birth` | ISO date |\n| `individual.phone.country_code` | dial code |\n| `individual.phone.number` | subscriber number |\n| `email` | top level, not inside the block |\n| `individual.residential_address.line1` | street |\n| `individual.residential_address.number` | required in Brazil |\n| `individual.residential_address.neighborhood` | required in Brazil |\n| `individual.residential_address.city` | non-blank |\n| `individual.residential_address.state` | required in Brazil |\n| `individual.residential_address.postal_code` | required in Brazil |\n| `individual.residential_address.country` | `BR`; required on every address |\n| `individual.source_of_funds` | declaration |\n| `individual.expected_monthly_volume_usd` | declaration |\n| `individual.monthly_income_usd` | declaration; the capacity the expected volume is measured against |\n| `individual.occupation` | profession |\n| `individual.nationality` | ISO 3166-1 alpha-2 |\n| `individual.is_pep` | politically-exposed-person declaration; `false` counts as answered |\n| `individual.account_purpose` | why the account is being opened |\n\n#### Individual, tax id issued outside Brazil\n\n| field | rule |\n|---|---|\n| `individual.legal_name` | full legal name |\n| `individual.tax_id.value` | `ssn`, `nif`, `passport` or `other`; no check digit is applied |\n| `email` | top level |\n| `individual.date_of_birth` | ISO date; every individual owes it, not only Brazilian ones |\n| `individual.phone.country_code` | dialling code, e.g. `+1`; a foreign-currency account cannot be opened without it |\n| `individual.phone.number` | national number |\n| `individual.residential_address.line1` | street |\n| `individual.residential_address.city` | non-blank |\n| `individual.residential_address.postal_code` | required for any account, not only Brazilian ones: the named-account holder registration refuses without it |\n| `individual.residential_address.country` | ISO 3166-1 alpha-2 |\n| `individual.phone.country_code` | dialling code, digits only, no `+` |\n| `individual.phone.number` | mobile; a landline is not accepted for the named-account holder record |\n| `individual.source_of_funds` | declaration |\n| `individual.expected_monthly_volume_usd` | declaration |\n| `individual.monthly_income_usd` | declaration; the capacity the expected volume is measured against |\n| `individual.occupation` | profession |\n| `individual.nationality` | ISO 3166-1 alpha-2 |\n| `individual.is_pep` | politically-exposed-person declaration; `false` counts as answered |\n| `individual.account_purpose` | why the account is being opened |\n\n#### Business, registration issued in Brazil\n\n| field | rule |\n|---|---|\n| `business.legal_name` | full legal name |\n| `business.registration.value` | the CNPJ; its check digit is verified |\n| `business.phone.country_code` | dial code |\n| `business.phone.number` | subscriber number |\n| `email` | top level |\n| `business.registered_address.line1` | street |\n| `business.registered_address.number` | required in Brazil |\n| `business.registered_address.neighborhood` | required in Brazil |\n| `business.registered_address.city` | non-blank |\n| `business.registered_address.state` | required in Brazil |\n| `business.registered_address.postal_code` | required in Brazil |\n| `business.registered_address.country` | `BR`; required on every address |\n| `business.company_type` | legal form |\n| `owners` | at least one associated person owning 25% or more |\n| `representative` | at least one associated person who can sign for the company (`director`, `officer` or `authorized_signer`). Separate from `owners`: a sole owner with only the `ubo` role does not satisfy it |\n| `business.source_of_funds` | declaration |\n| `business.expected_monthly_volume_usd` | declaration |\n| `business.annual_revenue_usd` | declaration; the capacity the expected volume is measured against |\n| `business.expected_daily_volume_usd` | declaration; not derived from the monthly figure |\n| `business.is_regulated_activity` | boolean; `false` counts as answered |\n| `business.nature_of_business` | what the company actually does |\n| `business.website` | public site or profile |\n| `business.account_purpose` | why the account is being opened |\n| `business.incorporation_date` | ISO date |\n\n#### Business, registration issued outside Brazil\n\n| field | rule |\n|---|---|\n| `business.legal_name` | full legal name |\n| `business.registration.value` | `ein`, `company_number` or `other`; no check digit is applied |\n| `email` | top level |\n| `business.phone.country_code` | dialling code, e.g. `+1`; a foreign-currency account cannot be opened without it |\n| `business.phone.number` | national number |\n| `owners` | at least one associated person owning **25% or more**. Corrected on 31 Aug 2026: only the Brazilian branch asked for this, so a foreign company could report `ready: true` having never declared a beneficial owner. The 25% floor is FATF, not a Brazilian rule |\n| `representative` | at least one associated person who can sign (`director`, `officer` or `authorized_signer`); without one the account holder stays pending at the bank and never receives coordinates |\n| `business.registered_address.line1` | street |\n| `business.registered_address.city` | non-blank |\n| `business.registered_address.postal_code` | required for any account, not only Brazilian ones: the named-account holder registration refuses without it |\n| `business.registered_address.country` | ISO 3166-1 alpha-2 |\n| `business.phone.country_code` | dialling code, digits only, no `+` |\n| `business.phone.number` | mobile; a landline is not accepted for the named-account holder record |\n| `business.source_of_funds` | declaration |\n| `business.expected_monthly_volume_usd` | declaration |\n| `business.annual_revenue_usd` | declaration; the capacity the expected volume is measured against |\n| `business.expected_daily_volume_usd` | declaration; not derived from the monthly figure |\n| `business.is_regulated_activity` | boolean; `false` counts as answered |\n| `business.nature_of_business` | what the company actually does |\n| `business.website` | public site or profile |\n| `business.account_purpose` | why the account is being opened |\n| `business.incorporation_date` | ISO date |\n\n#### Conditional: a business that declares a regulated activity\n\nThe four tables above list what is asked of every customer of that shape.\nOne requirement is conditional and therefore is not a row in them.\n\nWhen `business.is_regulated_activity` is `true`, two further fields become\nrequired and appear in `requirements.missing_fields`:\n\n| field | rule |\n|---|---|\n| `business.regulator` | name of the supervising authority |\n| `business.regulated_activity_details` | licence or scope of the regulated activity |\n\nThey are asked for **only** on a `true` answer. `false` is an answer - the\ncompany has declared it is not regulated, and asking who supervises it would\nbe asking for the detail of something it denied. An **absent**\n`is_regulated_activity` is an open question: `missing_fields` asks for the\nquestion itself and not for its follow-up, so you are never asked to name a\nregulator for an activity you never said was regulated.\n\n#### Opening an account in USD, EUR or GBP\n\n🔴 **Read this before you build against it.** `sepa`, `swift` and\n`faster_payments` are **live behind enablement** - the corridor is served in\nproduction, but opening an account for a given customer also requires that\nOZAV has enabled that customer for foreign-currency named accounts. A `live`\nrequest for a customer who is not enabled is refused with a named error that\nsays so.\n\n`fedwire` stays `sandbox only`: no US-domiciled account has ever been issued\non this corridor. A USD account is issued as a **GB IBAN** and reached by\n`swift` - that is the shape the provider returns, and the one to build\nagainst.\n\n`Rail.x-ozav-rail-availability` is the authoritative table; this paragraph\ndescribes it and is checked against it.\n\nThe field list below is published so you can build the request now and not\nchange it later - it is what the holder record asks for. It is not a\nstatement that the account opens on a `live` key today. Availability is\nanswered by that table, never by this section.\n\n**Business customers today, and the reason is ours, not the bank's.** A\nforeign-currency account is opened against a holder record filed as a\ncompany, so `type: individual` is refused there for now. Corrected on 31 Aug\n2026: an earlier version of this page said there was \"no individual path\"\nand that an individual \"will not hold\" such an account even once the\ncorridors open. That read as a permanent product rule and it is not one -\nthe institution behind the corridor does document individual holders. What\nis missing is OZAV's own branch for them.\n\nThe refusal stays until that branch exists, and it is the right refusal:\nopening one today would file a natural person as a company, with their\npersonal tax id standing in for both the company's tax id and its\nregistration number. A wrong filing at the institution is worse for the\ncustomer than a clear \"not yet\".\n\nAn individual can still be onboarded, verified, and hold a wallet, and BRL\ncorridors are unaffected by this paragraph.\n\n🔴 **And the same is true of BRL/Pix today - which this page did not say,\nand the sandbox does not enforce.** No Brazilian Pix provider currently\nopens a named account for an individual: all five declare\n`supportsIndividualAccount: false`. A `live` key asking for a BRL named\naccount with `type: individual` is refused with `422\nACCOUNT_HOLDER_TYPE_NOT_SUPPORTED`.\n\n⚠️ **A `test` key opens it anyway.** The sandbox provisioner does not read\nthe holder type, so it answers `201` for exactly the request `live`\nrefuses. Measured on 31 Aug 2026: **57 sandbox BRL accounts across 12\nintegrators** were opened for individual holders, the most recent that same\nday. If you are building an individual BRL flow against a test key, it will\nnot survive the switch to a live key - and the sandbox will not tell you.\n\nThis is a limitation of OZAV's provider coverage, not of your request, and\nit is expected to change: when a Pix provider that opens individual\naccounts is integrated, the refusal goes away. Until then, build BRL named\naccounts for `type: business`.\n\n✅ **A company registered outside Brazil can hold one.** `registration.type`\nmay be `ein`, `company_number` or `other`, and the value is sent as declared\n- no Brazilian format is imposed on a foreign registration, and\n`registration.country` is what determines the country of record. Until 20 Aug\n2026 this was refused: the holder record stamped every business as a\n14-digit CNPJ, so a foreign registration failed a Brazilian check nobody had\ndecided to apply to it.\n\n⚠️ The reverse is now stated explicitly rather than implied: an account on a\n**Brazilian** rail does need a Brazilian tax identity. A customer whose\nregistration is an `ein` is refused there with\n`422 ACCOUNT_HOLDER_TYPE_NOT_SUPPORTED`, naming the declared type - the\ncustomer is valid, only that corridor needs a CPF or CNPJ.\n\nRegistering that holder needs a slightly fuller set than onboarding alone,\nand the difference is the part integrators hit late:\n\n| field | note |\n|---|---|\n| `email` | top level |\n| `business.phone.number` | the subscriber number |\n| `business.phone.country_code` | dial code, e.g. `55` |\n| address `line1` | street |\n| address `city` | non-blank |\n| address `postal_code` | **required here**, and onboarding does not ask for it |\n| address `country` | **required here**, ISO 3166-1 alpha-2 |\n| address `state` | optional |\n\n🔴 **And one authorised person, by name.** The holder record is not complete\nwithout a natural person attached to it - the individual who controls or\nsigns for the company. Measured on 21 Aug 2026 against the two live holders:\nthe one with a person attached is active and has receiving coordinates; the\none without has none, and does not acquire them over time. An account opened\nfor a holder with no person attached is a bank account that never receives.\n\n| field | note |\n|---|---|\n| person `firstName` | given name, as on the identity document |\n| person `lastName` | family name |\n| | 🔴 **This is why `legal_name` on an associated person must be at least two words.** OZAV splits it into given and family name; a single-word name cannot be split, and repeating the same word in both would invent a surname on the bank's record. A person whose name will not split is skipped when OZAV picks who signs - and if they were the only signer, the holder is registered with nobody attached, stays pending at the bank, and the account never receives coordinates. `requirements` reports it as `individual.legal_name`, the same token as a missing name, because the fix is the same: correct the name. |\n| person `birthDate` | ISO date, `YYYY-MM-DD` |\n| person `roles` | at least one of `BeneficialOwner`, `AuthorizedPerson`, `Director` |\n| person `ownershipPercent` | only when the person is an owner; omitted otherwise |\n\n⚠️ `ownershipPercent` is sent **only** for a person declared as an owner.\nDeclaring a stake for someone who does not hold one is a false ownership\nstatement, and OZAV will not make it on your behalf.\n\n⚠️ **`postal_code` and a structured `phone` are the two that catch people\nout.** Outside Brazil, `requirements.missing_fields` asks only for\n`line1`, `city` and `country` on the address, and it does not ask for a\npostal code at all - that set is enough to complete onboarding, and not\nenough to open this account. Send both up front and you will not have to go\nback to your customer for them later.\n\n**Ownership.** This account can only be opened for a holder with a single\ncontrolling owner, and OZAV has to be able to establish that it is one. A\ncompany whose ownership is split across several qualifying owners - or whose\nownership OZAV cannot resolve - is refused rather than filed under a\ndeclaration that does not describe it. If that is your case, talk to us\nbefore integrating.\n\n✅ **These refusals name the customer, not the corridor.** They arrive as\n`422 ACCOUNT_HOLDER_TYPE_NOT_SUPPORTED`, which is a different code from\n`ACCOUNT_RAIL_NOT_AVAILABLE` on purpose: requesting a different currency or\nrail will not change the answer, because the answer is about who the holder\nis. Until 20 Aug 2026 both refusals shared the corridor code, so an\nintegrator reading an older response may have concluded the currency was\nunsupported when it was not.\n\n🔴 **Approval is not instant, and the wait has its own code.** Once the\nholder is registered, the provider's compliance team reviews them before the\naccount can receive anything; they state 24-48 hours. A `POST /accounts`\nduring that window returns `409 ACCOUNT_HOLDER_PENDING_APPROVAL`. Nothing in\nyour request is wrong and there is no field to correct - send the same\nrequest again once the review clears. Until 31 Aug 2026 this state came back\nas `500 ACCOUNT_MAPPING_FAILED`, telling you OZAV had broken when nothing\nhad; an integrator reading an older response may have opened a ticket for a\ncustomer who was simply still in review.\n\n⚠️ Do not poll it in a tight loop. The code is the same until the review\nfinishes, so retrying every few seconds only produces the same answer.\n\n🔴 **Documents are required here too - an earlier version of this page said\nthey were not, and that was wrong.** The `POST /accounts` call itself carries\nstructured data only, with no file attached. That is a fact about the call,\nnot about the requirement, and stating the first as if it settled the second\nis the mistake this paragraph is correcting.\n\nMeasured on 21 Aug 2026 against the two live holders at the institution: the\none with a company dossier attached is active and has receiving\ncoordinates; the one with none has stayed pending since it was created, and\ndoes not acquire them over time. **A holder with no dossier is a bank\naccount that never receives.**\n\nThe dossier is the same list as for a Brazilian account - the six below,\nplus an identity document for each associated person. Send it once; it is\nnot collected twice because a second currency was opened later.\n\n⚠️ **Sending the dossier does not, by itself, make the account receive.** The\ndocuments are what a compliance review needs in order to run; the review\nitself happens at the institution, on its own clock, and the account becomes\npayable when that review clears - not when the upload succeeds. Measured on\n21 Aug 2026: the holder that receives shows its sanctions and know-your-\nbusiness checks cleared; the one that does not shows a record still awaiting\nthat review, unchanged for weeks.\n\nSo: a complete dossier is the part you and OZAV control, and it is a\nprecondition, not a switch. Plan the go-live of a foreign-currency corridor\naround a review you do not schedule, and do not quote your own customer a\ndate that depends on it.\n\n#### Which documents, for whom, and for which account\n\nTwo mechanisms establish identity. An individual uses only the first; a\nbusiness uses both.\n\n| | how identity is established | files you upload |\n|---|---|---|\n| individual | a hosted verification session captures the document, liveness and face match (`POST /verifications`) | none |\n| business | hosted session for KYB/UBO, **plus** the company dossier you upload (`POST /customers/{customer_id}/documents`) | the list below |\n\nAn individual is NOT asked for file uploads. Start a verification instead\nand the session collects the identity artifacts directly from the person -\nthat is what `kyc_document`, `liveness` and `face_match` mean in\n`CapabilityKind`. Uploading files for an individual is neither required nor\na substitute for the session.\n\n🔴 **Corrected on 31 Aug 2026 - this paragraph used to say a business that\nonly ever holds foreign currency \"is not asked for the six\".** That was\ntrue of the implementation and wrong as a rule: the requirement was wired to\na Brazilian rail's readiness flag, so a US or Portuguese company was asked\nfor 2 documents where a Brazilian one was asked for 6, and `ready: true`\nquietly meant different things in different jurisdictions.\n\n**Proof of address and a bank statement are now asked of every business**,\nwhatever currency it holds. They are KYB, not a property of a payment rail.\nA Brazilian company is additionally asked for the rail's own list, so the\nBrazilian set is still the larger one - but the difference is now a rail\nrequirement that is named, not an accident of which flag was read.\n\n**A tax return is not required of anyone** - not a business, not an\nindividual, at any volume. What OZAV needs is economic capacity, and that is\nalready a required FIELD (`annual_revenue_usd` for a business,\n`monthly_income_usd` for an individual). A tax return varies by jurisdiction\nand much of the world has no direct equivalent; the structured field answers\nthe same question and compares across borders.\n\nBoth sets are collected the same way, and OZAV keeps one canonical record:\nnothing you already sent has to be sent again because a rail was chosen\nlater. A Brazilian account chosen later is still the first time the rail's\nextra documents are asked for - plan for that if BRL is anywhere on your\nroadmap, because collecting from your customer after the fact is exactly the\nround trip this section exists to prevent.\n\n#### Supporting documents a business must upload\n\n⚠️ **Uploading a document is TWO calls, and the first one alone does not\ncount.** `POST /customers/{customer_id}/documents` registers the document and\nreturns an `upload_url`; the record sits at `pending_upload`, which is\ntreated as **not sent**. The bytes go in the second call, and only then does\nthe record become `received` - the state the account gate accepts.\n\nA `201` from the first call is not a delivered document. If you stop there,\nthe dossier looks complete on your side and is empty on ours, and the account\nis refused for a document you believe you sent. (There are records sitting at\n`pending_upload` in production today for exactly this reason.)\n\n`requirements.missing_fields` covers request FIELDS. Documents are uploaded\nthrough `POST /customers/{customer_id}/documents` **plus the upload call**,\nand are not reported there, so they are listed here instead - opening a Brazilian account asks\nfor all six, plus an identity document per associated person, before it will\nproceed. A `test` key does not enforce this: the sandbox opens the account\nwith no documents at all, so sandbox success is not evidence the dossier is\ncomplete.\n\n⚠️ **Documents are no longer a Brazilian-only requirement (11-Sep-2026).**\nOpening a `live` account in USD, EUR or GBP used to check contact details\nonly and asked for no document at all. It now applies the same gate, with the\nsame `422 ACCOUNT_PROFILE_INCOMPLETE` and the same `missing` list, entries\nprefixed `documents.`:\n\n| holder | documents required to open a non-BRL account |\n|---|---|\n| individual | `identity_document`, `proof_of_address`, `bank_statement` |\n| business | `company_registration`, `ownership_structure`, `proof_of_address`, `bank_statement` |\n\nA Brazilian account still asks for all six, because the BRL rail adds\n`financial_statement` and `source_of_funds` on top. You do not have to\nreconcile the two lists yourself: `GET /customers/{customer_id}/documents`\nreturns `still_required` for that customer, and it is computed by the same\ncode the account gate runs - what it names is exactly what the gate will ask\nfor.\n\n| `doc_type` to send | what it establishes |\n|---|---|\n| `company_registration` | the company exists and is currently registered |\n| `ownership_structure` | who ultimately owns it |\n| `proof_of_address` | where it operates from |\n| `financial_statement` | its financial position |\n| `bank_statement` | recent account activity |\n| `source_of_funds` | where the money comes from |\n\nThese are the literal `doc_type` values - any other string is rejected with\n`400`. **One `bank_statement` counts for two of them**: it satisfies both the\nfinancial position and the recent activity, so five uploads can cover all\nsix requirements.\n\nA business also needs at least one associated person whose `roles` include\n`authorized_signer`, `officer` or `director`, in addition to the ≥25% owner\nthat `missing_fields` reports as `owners` (role `ubo`). They can be the same\nperson, and `roles` takes more than one value.\n\n⚠️ **Each of those people needs an identity document of their own.** Send it\nas `identity_document` - it is not covered by any of the six company\ndocuments above, and it is the one integrators discover last, after the\ndossier is already assembled. A person OZAV has already approved does not\nneed it re-sent.\n\n✅ **You do not have to guess: `POST /accounts` tells you.** When the dossier\nis not yet complete the call is refused with `422\nACCOUNT_PROFILE_INCOMPLETE`, and the error carries a `missing` list naming\nevery field and document still needed, down to address components such as\n`business.registered_address.postal_code`. Once you have declared a person\nwho is a ≥25% owner or an authorised signer, it also reports what is missing\nfor that person specifically - `associated_persons[0].cpf`,\n`associated_persons[0].identity_document_or_approval`. Before that it asks\nfor the person itself (`associated_persons.ubo_25_plus`), so expect the list\nto get longer once you add one. That list is the\nauthoritative checklist for opening a Brazilian account, and it is\ndeliberately longer than `requirements.missing_fields` on the customer:\n`missing_fields` answers \"is this customer complete?\", the `422` answers \"can\nthis customer be given an account?\" Drive your onboarding UI off the `422`\nwhen the customer already exists, and off the tables above when you are still\nbuilding the create request.\n\nThe categories above are a DECLARATION vocabulary, not a list of which\nartifact satisfies which category in a given country: whether a specific\nnational registry extract or utility bill is accepted is decided in review,\nand OZAV does not publish a per-jurisdiction acceptance table.\n\n**`approved` is not the finish line.** A customer can pass its checks and\nstill be refused on the money paths, because OZAV activates a customer only\nonce it also holds a payable identity. Today that means an account in\nBrazilian rails needs a CPF or CNPJ, so a customer whose tax identity is a\npassport can be created and verified but cannot yet be given a named\nBrazilian account.\n\n⚠️ **There is no public field for that last step.** `status` goes no further\nthan `approved` - the values are `pending`, `in_review`, `approved`,\n`rejected` and `suspended`, and nothing else is ever returned. Do not poll\nfor a state that will not arrive. The way you find out is `POST /accounts`:\nit either opens the account or refuses with a coded error you can act on.\n\n## Jurisdictions we serve\n\nEligibility is decided by **declared residence and place of registration**,\nnever by nationality alone.\n\nCustomers whose tax identity or address is issued in one of these\njurisdictions are refused at creation, with `422 JURISDICTION_NOT_SERVED`\nnaming the field that caused it:\n\n| refused |\n|---|\n| `AF` Afghanistan, `BY` Belarus, `CU` Cuba, `IR` Iran, `KP` North Korea, `MM` Myanmar, `RU` Russia, `SD` Sudan, `SS` South Sudan, `SY` Syria, `ZW` Zimbabwe |\n\nThese are accepted and carry enhanced due diligence. Expect longer review\nand requests for further evidence:\n\n| enhanced due diligence |\n|---|\n| `AL` `BB` `BF` `CD` `CM` `GH` `HT` `JM` `JO` `ML` `MZ` `NG` `NI` `PA` `PH` `PK` `SN` `TT` `TZ` `UG` `VE` `VN` `YE` |\n\nEverywhere else is served, subject to name screening against the\nconsolidated UN, OFAC, UK and EU lists and to a risk-based decision. Being\noutside both tables means you may proceed; it does not by itself guarantee\nacceptance.\n\n## Quickstarts\n\nStart with `GET /capabilities` to confirm the entitlements and scopes on\nyour key, then follow the flow that matches your integration:\n\n- **Compliance-only:** create or reuse a customer, create a verification or\n  screening, and read the resulting compliance status. In sandbox, use\n  **KYC simulate** with `POST /verifications/{id}/simulate` to test approval\n  and rejection callbacks.\n- **Neobank sandbox:** create a customer, simulate KYC approval, then call\n  `GET /wallets?customer_id=...` and `GET /accounts?customer_id=...` to\n  read the simulated wallet and BRL/Pix account instructions.\n- **Liquidity quote-only:** create or reuse a customer and call\n  `POST /quotes` to price a supported corridor before attempting any\n  money-moving flow.\n- **Webhooks and events:** create a webhook endpoint, send a test delivery,\n  and reconcile deliveries with `GET /events`.\n\nSandbox keys let you exercise these flows without moving real funds. Live\nkeys persist production records, but money movement and live card issuance\nremain gated by your organization's enabled rails.\n\n## Authentication\n\nAuthenticate every request with your secret key in the **`X-API-Key`**\nheader (or as an `Authorization: Bearer` token - the same key, either way).\n\n```\nX-API-Key: ozav_sk_test_5f8d…    # sandbox\nX-API-Key: ozav_sk_live_9a1c…    # production\n```\n\n- **The key prefix selects the environment.** `ozav_sk_test_` keys resolve\n  against the sandbox; `ozav_sk_live_` keys resolve against production. The\n  base URL is identical for both - there are no separate hostnames and no\n  code changes to promote from test to live.\n- **Two authorization axes.** Your organization holds **entitlements** (the\n  products enabled for you: `liquidity`, `compliance`, `customers`, `cards`,\n  `custody`, `self_custody`, `kyc_reliance`). Each key additionally carries\n  **scopes** (e.g. `quotes:write`, `transfers:read`) that restrict what\n  that key may do. Effective permission is the intersection. A missing\n  product returns `403 ENTITLEMENT_REQUIRED` (\"talk to us\"); a missing\n  scope returns `403 INSUFFICIENT_SCOPE` (\"reissue the key with that\n  scope\").\n- **Getting keys.** Mint, list, rotate, and revoke keys under the API Keys\n  resource, or in the developer portal. A new key's secret is shown **once**\n  at creation - store it securely. Rotate with an overlap window so the old\n  secret keeps working while you cut over.\n- **Keep secrets server-side.** A live secret key can move money. Never ship\n  it in a browser, a mobile binary, or a public repository.\n\n## Sandbox & Environments\n\nThere is one base URL - `https://developers.ozav.io/v2` - for both\nenvironments; the key prefix does the routing.\n\nIt is `developers.`, not `api.`. `api.ozav.io` resolves and answers, but it\nis not this API: it returns `{\"error\":\"requested path is invalid\"}`, a shape\nthat appears nowhere in this document and carries no `request_id`. If you\nget an error whose body does not match the `Error` schema below, check the\nhostname before checking anything else.\n\n| | Test | Live |\n|---|---|---|\n| Key prefix | `ozav_sk_test_` | `ozav_sk_live_` |\n| Moves real money | No | Yes |\n| Data | Isolated sandbox | Production |\n\nA `test` key exercises customers, verifications, accounts, quotes,\ntransfers and screenings with no real settlement and no real bank-account\nopening. When you are ready, swap the key for its `live_` counterpart; your\nCODE does not change.\n\n**Your DATA does not carry over, and this is the part that surprises people.**\nEvery resource belongs to exactly one environment. A customer created with a\n`test` key exists only in test; a customer created with a `live` key exists\nonly in live. **The ids are different, and an id from one environment is not\nfound in the other** - `GET /customers/{id}` with the wrong environment's key\nanswers `404 CUSTOMER_NOT_FOUND`, and `GET /customers` lists only that\nenvironment's records. A brand new `test` key therefore lists **zero**\ncustomers, even for an organization with many customers in production. That\nis not an error and not a permission problem: there is simply nothing in test\nyet.\n\nSo the first thing to do with a `test` key is **create your own test data**\nwith it: `POST /customers`, then accounts, wallets, quotes. Do not reuse\nproduction ids. When you go live, the same code runs against the `live` key\nand operates on the live records it creates there.\n\n### What the sandbox cannot exercise on its own\n\nRead this before you plan a schedule around it. Each row below is a place\nwhere the resource is created but the flow cannot be finished with a\nself-serve `test` key, and each needs OZAV to act on your account. None of\nthem is a bug you can work around by changing your request.\n\n| You want to | What happens | What unblocks it |\n|---|---|---|\n| Create a `full` customer and then verify it | The sandbox create already answers `status: approved` for a `full` customer (the simulated settlement path reports it active), so a flow tested only here never reaches `POST /verifications`. A `reliance` customer is `pending` in both environments until profile + partner document + screening are in | Nothing to unblock - a warning: on `live` a `full` create answers `pending` and only a verification advances it. Exercise `POST /verifications` + `/simulate` in sandbox anyway, so the step exists in your code. |\n| Create a customer that settles | Works, but the sandbox does not exercise the terms gate: the attestation is simulated, and a missing, unknown or foreign `terms_session_id` is accepted with `201` | Nothing to unblock, and one thing to know: on `live` the same call is refused with `422 TERMS_ACCEPTANCE_REQUIRED` unless you pass an accepted session. Collect it with `POST /customers/terms-sessions` and `POST /customers/terms-sessions/accept` (both public, since 2 September 2026), and pass the id on `POST /customers`. Build that into your flow now; the sandbox will not remind you. |\n| Render the Pix payment screen | Works, and this row said the opposite until 3 Sep 2026. The sandbox does NOT confirm the pay-in before responding: it holds the transfer in `awaiting_payment` and returns `source.payment_instructions` carrying a simulated BR Code, a real EMV MPM payload with a valid CRC that a genuine Pix library parses | Nothing to unblock. To move past the payment, call `POST /transfers/{transfer_id}/simulate`: it confirms the pay-in the way a real payer would and advances the transfer through both legs to a terminal `completed`, emitting `transfer.updated` and then `transfer.completed`. Until 8 Sep 2026 it advanced only the pay-in leg and the transfer stopped at `payment_confirmed` with `destination_leg: pending` forever - an integrator asked whether the crypto leg settled asynchronously, and the honest answer was that nothing settled it. |\n| Pay out to a Pix key (off-ramp) | Works end to end: the sandbox executor pays the destination, the transfer lands `completed` and `transfer.completed` fires. Until 4 Sep 2026 every sandbox Pix off-ramp died on `422 PROVISIONING_REQUIRED`, because a Pix-key destination carries no venue reference and there is no venue in the sandbox | Nothing to unblock. One thing to know for `live`: there, a Pix-key destination becomes payable only after we register it with the payout venue; read `capabilities.receive` on the destination (true on a sandbox Pix destination, false on a live one until registered), never `status`. |\n| Test a card authorization | Every `/cards` route answers `403 ENTITLEMENT_REQUIRED` for a new organization, which no longer receives the `cards` product at creation | Ask OZAV to grant the `cards` product to your organization. After that, the card is `active` and every purchase declines with `Insufficient balance`, because no endpoint loads a prepaid balance. See `funding_type` on `Card`. |\n| Send a user to hosted verification | A `test` key never starts a hosted session (a real one bills a third party per attempt), so `session_url` is absent | Drive the outcome with `POST /verifications/{id}/simulate`, which is the sandbox affordance. On `live`, also ask for a hosted capability and ask OZAV to enable hosted capture; see `session_url`. |\n| Open a named account for a holder | The sandbox runs the SAME profile gate as `live`, only shallower, and this row used to read as though it ran none. It still requires the customer to be approved, still applies the rail matrix, and still requires a document that passes its own check digit (11 digits for CPF, 14 for CNPJ) plus a holder name, refusing with the same `422 ACCOUNT_PROFILE_INCOMPLETE` you would get on `live`. What it skips is everything the provider asks for beyond that (address, phone, mother's name, the KYB shareholder packet, uploaded files), so it opens with **no supporting documents at all** | Nothing to unblock: this one is a warning, not a limitation. A sandbox account opening proves your request is well-formed and the holder's core identifiers are present, never that the holder is ready. Send the documents with `POST /customers/{customer_id}/documents` first; the `422 ACCOUNT_PROFILE_INCOMPLETE` names every field and document still missing, and it is the authoritative checklist. 🔴 **But one refusal is NOT on that checklist, and no document clears it:** a BRL named account for `type: individual` refuses on `live` with `422 ACCOUNT_HOLDER_TYPE_NOT_SUPPORTED`, because no Brazilian Pix provider opens one today - and the sandbox opens it anyway. Measured on 31 Aug 2026: 57 such sandbox accounts across 12 integrators. If your flow is individual + BRL, sending more documents will not change the answer; see the named-account section. |\n| Read a wallet balance | `GET /wallets/{wallet_id}/balances` in `test` starts every wallet at a round 10,000.00 USDC and USDT and then moves with the transfers that COMPLETED on it - an on-ramp credits the amount delivered, an off-ramp debits the amount sent. Until 8 Sep 2026 it was a frozen constant, so a credit, a withdrawal and a no-op were indistinguishable | Nothing to unblock, and three things to know. It is a derived figure, not a ledger: it does not reflect crypto sent to the wallet from outside, nor card spend. `GET /wallets/{wallet_id}/deposit-addresses` and `/onchain-balances` stay EMPTY in `test`, because a sandbox wallet has no chain addresses. And the opening 10,000 is deliberately round: nobody should mistake it for money. |\n| Build a self-custody transaction intent | Every sandbox wallet is `managed` by default | Ask OZAV to grant the `self_custody` product to your sandbox organization, then send a `self_custody` block on `POST /customers`. With both products enabled, pick per customer with `custody_model`; omitting it defaults to `self_custody`. |\n\nEverything else in the list above the table does work end to end, including\na full BRL on-ramp transfer, screening against designated lists with no\ncustomer record, document upload, and webhook delivery with signature\nverification.\n\nThat first item was FALSE until 8 September 2026 and is worth naming, since\na sentence like it is exactly what an integrator plans a schedule around. A\nsandbox BRL on-ramp could not reach `completed`: `POST\n/transfers/{transfer_id}/simulate` advanced only the pay-in leg, an on-ramp\nhas no payout leg to dispatch, and the machine that credits a wallet in\nproduction is driven by a real Pix charge that the sandbox never creates.\nMeasured in production on that date: none of the 54 sandbox on-ramps ever\nleft `payment_confirmed`, the oldest dating from 17 August. It is true now.\n\n## Idempotency\n\nMoney-moving and creation endpoints accept an **`Idempotency-Key`** header\nso a retry can never execute twice.\n\n- **Format:** 16-256 characters of `[A-Za-z0-9_-]`. Generate a fresh key\n  per logical operation (a UUID works well) and reuse it only when retrying\n  that exact operation.\n- **Required vs optional.** `POST /transfers` **requires** it - a transfer\n  without one is rejected. `POST /customers` and `POST /payout-destinations`\n  accept it (optional, recommended). Reads never need one.\n- 🔴 **`POST /accounts` is the exception, and it does not read the header at\n  all.** Sending `Idempotency-Key` there is not an error and not a\n  protection: none of the mechanics below apply to it - no\n  `Idempotent-Replayed`, no `409 IDEMPOTENCY_KEY_REUSED` on a changed body,\n  no in-flight `202`. This paragraph exists because the sentence above says\n  \"creation endpoints\" and an integrator would reasonably include it.\n\n  What protects that endpoint instead is the **natural key**: an account is\n  unique per `(customer, currency, country, rail)`. Repeat the same request\n  and you get **`200`** with the account that already exists, where the\n  first call returned **`201`** - so the status code, not a header, is how\n  you tell a fresh account from an existing one. You do not need to generate\n  a key for it, and reusing one across different corridors will not confuse\n  it.\n- **Replay.** A retry with the **same key and same body** replays the\n  original response verbatim - same status code - plus the header\n  **`Idempotent-Replayed: true`**, so you can tell a replay from a fresh\n  call.\n- **In flight.** If the first request is still executing when the retry\n  arrives, the retry returns **`202`** with `{\"status\":\"processing\"}` -\n  wait, then retry with the same key to receive the stored result.\n- **A `202` that does not resolve means the outcome is unknown, not\n  pending.** If the original request failed after the resource may already\n  have been written, the key stays claimed and every retry with it answers\n  `202` - there is no stored result coming. Do not loop. Reconcile with a\n  `GET` (list by `customer_id`, or read the id you were given) to find out\n  whether the resource exists, then continue with a **new** key if it does\n  not. This is deliberate: a claimed key that outlives its request is\n  recoverable, a duplicate customer or payout destination is not.\n- **Conflicts.** The same key with a **different body** is a client bug and\n  returns **`409 IDEMPOTENCY_KEY_REUSED`** - it never overwrites the first\n  result. Use a new key when the request has genuinely changed.\n\nRetry idempotency-safe on `429` and `5xx` with the same key and exponential\nbackoff; treat a `4xx` other than `409` as terminal. Cap the retries: if\nthe response becomes a `202` that repeats, stop and reconcile as described\nabove rather than retrying indefinitely.\n\n## Pagination\n\nList endpoints are cursor-paginated and uniform.\n\n- **`limit`** - page size, `1`-`100`, default `20`.\n- **`starting_after`** - an opaque cursor; pass the previous page's\n  `next_cursor`.\n\nEvery list responds with the same envelope:\n\n```json\n{\n  \"data\": [ /* … */ ],\n  \"has_more\": true,\n  \"next_cursor\": \"b3phdl9jdXNfMDFK…\"\n}\n```\n\nLoop until `has_more` is `false`. Cursors are opaque - do not parse or\nconstruct them; a malformed cursor fails loud with `400 INVALID_CURSOR`\nrather than silently resetting to page one.\n\n## Amounts & Currencies\n\n- **Amounts are decimal strings**, never floats: `\"1250.00\"`, `\"0.50\"`.\n  This preserves precision across currencies - the float `0.1` cannot\n  represent one tenth exactly, and money math must not drift.\n- **Precision.** Quote and balance amounts carry up to **6** fractional\n  digits; the amount you submit when creating a transfer accepts up to\n  **8**.\n- **Currency codes** are uppercase ISO 4217 (`BRL`, `USD`, `EUR`, `MXN`) or\n  stablecoin symbols (`USDC`, `USDT`). Codes are accepted case-insensitively\n  on input and always returned uppercase.\n- **Exchange conditions are a spread in integer basis points** - `150`\n  means 150 bps (1.50%), stored as an integer and divided by 10,000 in rate\n  math. Never a float or a percentage string. A quote's public\n  `exchange_conditions.spread_bps` is only the additional spread selected\n  by the client for that quote. OZAV cost, floor, and margin analytics stay\n  internal.\n\n## Errors\n\nEvery error returns the same JSON envelope alongside the HTTP status:\n\n```json\n{\n  \"error\": {\n    \"type\": \"invalid_request_error\",\n    \"code\": \"MISSING_FIELD\",\n    \"message\": \"email is required.\",\n    \"param\": \"email\",\n    \"request_id\": \"req_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"details\": [\n      { \"code\": \"MISSING_FIELD\", \"message\": \"email is required.\", \"param\": \"email\" }\n    ]\n  }\n}\n```\n\n- **`type`** is one of eight stable classes:\n\n  | `type` | Meaning | Typical status |\n  |---|---|---|\n  | `invalid_request_error` | Malformed or unfulfillable input - a missing field, a bad value, or a well-formed request that cannot be executed. | `400`, `422` |\n  | `authentication_error` | Missing, invalid, revoked, or expired key. | `401` |\n  | `permission_error` | Authenticated but not permitted - product not enabled, or the key lacks the scope. | `403` |\n  | `not_found_error` | Resource does not exist or is not visible to the caller. | `404` |\n  | `conflict_error` | Conflicts with existing state - e.g. an idempotency key reused with a new body, or a duplicate customer. | `409` |\n  | `rate_limit_error` | Too many requests - honor `Retry-After`. | `429` |\n  | `idempotency_error` | The idempotency layer could not be reached; retry the same request. | `503` |\n  | `api_error` | An unexpected server-side or dependency error; OZAV fails closed rather than guess. | `500`, `503` |\n\n- **`code`** is a stable, machine-readable string you can branch on\n  (`ENTITLEMENT_REQUIRED`, `INSUFFICIENT_SCOPE`, `IDEMPOTENCY_KEY_REUSED`,\n  `RAIL_NOT_AVAILABLE`, `QUOTE_EXPIRED`, …).\n- **`param`** names the offending field when applicable; **`details`** may\n  carry per-field errors.\n- **`request_id`** identifies the exact call - include it in every support\n  request.\n\nThe complete, machine-readable catalog of `code` values ships alongside\nthis reference as `v2-error-codes.json`, so you can generate typed error\nhandling directly from it.\n\n### Resource ids, and what a malformed one returns\n\nThe resource ids that appear in a path position carry these prefixes\n(other published ids, such as `ozav_q_` for quotes, exist but are not used\nin a path position and are not listed here):\n\n| prefix | resource |\n|---|---|\n| `ozav_cus_` | customer |\n| `ozav_ap_` | associated person |\n| `ozav_pd_` | payout destination |\n| `ozav_tr_` | transfer |\n| `ozav_acc_` | account |\n| `ozav_wal_` | wallet |\n| `ozav_card_` | card |\n| `ozav_key_` | API key |\n| `ozav_scr_` | screening |\n| `ozav_vrf_` | verification |\n| `ozav_wh_` | webhook endpoint |\n| `ozav_txi_` | transaction intent |\n| `ozav_apr_` | approval |\n| `ozav_wtb_` | transfer batch |\n| `ozav_tok_` | client token |\n| `ozav_nft_` | client NFT collection |\n\n**An id whose prefix does not match the position it was used in returns\n`404`, not `400`.** The request never reaches a lookup - the id could not\nname a resource of that type - but the status is `404` so that one status\ncovers every \"you cannot have this resource\" case. Branch on `code` and\nread `message`, not on the status, to tell the cases apart.\n\nThe message names the prefix that position expects, and, when the id\ncarries a prefix from the table above, names which resource it belongs to\ninstead. Sending a `ozav_cus_…` where a wallet id belongs tells you exactly\nthat.\n\n**What `404` never tells you is whether a resource exists.** A well-formed\nid belonging to another account, to the other environment, or to nothing at\nall all return the same generic phrase, indistinguishable from one another.\nOnly the malformed case gets the extra sentence, because that sentence is a\nstatement about the text you just sent - not about our records.\n\n## Networks & Assets\n\n`components.schemas.Network` is the public v2 wire enum today. OZAV accepts\nonly `USDC` and `USDT` as blockchain assets on this surface today. Other\nstablecoins such as DAI, USDB, PYUSD, and EURC are not accepted by public\nAPI v2 flows unless this table and `GET /capabilities` explicitly say so.\n\n| Network | Public v2 status | Custody stack status | Accepted assets today |\n|---|---|---|---|\n| `ethereum` | accepted | active | USDC, USDT |\n| `base` | accepted | active | USDC |\n| `optimism` | accepted | active | USDC |\n| `polygon` | accepted | active | USDC, USDT |\n| `avalanche_c_chain` | accepted | active | USDC, USDT |\n| `worldchain` | accepted | active | USDC |\n| `unichain` | accepted | active | USDC |\n| `linea` | accepted | active | USDC |\n| `sonic` | accepted | active | USDC |\n| `sei` | accepted | active | USDC |\n| `celo` | accepted | active | USDC |\n| `zksync_era` | accepted | active | USDC |\n| `solana` | accepted | active | USDC, USDT |\n| `tron` | accepted | active | USDT |\n| `stellar` | accepted | active | USDC |\n| `tempo` | not accepted | deferred | none |\n| `arbitrum` | not accepted | active | USDC, USDT |\n| `aptos` | not accepted | deferred | none |\n| `sui` | not accepted | deferred | none |\n\nActive custody stack status does not mean a public end-to-end flow is live.\nWallet provisioning, live card issuance, live BRL/Pix account opening,\nmanaged wallet sends, and self-custody transaction building remain gated unless\n`GET /capabilities` says your organization can use them.\n\n## Integration Guides\n\nThe same endpoints compose into different integrations depending on the\nproducts you hold. These walkthroughs describe the **intended** end-to-end\nshape of each flow. Steps outside your entitlements answer a named error for your\norganization and may support only selected corridors first. Steps against a\n`coming_soon` resource are **not yet callable** - they are shown so you can\nbuild against the final contract. Check\n**What's available today** and `GET /capabilities` for the current reality.\n\n### 1. Full-stack neobank (customer → verify → documents → account → quote → transfer)\n\n> **Availability:** this is the intended full loop. Screening and quoting\n> are **live**; customer and verification **records** persist. But\n> live provisioning, managed wallet sends, and live account opening are still\n> gated. BRL/Pix accounts, payout destinations, and transfer creation are\n> served; completion to stable/fiat settlement depends on your enabled rails\n> and settlement provider permissions. Build against the contract; watch\n> `GET /capabilities` for go-live.\n\nProducts: `customers`, `compliance`, `liquidity` (plus `custody` for\nwallets).\n\n1. **Create the end-customer.** `POST /customers` with the identity you\n   hold - `type: individual` + `individual.tax_id`, or `type: business` +\n   `business.registration`. The customer starts `pending`; the response's\n   `verification.mode` tells you which of the two paths below applies.\n2. **Verify - two paths, decided by your organization's posture.**\n   Under `reliance` (the default for API integrators): there is nothing to\n   start. Complete the profile (`requirements.missing_fields` empty, via\n   `POST`/`PATCH /customers`), add associated persons for a business, and\n   upload the partner document (`identity_document` for an individual,\n   `company_registration` for a business). OZAV screens the customer\n   against designated lists on its own and the customer becomes\n   `approved` on the call that completes the set - no `POST /verifications`.\n   Under `full` (neobank: the end-user is on your screens): start checks with\n   `POST /verifications` (`kyc_document`, `liveness`, `face_match`,\n   `aml_name_screening`), redirect the end-user to `session_url`, then poll\n   `GET /verifications/{id}` or recover sessions with\n   `GET /verifications?customer_id=`; in sandbox drive it with\n   `POST /verifications/{id}/simulate`. Either way, watch the customer's\n   `status` until it is `approved`. Sandbox simulation creates simulated\n   managed wallet and BRL/Pix account projections for approved customers\n   with complete BR data; it does not create a real on-chain wallet or open\n   a real bank account.\n3. **Upload the customer's supporting documents.** `POST\n   /customers/{customer_id}/documents` to declare each one, then `PUT` the\n   bytes to the `upload_url` it returns. Two calls: a declared document sits\n   at `pending_upload` and does not count until the bytes arrive and the\n   server matches the hash.\n\n   > **Do not skip this because the customer is `approved`.** `approved` is\n   > the verification result; it does not mean the documents are on file,\n   > and account opening reads the documents, not the status. On `live`,\n   > step 4 refuses until they are all present. In the sandbox it does not,\n   > so a run that passes end to end here proves your requests are\n   > well-formed, never that the customer is ready. `GET\n   > /customers/{customer_id}/documents` shows what you have already sent.\n\n   Which documents apply depends on the customer type and the account you\n   are opening; the `422` from step 4 names every one still missing and is\n   the authoritative checklist.\n4. **Read or request the customer's named account.** Use\n   `GET /accounts?customer_id=...` after sandbox approval, or\n   `POST /accounts` to request a named fiat account for a customer whose\n   documents are on file. BRL/BR/pix is sandbox-active today; future\n   currency and rail combinations use the same endpoint as they are\n   enabled. Sandbox returns simulated receiving instructions. Live opening\n   requires enablement.\n5. **Register the payout destination.** `POST /payout-destinations` with\n   the rail's `details` (e.g. `pix` → `pix_key`; `ach` → account + routing).\n   BRL/Pix key destinations can be created; other rails may still\n   return a gated provisioning error until enabled.\n6. **Quote.** `POST /quotes` with `source`/`target` currencies; read\n   `exchange_conditions` (spread in bps) and `rate`. Quotes are\n   short-lived - mind `expires_at`. *(Live today.)*\n7. **Transfer.** `POST /transfers` referencing the `quote_id`, with an\n   `Idempotency-Key`. For BRL/Pix on-ramp, use `source.type:\n   fiat_payment` and `destination.type: wallet`; the response includes\n   Pix payment instructions for that transaction. BRL/Pix off-ramp uses\n   `source.type: wallet` and `destination.type: payout_destination` and is\n   rollout-gated.\n8. **Track.** Poll `GET /transfers/{id}`: `status` walks\n   `awaiting_payment → payment_confirmed → converting → payout_initiated →\n   completed`, and `source_leg` / `destination_leg` report each side\n   independently.\n\n### 2. Liquidity-only (quote → transfer)\n\n> **Availability:** the quote step is **live today**. Transfer creation is\n> served for BRL/Pix paths; full settlement remains controlled by\n> rail/provider enablement. Other transfer combinations still fail closed\n> until their settlement path is enabled.\n\nProducts: `customers`, `liquidity`. With verified customers and registered\ndestinations already in place, the intended flow is two calls:\n\n1. **Quote** - `POST /quotes` (`source`/`target`). Optionally add\n   `spread_bps` as your own additional spread on top of the OZAV all-in\n   rate when enabled for your organization. *(Live today.)*\n2. **Transfer** - `POST /transfers` with the `quote_id` and an\n   `Idempotency-Key`; then poll `GET /transfers/{id}` (or list with\n   `GET /transfers?status=`). *(On the roadmap - see the note above.)*\n\n### 3. Compliance-only (screen and verify as a service)\n\nProducts: `customers`, `compliance`. No settlement customer is provisioned\n- this profile is verification-only.\n\n1. **Screen** - `POST /screenings` against designated lists (`sanctions`,\n   `pep`, `adverse_media`). Subjects can be **inline** (a `person` or\n   `business`, no customer record required) or reference an existing\n   `customer_id`. `result` is `clear`, `match_found`, or `review`\n   (conservative - a subject is never silently cleared).\n2. **Verify** - for document, liveness, or UBO checks, `POST /verifications`\n   for a customer, then poll `GET /verifications/{id}`. If you lose the id,\n   recover it with `GET /verifications?customer_id=`.\n3. **Retain evidence** - keep the `ozav_scr_…` id; reconstruct the trail\n   with `GET /screenings?customer_id=`.\n\n### 4. Wallets (read balances, build unsigned transactions)\n\nProducts: `custody` (managed) and/or `self_custody`.\n\n1. **List and read wallets.** `GET /wallets` (filter by `?customer_id=`)\n   and `GET /wallets/{id}`. Each wallet reports a `custody_model` -\n   `managed` (held under OZAV-side custody) or `self_custody` (the end-user\n   holds the key).\n2. **Read balances.** `GET /wallets/{id}/balances` returns balances by\n   currency (and network where applicable). Balance reads are\n   tenant-scoped: a wallet outside your organization returns `404`.\n3. **Build a transaction (self-custody).** For `self_custody` wallets,\n   `POST /wallets/{id}/transaction-intents` returns an **unsigned**\n   transaction; your side - or the end-user's wallet - signs and\n   broadcasts. OZAV never holds keys for self-custody wallets.\n4. **Read and shape the policy (managed).** `GET /wallets/{id}/policy` shows\n   what OZAV applies before signing: allowed destinations, the limits in\n   force and their `source`, and the changes still under review.\n   `PATCH /wallets/{id}/policy` proposes a new destination or new limits;\n   nothing changes until OZAV decides, and `wallet_policy.updated` tells you\n   when it does.\n5. **Require your own approvals (managed).** Set `approvals` on the policy\n   (`required_approvals`, optional `above_usd`). A covered send then answers\n   `202 awaiting_approval` and emits `transfer.approval_required`; approver\n   keys holding `approvals:write` decide it at `POST /approvals/{id}/approve`\n   or `/reject`. The last approval executes the send and emits\n   `transfer.approved`. Mint one key per approver: the key that sent the\n   request can never approve it.\n6. **Freeze in an emergency (managed).** `POST /wallets/{id}/freeze` pauses\n   every send at once (`409 WALLET_FROZEN` until lifted); deposits keep\n   arriving. `POST /wallets/{id}/unfreeze` lifts it - through your quorum\n   when one is on.\n7. **Keep an address book (managed).** `GET /wallets/{id}/addresses` lists\n   allowed and proposed destinations with your labels;\n   `POST /wallets/{id}/addresses` proposes a labelled one (same review as\n   the policy); `DELETE /wallets/{id}/addresses/{network}/{address}`\n   removes one at once.\n\n## Roadmap\n\nAdditional fiat account rails, crypto deposit addresses, and a unified\ntransaction feed are in development and will be published to this reference\nwhen generally available. Webhook endpoint management and delivery history\nare live; the event catalog expands only when a resource actually emits a\nnew event type.\n",
    "contact": {
      "name": "OZAV Platform",
      "url": "https://www.ozav.io",
      "email": "platform@ozav.io"
    },
    "termsOfService": "https://www.ozav.io/terms",
    "license": {
      "name": "Proprietary - © OZAV LTDA",
      "url": "https://www.ozav.io/terms"
    }
  },
  "x-ozav-personas": [
    {
      "key": "neobank",
      "description": "Full-stack digital banking - customers, quotes, transfers, wallets, compliance."
    },
    {
      "key": "liquidity",
      "description": "Liquidity-only - quotes, on/off-ramp transfers, payout destinations."
    },
    {
      "key": "compliance",
      "description": "Compliance-only - screenings and verifications as a standalone service."
    },
    {
      "key": "custody",
      "description": "Wallets under managed custody or self-custody (client-signed) models."
    }
  ],
  "servers": [
    {
      "url": "https://developers.ozav.io/v2",
      "description": "Production (live and test keys; environment is derived from the key prefix)"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    },
    {
      "BearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Capabilities",
      "description": "Discover, at runtime, exactly what a key can do. `GET /capabilities`\nreturns the caller's **entitlements** (the products enabled for the\norganization), the **scopes** granted to the presented key, and a\nper-feature readiness map (`live`, `beta`, `coming_soon`). Call it first\nin any integration so your code branches on real access instead of\nguessing.\n"
    },
    {
      "name": "Customers",
      "description": "The anchor resource every other object hangs off. A customer is an\nend-user you serve - an `individual` or a `business` - created once with\nthe identity you hold. Verification, payout destinations, wallets,\nquotes, and transfers all reference a `customer_id`. Business customers\ncarry associated persons (UBOs, directors, officers, and signers).\n\n**Creating the customer asks for no documents.** Every field on `POST\n/customers` is optional beyond the type and the contact, and what is still\nowed comes back on the record itself as `requirements.missing_fields`.\nDocuments are a separate mechanism: they are registered with `POST\n/customers/{customer_id}/documents` and then uploaded, and they are\ndeliberately NOT reported in `missing_fields` - see the note on that field.\n\n**Who owes documents at all:** an `individual` owes none, ever. A\n`business` owes a dossier only when a BRL account is opened for it. The\nlist, the literal `doc_type` values and the two-call upload sequence are in\n**\"What each customer type must provide\"** under *Onboarding & Compliance*.\n"
    },
    {
      "name": "Verifications",
      "description": "Identity and business verification sessions (KYC / KYB). Start one or\nmore checks for a customer from the canonical capability taxonomy -\ndocument, liveness, face match, proof of address, UBO, and more - then\npoll the session until each check reports an outcome. Use this when you\nneed verified identity, not just a list screening.\n"
    },
    {
      "name": "Screenings",
      "description": "Standalone designated-list screening as a service - one call, no\ncustomer record required. Screen a person, business, existing customer,\nor a wallet address against OZAV's in-house dataset (UN, OFAC,\nEU, UK, and national sources, refreshed daily). Results are conservative:\nan inconclusive subject is flagged `review`, never silently cleared.\n"
    },
    {
      "name": "Quotes",
      "description": "Amounts below the platform minimum for the fixed leg's currency are refused with\n`400 AMOUNT_BELOW_MINIMUM`, and the message names the minimum. Quoting below it\nwould promise an execution that can never settle.\n\nFX quotes with OZAV all-in rates. A quote is opaque, short-lived, and\nmay carry an additional client-selected spread in basis points, then is\nexecuted by creating a transfer that references it. Internal cost, floor,\nand margin analytics are not part of the public response.\n"
    },
    {
      "name": "Transfers",
      "description": "The single money-movement resource. One endpoint is intended to serve\nevery flow - FX, on-ramp, off-ramp, wallet-to-wallet, and crypto\nsend/receive - selected by the `(source.type, destination.type)` pair\nrather than by a different URL. Selected BRL/Pix\npaths create real provider-neutral transfer records and Pix payment\ninstructions; other combinations fail closed with a stable error rather\nthan returning a silent partial result.\n"
    },
    {
      "name": "Payout Destinations",
      "description": "Where a customer's funds land off-ramp - bank, PIX, and other rails -\nas one canonical resource with a rail-keyed `details` union.\nBRL/Pix key destinations persist as\nprovider-neutral OZAV records. Rails that require external venue\nprovisioning may still fail closed until enabled.\n"
    },
    {
      "name": "Accounts",
      "description": "Customer-owned named fiat accounts for receiving funds into OZAV.\nThe same `ozav_acc_` resource covers BRL/Pix,\nUSD/ACH, USD/wire, EUR/SEPA, GBP/Faster Payments, and future fiat rails\nas they are enabled. Sandbox accounts are simulated and cost-free; live\nopening requires enablement for your organization. The public response\nnever exposes the banking venue behind the account.\n\n**Which documents a live account needs depends on the currency, and this is\nthe single most common round trip.** Opening a **BRL** account for a\nbusiness asks for a six-document dossier plus an identity document per\nassociated person. Opening **USD, EUR or GBP** transmits structured data\nonly and asks for no files at all. An **individual** never uploads files\nfor any currency - a hosted verification session collects the identity\nartifacts directly from the person.\n\nThe full list, the literal `doc_type` values, and the two-call upload\nsequence are in **\"What each customer type must provide\"** under\n*Onboarding & Compliance*. You do not have to guess it up front: `POST\n/accounts` refuses an incomplete dossier with `422\nACCOUNT_PROFILE_INCOMPLETE` and a `missing` list naming what is absent.\n\n⚠️ A business that started with a foreign-currency account uploaded\nnothing, so choosing BRL later is the first time the dossier is asked for.\nIf BRL is anywhere on your roadmap, collect it before you need it.\n"
    },
    {
      "name": "Wallets",
      "description": "Balances and custody for a customer's holdings. Each wallet reports a\n`custody_model` - `managed` (held under OZAV-side custody arrangements)\nor `self_custody` (the end-user holds the key). Reads are tenant-scoped:\na wallet outside your organization returns `404`, never a leak.\n"
    },
    {
      "name": "Cards",
      "description": "Canonical OZAV card issuing and lifecycle surface. Cards use opaque\n`ozav_card_` ids, are funded from OZAV balances (`prepaid_balance`), and\nnever expose issuing-provider identifiers or sensitive card data. Virtual\ncards issue end-to-end in sandbox; live issuance is enabled per\norganization once the card program is active.\n"
    },
    {
      "name": "Approvals",
      "description": "Your own approval quorum for managed-custody sends. When a wallet's policy\nsays `approvals.mode: customer_quorum`, a covered send answers\n`202 awaiting_approval` and waits here until `required_approvals` of your\napprover keys (scope `approvals:write`, other than the key that sent it)\napprove. The last approval executes the send in that same request; any\nrejection releases it; 72 hours unanswered, it expires. Your quorum is one\nmore gate before OZAV signs, never one less: OZAV's screening, limits and\nreview still apply after it.\n"
    },
    {
      "name": "Transaction Intents",
      "description": "Unsigned-transaction building for `self_custody` wallets. OZAV assembles\nthe calldata and returns an unsigned transaction; signing and\nbroadcasting stay entirely on your side or the end-user's wallet. OZAV\nnever holds keys for self-custody wallets.\n"
    },
    {
      "name": "Yield",
      "description": "Read the yield strategies OZAV admits and the positions a customer holds\nin them, with acquisition cost and realized performance measured on-chain.\nEach strategy carries a stable, opaque `id`; the underlying venue and its\ncontract address are never exposed. Applying and redeeming under managed\ncustody are not available yet. A client that holds its own key can have\nOZAV assemble unsigned deposit and withdrawal transactions, which OZAV\nnever signs and never broadcasts; that path is in a closed pilot.\n"
    },
    {
      "name": "Swap",
      "description": "Quote a stablecoin conversion across the networks OZAV supports. The quote\ncompares independent sources, reports the network cost on its own line,\nand states what the conversion costs without naming the venue that\nproduced it. Execution is not exposed yet.\n"
    },
    {
      "name": "API Keys",
      "description": "Self-serve key lifecycle: create keys scoped to a subset of your\nentitlements, list them (prefixes only - secrets are shown once at\ncreation), rotate with an overlap window, and revoke. Test and live keys\ndiffer only by prefix; the environment is derived from it.\n"
    },
    {
      "name": "Webhooks",
      "description": "Register HTTPS endpoints to receive signed event notifications. Each\nendpoint has its own signing secret (`ozav_whsec_`, shown once at create /\nrotate) and a list of subscribed event types. Endpoints are tenant-scoped.\n\n**Deliveries are also scoped by environment.** An event produced by a live\nkey is delivered only to endpoints registered in `live`, and a sandbox\nevent only to `test` endpoints. If you integrate in both, register one\nendpoint per environment - a single endpoint no longer receives both.\n\n**Verifying a delivery.** Every delivery carries three headers:\n\n```\nX-OZAV-Signature:   t=<unix>,v1=<hex>\nX-OZAV-Event-ID:    stable across retries, dedupe on it\nX-OZAV-Delivery-ID: unique per delivery attempt\n```\n\nParse `t` and `v1` out of `X-OZAV-Signature`, then recompute:\n\n```\nv1 == HMAC_SHA256(key = <your endpoint secret, WITH the ozav_whsec_ prefix>,\n                  message = \"<t>.\" + <raw request body>)\n```\n\nThree things break verification and are worth stating plainly, because each\none fails silently on your side:\n\n1. **The key is the whole secret string, prefix included.** `ozav_whsec_` is\n   part of the key, not a display label. Stripping it fails 100% of deliveries.\n2. **Sign the raw body bytes**, before any JSON parse. Re-serializing may or\n   may not reproduce the same bytes, so do not rely on it.\n3. **Compare against `v1` only**, not the whole `t=...,v1=...` header value.\n\nReject deliveries whose `t` is outside your tolerance window (five minutes is\nrecommended) and dedupe by `X-OZAV-Event-ID`.\n\n**When deliveries are not arriving**, read\n`GET /webhook-endpoints/{id}/deliveries` before debugging signatures. It\nreports `response_status` (what your server answered) and `error` (transport\nfailures). A `response_status` of `401` is a signature problem; a null\n`response_status` with an `error` is a network problem such as DNS or TLS,\nand no signature change will fix it.\n"
    }
  ],
  "x-tagGroups": [
    {
      "name": "Getting Started",
      "tags": [
        "Capabilities"
      ]
    },
    {
      "name": "Onboarding & Compliance",
      "tags": [
        "Customers",
        "Verifications",
        "Screenings"
      ]
    },
    {
      "name": "Money Movement",
      "tags": [
        "Quotes",
        "Transfers",
        "Accounts",
        "Payout Destinations"
      ]
    },
    {
      "name": "Wallets & Custody",
      "tags": [
        "Wallets",
        "Approvals",
        "Cards",
        "Transaction Intents",
        "Yield",
        "Swap"
      ]
    },
    {
      "name": "Platform",
      "tags": [
        "API Keys",
        "Webhooks"
      ]
    }
  ],
  "paths": {
    "/capabilities": {
      "get": {
        "operationId": "getCapabilities",
        "tags": [
          "Capabilities"
        ],
        "summary": "Get the caller's entitlements, key scopes, and feature readiness",
        "description": "Returns the two authorization axes for the presented key - the\norganization's `entitlements` and the key's `scopes` - plus the resolved\n`environment` and a per-feature readiness map (`live`, `beta`,\n`coming_soon`). Call it first in any integration and branch on the access\nit reports rather than hard-coding assumptions. Requires only a valid key\n(any entitlement, no specific scope).\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The two authorization axes and per-feature availability.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "entitlements": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Products enabled for the organization (e.g. liquidity, compliance, customers, cards, custody, self_custody, kyc_reliance)."
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Scopes granted to the presented API key."
                    },
                    "environment": {
                      "type": "string",
                      "description": "Which environment the presented key resolves to - `test` (sandbox) or `live` (production).",
                      "enum": [
                        "test",
                        "live"
                      ]
                    },
                    "features": {
                      "type": "object",
                      "description": "Per-feature readiness, keyed by feature name. Each value is one of\n`live` (generally available), `beta` (available to design partners,\nmay need enablement), or `coming_soon` (defined but not yet callable).\n",
                      "additionalProperties": {
                        "type": "string",
                        "enum": [
                          "live",
                          "beta",
                          "coming_soon"
                        ]
                      }
                    },
                    "jurisdictions": {
                      "type": "object",
                      "description": "Where OZAV onboards customers, decided by declared residence\nand place of registration - never by nationality alone.\n\n`refused` is enforced at creation: a customer whose tax\nidentity or address is issued there is rejected with\n`422 JURISDICTION_NOT_SERVED`. `enhanced_due_diligence` is\naccepted, with longer review and further evidence requested.\n\nAnywhere else is served, subject to name screening and a\nrisk-based decision. Absence from both lists means you may\nproceed; it is not by itself a guarantee of acceptance.\n",
                      "properties": {
                        "refused": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "pattern": "^[A-Z]{2}$"
                          }
                        },
                        "enhanced_due_diligence": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "pattern": "^[A-Z]{2}$"
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "neobank_test_key": {
                    "summary": "A full-stack test key",
                    "value": {
                      "entitlements": [
                        "customers",
                        "compliance",
                        "liquidity",
                        "custody"
                      ],
                      "scopes": [
                        "customers:read",
                        "customers:write",
                        "quotes:write",
                        "accounts:read",
                        "accounts:write",
                        "transfers:write",
                        "transfers:read",
                        "destinations:write",
                        "screenings:write"
                      ],
                      "environment": "test",
                      "features": {
                        "quotes": "live",
                        "screenings": "live",
                        "transfers": "live",
                        "accounts": "live",
                        "verifications": "live",
                        "wallets": "live"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/capabilities \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/capabilities\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/capabilities\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/customers": {
      "post": {
        "operationId": "createCustomer",
        "tags": [
          "Customers"
        ],
        "summary": "Create a customer",
        "description": "Creates a customer - the anchor resource that quotes, verifications,\naccounts, payout destinations, wallets, and transfers all reference. Supply\n`type: individual` with an `individual` block (including\n`individual.tax_id`) or `type: business` with a `business` block\n(including `business.registration`); the required identity is enforced\nserver-side. The customer always starts `status: pending` and is verified\nseparately (see `POST /verifications`). Accepts an optional\n`Idempotency-Key` (recommended). Returns the created `Customer`.\n\nThe record persists and reads back. Customer creation does\nnot open a real wallet or bank account by itself. In sandbox,\nverification simulation can materialize simulated wallet and BRL/Pix\naccount projections after approval; live provisioning requires\nenablement for your organization.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyOptional"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerCreateRequest"
              },
              "examples": {
                "individual": {
                  "summary": "Individual customer",
                  "value": {
                    "type": "individual",
                    "email": "ana@example.com",
                    "external_ref": "your-user-4821",
                    "individual": {
                      "legal_name": "Ana Ramos",
                      "date_of_birth": "1990-04-12",
                      "nationality": "BR",
                      "tax_id": {
                        "country": "BR",
                        "type": "cpf",
                        "value": "39053344705"
                      },
                      "residential_address": {
                        "line1": "Rua das Flores 100",
                        "city": "São Paulo",
                        "state": "SP",
                        "postal_code": "01000-000",
                        "country": "BR"
                      }
                    }
                  }
                },
                "business": {
                  "summary": "Business customer",
                  "value": {
                    "type": "business",
                    "email": "ops@acme.com.br",
                    "business": {
                      "legal_name": "Acme Pagamentos Ltda",
                      "registration": {
                        "country": "BR",
                        "type": "cnpj",
                        "value": "11222333000181"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Customer created.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                },
                "examples": {
                  "individual_pending": {
                    "summary": "Created - always starts pending",
                    "value": {
                      "id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "customer",
                      "type": "individual",
                      "status": "pending",
                      "external_ref": "your-user-4821",
                      "email": "ana@example.com",
                      "individual": {
                        "legal_name": "Ana Ramos",
                        "date_of_birth": "1990-04-12",
                        "nationality": "BR",
                        "tax_id": {
                          "country": "BR",
                          "type": "cpf",
                          "value": "39053344705"
                        }
                      },
                      "verification": {
                        "mode": "full",
                        "status": "pending",
                        "checks": {}
                      },
                      "requirements": {
                        "due": [],
                        "missing_fields": [
                          "mother_name",
                          "phone",
                          "residential_address",
                          "source_of_funds",
                          "expected_monthly_volume_usd",
                          "monthly_income_usd",
                          "occupation",
                          "is_pep",
                          "account_purpose"
                        ]
                      },
                      "created_at": "2026-07-12T18:00:00Z",
                      "updated_at": "2026-07-12T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "$ref": "#/components/responses/IdempotencyProcessing"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "500": {
            "description": "The customer was persisted but its public representation could not be produced (`CUSTOMER_MAPPING_FAILED`). This is now narrow: it fires only when a field the schema marks REQUIRED cannot be produced. A record that is merely incomplete is returned with its optional blocks omitted instead (see `CustomerVerification.mode`), so one incomplete record never fails a call. The record may exist - reconcile with `GET /customers` before creating another.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"type\": \"individual\",\n    \"email\": \"ana@example.com\",\n    \"external_ref\": \"your-user-4821\",\n    \"individual\": {\n      \"legal_name\": \"Ana Ramos\",\n      \"date_of_birth\": \"1990-04-12\",\n      \"nationality\": \"BR\",\n      \"tax_id\": {\n        \"country\": \"BR\",\n        \"type\": \"cpf\",\n        \"value\": \"39053344705\"\n      },\n      \"residential_address\": {\n        \"line1\": \"Rua das Flores 100\",\n        \"city\": \"São Paulo\",\n        \"state\": \"SP\",\n        \"postal_code\": \"01000-000\",\n        \"country\": \"BR\"\n      }\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"type\": \"individual\",\n    \"email\": \"ana@example.com\",\n    \"external_ref\": \"your-user-4821\",\n    \"individual\": {\n      \"legal_name\": \"Ana Ramos\",\n      \"date_of_birth\": \"1990-04-12\",\n      \"nationality\": \"BR\",\n      \"tax_id\": {\n        \"country\": \"BR\",\n        \"type\": \"cpf\",\n        \"value\": \"39053344705\"\n      },\n      \"residential_address\": {\n        \"line1\": \"Rua das Flores 100\",\n        \"city\": \"São Paulo\",\n        \"state\": \"SP\",\n        \"postal_code\": \"01000-000\",\n        \"country\": \"BR\"\n      }\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/customers\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"type\": \"individual\",\n        \"email\": \"ana@example.com\",\n        \"external_ref\": \"your-user-4821\",\n        \"individual\": {\n            \"legal_name\": \"Ana Ramos\",\n            \"date_of_birth\": \"1990-04-12\",\n            \"nationality\": \"BR\",\n            \"tax_id\": {\n                \"country\": \"BR\",\n                \"type\": \"cpf\",\n                \"value\": \"39053344705\",\n            },\n            \"residential_address\": {\n                \"line1\": \"Rua das Flores 100\",\n                \"city\": \"São Paulo\",\n                \"state\": \"SP\",\n                \"postal_code\": \"01000-000\",\n                \"country\": \"BR\",\n            },\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listCustomers",
        "tags": [
          "Customers"
        ],
        "summary": "List customers",
        "description": "Lists your customers, newest first, using the standard cursor pagination\n(`limit`, `starting_after`). Returns a `Page` whose `data` is an array of\n`Customer`.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated customers.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Customer"
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "page": {
                    "summary": "One page of customers",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "object": "customer",
                          "type": "individual",
                          "status": "approved",
                          "email": "ana@example.com",
                          "individual": {
                            "legal_name": "Ana Ramos",
                            "tax_id": {
                              "country": "BR",
                              "type": "cpf",
                              "value": "39053344705"
                            }
                          },
                          "verification": {
                            "mode": "full",
                            "status": "approved",
                            "checks": {}
                          },
                          "requirements": {
                            "due": [],
                            "missing_fields": [
                              "mother_name",
                              "phone",
                              "residential_address",
                              "source_of_funds",
                              "expected_monthly_volume_usd",
                              "monthly_income_usd",
                              "occupation",
                              "is_pep",
                              "account_purpose"
                            ]
                          },
                          "created_at": "2026-07-12T18:00:00Z",
                          "updated_at": "2026-07-12T18:05:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null,
                      "still_required": [
                        "company_registration",
                        "ownership_structure",
                        "source_of_funds"
                      ]
                    }
                  },
                  "incompleto": {
                    "summary": "A record OZAV cannot fully represent, alongside a complete one",
                    "description": "The second record has no recorded verification posture, so `verification.mode`, `individual` and `business` are omitted and `requirements.missing_fields` names what is still needed. Note the first record is unaffected: one incomplete record never fails the call, and its `status` must not be read as a verification attestation while `mode` is absent.\n",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "object": "customer",
                          "type": "individual",
                          "status": "approved",
                          "email": "ana@example.com",
                          "individual": {
                            "legal_name": "Ana Ramos",
                            "tax_id": {
                              "country": "BR",
                              "type": "cpf",
                              "value": "39053344705"
                            }
                          },
                          "verification": {
                            "mode": "full",
                            "status": "approved",
                            "checks": {}
                          },
                          "requirements": {
                            "due": [],
                            "missing_fields": [
                              "mother_name",
                              "phone",
                              "residential_address",
                              "source_of_funds",
                              "expected_monthly_volume_usd",
                              "monthly_income_usd",
                              "occupation",
                              "is_pep",
                              "account_purpose"
                            ]
                          },
                          "created_at": "2026-07-12T18:00:00Z",
                          "updated_at": "2026-07-12T18:05:00Z"
                        },
                        {
                          "id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                          "object": "customer",
                          "type": "business",
                          "status": "approved",
                          "email": "contato@example.com",
                          "verification": {
                            "status": "approved",
                            "checks": {}
                          },
                          "requirements": {
                            "due": [],
                            "missing_fields": [
                              "legal_name",
                              "registration",
                              "registered_address"
                            ]
                          },
                          "created_at": "2026-07-12T18:10:00Z",
                          "updated_at": "2026-07-12T18:10:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/customers\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/customers/terms-sessions": {
      "post": {
        "operationId": "createTermsSession",
        "tags": [
          "Customers"
        ],
        "summary": "Create a terms session",
        "description": "Creates a terms-of-service session. Customer provisioning at a settlement\npartner is fail-closed on an accepted terms session: `POST /customers`\nreturns `422 TERMS_ACCEPTANCE_REQUIRED` until you submit the\n`terms_session_id` of a session that has been accepted.\n\nThe response carries `id` - the session token - **once, at creation**.\nOZAV stores only its hash, so it is never returned again. Persist it\nbefore you present the terms to your end user.\n\nPresent `terms_url` to the end user, collect their acceptance in your own\nflow, then record it with `POST /customers/terms-sessions/accept`.\nSessions expire; `expires_at` is absolute.\n\nAccepts an optional `Idempotency-Key`. Replaying the same key returns the\nsame session **without** `id` - the token is only ever issued once.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyOptional"
          }
        ],
        "responses": {
          "200": {
            "description": "Idempotent replay - the same session, without the token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TermsSession"
                }
              }
            }
          },
          "201": {
            "description": "Terms session created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TermsSession"
                },
                "examples": {
                  "created": {
                    "summary": "Created - token returned once",
                    "value": {
                      "id": "ts_8Kd2mQ4vR7xN1pL5wT3yB6zA9cF0hJ2s",
                      "object": "terms_session",
                      "status": "pending",
                      "terms_url": "https://ozav.io/terms",
                      "terms_version": "2.0",
                      "expires_at": "2026-09-03T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/terms-sessions \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/terms-sessions\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/customers/terms-sessions\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/customers/terms-sessions/accept": {
      "post": {
        "operationId": "acceptTermsSession",
        "tags": [
          "Customers"
        ],
        "summary": "Record acceptance of a terms session",
        "description": "Records that your end user accepted the terms of the session, together\nwith the evidence of that acceptance. You collect the acceptance in your\nown flow; this call is your attestation of it, and OZAV stores it against\nthe terms version that was in force when the session was created.\n\nIdempotent: repeating the call returns the same result and never\noverwrites the original evidence. The session token is **not** returned.\n\nAfter a `200`, submit the same `terms_session_id` to `POST /customers`.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TermsSessionAcceptRequest"
              },
              "examples": {
                "accept": {
                  "summary": "Attest the acceptance you collected",
                  "value": {
                    "terms_session_id": "ts_8Kd2mQ4vR7xN1pL5wT3yB6zA9cF0hJ2s",
                    "accepted_by": {
                      "legal_name": "Ana Ramos",
                      "email": "ana@example.com"
                    },
                    "accepted_at": "2026-09-02T17:41:09Z",
                    "ip_address": "203.0.113.42",
                    "user_agent": "Mozilla/5.0"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acceptance recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TermsSession"
                },
                "examples": {
                  "accepted": {
                    "summary": "Accepted",
                    "value": {
                      "object": "terms_session",
                      "status": "accepted",
                      "terms_url": "https://ozav.io/terms",
                      "terms_version": "2.0",
                      "expires_at": "2026-09-03T18:00:00Z",
                      "accepted_at": "2026-09-02T17:41:09Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/terms-sessions/accept \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"terms_session_id\": \"ts_8Kd2mQ4vR7xN1pL5wT3yB6zA9cF0hJ2s\",\n    \"accepted_by\": {\n      \"legal_name\": \"Ana Ramos\",\n      \"email\": \"ana@example.com\"\n    },\n    \"accepted_at\": \"2026-09-02T17:41:09Z\",\n    \"ip_address\": \"203.0.113.42\",\n    \"user_agent\": \"Mozilla/5.0\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/terms-sessions/accept\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"terms_session_id\": \"ts_8Kd2mQ4vR7xN1pL5wT3yB6zA9cF0hJ2s\",\n    \"accepted_by\": {\n      \"legal_name\": \"Ana Ramos\",\n      \"email\": \"ana@example.com\"\n    },\n    \"accepted_at\": \"2026-09-02T17:41:09Z\",\n    \"ip_address\": \"203.0.113.42\",\n    \"user_agent\": \"Mozilla/5.0\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/customers/terms-sessions/accept\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"terms_session_id\": \"ts_8Kd2mQ4vR7xN1pL5wT3yB6zA9cF0hJ2s\",\n        \"accepted_by\": {\n            \"legal_name\": \"Ana Ramos\",\n            \"email\": \"ana@example.com\",\n        },\n        \"accepted_at\": \"2026-09-02T17:41:09Z\",\n        \"ip_address\": \"203.0.113.42\",\n        \"user_agent\": \"Mozilla/5.0\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/customers/{customer_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerId"
        }
      ],
      "get": {
        "operationId": "getCustomer",
        "tags": [
          "Customers"
        ],
        "summary": "Retrieve a customer",
        "description": "Retrieves a single customer by its `ozav_cus_` id, including its current\n`status`, `verification` posture, and outstanding `requirements`. An id\nthat does not exist or belongs to another organization returns `404`.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                },
                "examples": {
                  "approved": {
                    "summary": "A verified individual customer",
                    "value": {
                      "id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "customer",
                      "type": "individual",
                      "status": "approved",
                      "external_ref": "your-user-4821",
                      "email": "ana@example.com",
                      "individual": {
                        "legal_name": "Ana Ramos",
                        "date_of_birth": "1990-04-12",
                        "nationality": "BR",
                        "tax_id": {
                          "country": "BR",
                          "type": "cpf",
                          "value": "39053344705"
                        }
                      },
                      "verification": {
                        "mode": "full",
                        "status": "approved",
                        "checks": {
                          "kyc_document": {
                            "outcome": "pass",
                            "verified_at": "2026-07-12T18:04:00Z"
                          }
                        }
                      },
                      "requirements": {
                        "due": [],
                        "missing_fields": [
                          "mother_name",
                          "phone",
                          "residential_address",
                          "source_of_funds",
                          "expected_monthly_volume_usd",
                          "monthly_income_usd",
                          "occupation",
                          "is_pep",
                          "account_purpose"
                        ]
                      },
                      "created_at": "2026-07-12T18:00:00Z",
                      "updated_at": "2026-07-12T18:05:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "patch": {
        "operationId": "updateCustomer",
        "tags": [
          "Customers"
        ],
        "summary": "Update a customer",
        "description": "Updates a customer's mutable attributes (contact fields, identity blocks,\nmetadata). Only the fields you include are changed; omitted fields are\nleft untouched. Returns the updated `Customer`.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerUpdateRequest"
              },
              "examples": {
                "contact_update": {
                  "summary": "Update contactable fields",
                  "value": {
                    "email": "ana.ramos@example.com",
                    "metadata": {
                      "tier": "gold"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                },
                "examples": {
                  "updated": {
                    "summary": "Contact email and metadata updated",
                    "value": {
                      "id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "customer",
                      "type": "individual",
                      "status": "approved",
                      "email": "ana.ramos@example.com",
                      "individual": {
                        "legal_name": "Ana Ramos",
                        "tax_id": {
                          "country": "BR",
                          "type": "cpf",
                          "value": "39053344705"
                        }
                      },
                      "verification": {
                        "mode": "full",
                        "status": "approved",
                        "checks": {}
                      },
                      "requirements": {
                        "due": [],
                        "missing_fields": [
                          "mother_name",
                          "phone",
                          "residential_address",
                          "source_of_funds",
                          "expected_monthly_volume_usd",
                          "monthly_income_usd",
                          "occupation",
                          "is_pep",
                          "account_purpose"
                        ]
                      },
                      "created_at": "2026-07-12T18:00:00Z",
                      "updated_at": "2026-07-12T18:10:00Z",
                      "metadata": {
                        "tier": "gold"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -X PATCH \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"email\": \"ana.ramos@example.com\",\n    \"metadata\": {\n      \"tier\": \"gold\"\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"PATCH\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"email\": \"ana.ramos@example.com\",\n    \"metadata\": {\n      \"tier\": \"gold\"\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.patch(\n    \"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"email\": \"ana.ramos@example.com\",\n        \"metadata\": {\n            \"tier\": \"gold\",\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/customers/{customer_id}/associated-persons": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerId"
        }
      ],
      "post": {
        "operationId": "createAssociatedPerson",
        "tags": [
          "Customers"
        ],
        "summary": "Add an associated person (UBO, director, officer, or signer) to a business customer",
        "description": "Adds an associated person - a UBO, director, officer, or authorized\nsigner - to a business customer, for KYB. Supply one or more `roles` and\nthe person's `individual` identity block (and `ownership_percent` for\nowners). Valid only for `business` customers. Returns the created\n`AssociatedPerson`.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssociatedPersonCreateRequest"
              },
              "examples": {
                "ubo": {
                  "summary": "A 40%-owning UBO who is also a director",
                  "value": {
                    "roles": [
                      "ubo",
                      "director"
                    ],
                    "ownership_percent": 40,
                    "individual": {
                      "legal_name": "Carlos Souza",
                      "tax_id": {
                        "country": "BR",
                        "type": "cpf",
                        "value": "39053344705"
                      }
                    },
                    "email": "carlos@acme.com.br"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Associated person created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssociatedPerson"
                },
                "examples": {
                  "ubo": {
                    "summary": "A 40%-owning UBO who is also a director",
                    "value": {
                      "id": "ozav_ap_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "associated_person",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "roles": [
                        "ubo",
                        "director"
                      ],
                      "ownership_percent": 40,
                      "individual": {
                        "legal_name": "Carlos Souza",
                        "tax_id": {
                          "country": "BR",
                          "type": "cpf",
                          "value": "39053344705"
                        }
                      },
                      "email": "carlos@acme.com.br",
                      "verification": {
                        "mode": "full",
                        "status": "pending",
                        "checks": {}
                      },
                      "created_at": "2026-07-12T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"roles\": [\n      \"ubo\",\n      \"director\"\n    ],\n    \"ownership_percent\": 40,\n    \"individual\": {\n      \"legal_name\": \"Carlos Souza\",\n      \"tax_id\": {\n        \"country\": \"BR\",\n        \"type\": \"cpf\",\n        \"value\": \"39053344705\"\n      }\n    },\n    \"email\": \"carlos@acme.com.br\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"roles\": [\n      \"ubo\",\n      \"director\"\n    ],\n    \"ownership_percent\": 40,\n    \"individual\": {\n      \"legal_name\": \"Carlos Souza\",\n      \"tax_id\": {\n        \"country\": \"BR\",\n        \"type\": \"cpf\",\n        \"value\": \"39053344705\"\n      }\n    },\n    \"email\": \"carlos@acme.com.br\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"roles\": [\n            \"ubo\",\n            \"director\",\n        ],\n        \"ownership_percent\": 40,\n        \"individual\": {\n            \"legal_name\": \"Carlos Souza\",\n            \"tax_id\": {\n                \"country\": \"BR\",\n                \"type\": \"cpf\",\n                \"value\": \"39053344705\",\n            },\n        },\n        \"email\": \"carlos@acme.com.br\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listAssociatedPersons",
        "tags": [
          "Customers"
        ],
        "summary": "List a customer's associated persons",
        "description": "Lists the associated persons of a business customer, using the standard\ncursor pagination. Returns a `Page` of `AssociatedPerson`.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          }
        ],
        "responses": {
          "200": {
            "description": "Associated persons.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/AssociatedPerson"
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "page": {
                    "summary": "One page of associated persons",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_ap_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "object": "associated_person",
                          "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "roles": [
                            "ubo",
                            "director"
                          ],
                          "ownership_percent": 40,
                          "individual": {
                            "legal_name": "Carlos Souza",
                            "tax_id": {
                              "country": "BR",
                              "type": "cpf",
                              "value": "39053344705"
                            }
                          },
                          "verification": {
                            "mode": "full",
                            "status": "pending",
                            "checks": {}
                          },
                          "created_at": "2026-07-12T18:00:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/customers/{customer_id}/associated-persons/{person_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerId"
        },
        {
          "name": "person_id",
          "in": "path",
          "required": true,
          "description": "The opaque associated-person id (`ozav_ap_...`).",
          "schema": {
            "type": "string"
          }
        }
      ],
      "patch": {
        "operationId": "updateAssociatedPerson",
        "tags": [
          "Customers"
        ],
        "summary": "Update an associated person",
        "description": "Updates an associated person's mutable attributes (roles, ownership\npercentage, identity block). Only included fields are changed. Returns\nthe updated `AssociatedPerson`.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssociatedPersonUpdateRequest"
              },
              "examples": {
                "ownership": {
                  "summary": "Revise ownership",
                  "value": {
                    "ownership_percent": 55
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated associated person.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssociatedPerson"
                },
                "examples": {
                  "updated": {
                    "summary": "Ownership revised to 55%",
                    "value": {
                      "id": "ozav_ap_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "associated_person",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "roles": [
                        "ubo",
                        "director"
                      ],
                      "ownership_percent": 55,
                      "individual": {
                        "legal_name": "Carlos Souza",
                        "tax_id": {
                          "country": "BR",
                          "type": "cpf",
                          "value": "39053344705"
                        }
                      },
                      "verification": {
                        "mode": "full",
                        "status": "pending",
                        "checks": {}
                      },
                      "created_at": "2026-07-12T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons/ozav_ap_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -X PATCH \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"ownership_percent\": 55\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons/ozav_ap_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"PATCH\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"ownership_percent\": 55\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.patch(\n    \"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons/ozav_ap_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"ownership_percent\": 55,\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/customers/{customer_id}/documents": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerId"
        }
      ],
      "post": {
        "operationId": "createCustomerDocument",
        "tags": [
          "Customers"
        ],
        "summary": "Start a supporting-document upload for a customer",
        "description": "Declares a provider-neutral KYC/KYB document and returns a short-lived\n`upload_url` the client PUTs the file bytes to. Documents may belong to\nthe customer itself or to one of its associated persons. Use generic\ndocument categories such as `company_registration`, `identity_document`,\n`selfie`, `liveness`, `proof_of_address`, `ownership_structure`,\n`financial_statement`, and `source_of_funds`; OZAV maps those into any\nprovider-specific payload behind the public API.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerDocumentUploadRequest"
              },
              "examples": {
                "proof_of_address": {
                  "summary": "Declare a proof-of-address PDF",
                  "value": {
                    "doc_type": "proof_of_address",
                    "mime": "application/pdf"
                  }
                },
                "associated_person_identity_front": {
                  "summary": "Declare a representative identity document",
                  "value": {
                    "subject_type": "associated_person",
                    "associated_person_id": "ozav_ap_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "doc_type": "identity_document",
                    "side": "front",
                    "issuing_country": "BR",
                    "mime": "image/jpeg"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Upload grant created; PUT the file bytes to `upload_url`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerDocumentUpload"
                },
                "examples": {
                  "created": {
                    "summary": "A pending-upload document with its signed URL",
                    "value": {
                      "id": "ozav_doc_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "document",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "subject_type": "customer",
                      "doc_type": "proof_of_address",
                      "side": null,
                      "issuing_country": null,
                      "status": "pending_upload",
                      "mime": "application/pdf",
                      "size_bytes": null,
                      "sha256": null,
                      "created_at": "2026-07-12T18:00:00Z",
                      "updated_at": "2026-07-12T18:00:00Z",
                      "upload_url": "https://storage.ozav.io/upload/ozav_cus_01J8.../ozav_doc_01J8...",
                      "upload_expires_at": "2026-07-12T20:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"doc_type\": \"proof_of_address\",\n    \"mime\": \"application/pdf\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"doc_type\": \"proof_of_address\",\n    \"mime\": \"application/pdf\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"doc_type\": \"proof_of_address\",\n        \"mime\": \"application/pdf\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listCustomerDocuments",
        "tags": [
          "Customers"
        ],
        "summary": "List a customer's supporting documents",
        "description": "Lists a customer's uploaded supporting documents, using the standard cursor\npagination. Returns a `Page` of `CustomerDocument`. Never returns the raw\nstorage location or a download URL.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          }
        ],
        "responses": {
          "200": {
            "description": "Customer documents.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CustomerDocument"
                          }
                        },
                        "still_required": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CustomerDocumentType"
                          },
                          "description": "The document categories this customer still owes, computed over ALL of its documents rather than the page you are reading, so paging does not change the answer. A document counts only once its bytes have arrived: one you declared but never uploaded, and one that failed review, both still appear here.\n\n**Read its ABSENCE carefully. An empty array and a missing field mean opposite things.** An empty array says the customer owes nothing. The field is OMITTED when OZAV does not model a required set for this customer, which today means only a `type` that is neither `individual` nor `business`. Individuals and businesses of any country are modelled. Omitted never means \"nothing is required\": it means we will not guess. It is also omitted if the summary could not be computed, so that a failed read can never be read as a clean bill.\n\nThe set depends on the customer and the jurisdiction, never on a provider. An individual owes a photo ID and a proof of address (plus a tax return above the high-volume threshold). A business owes its incorporation papers, and its ownership registry where the country and legal form keep them as separate documents. A business registered in Brazil additionally owes the financial pack the local account rail requires.\nThis is guidance, not the gate. The `422` from account opening remains the authoritative checklist, and this list is built to never ask for less than that `422` will."
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "page": {
                    "summary": "One page of documents",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_doc_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "object": "document",
                          "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "doc_type": "proof_of_address",
                          "status": "received",
                          "mime": "application/pdf",
                          "size_bytes": 20480,
                          "sha256": "b9f3a1c2d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f",
                          "created_at": "2026-07-12T18:00:00Z",
                          "updated_at": "2026-07-12T18:05:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/customers/{customer_id}/documents/{document_id}/upload": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerId"
        },
        {
          "$ref": "#/components/parameters/DocumentId"
        }
      ],
      "put": {
        "operationId": "uploadCustomerDocumentBytes",
        "tags": [
          "Customers"
        ],
        "summary": "Upload supporting-document bytes",
        "description": "Uploads file bytes to the short-lived OZAV-hosted `upload_url`\nreturned by `POST /customers/{customer_id}/documents`. Send the same\nAPI key used to create the grant. OZAV stores the file in private\nstorage, recomputes size and SHA-256 server-side, and moves the\ndocument to `received` when the bytes are accepted.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "upload_token",
            "in": "query",
            "required": true,
            "description": "Short-lived upload token embedded in the `upload_url`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/pdf": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/webp": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/heic": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Document bytes accepted and the document is marked received.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerDocument"
                },
                "examples": {
                  "received": {
                    "summary": "A received supporting document",
                    "value": {
                      "id": "ozav_doc_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "document",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "doc_type": "proof_of_address",
                      "status": "received",
                      "mime": "application/pdf",
                      "size_bytes": 2048,
                      "sha256": "5f70bf18a08600701687fb0d4f009dc7bbdc9e2b0e7f2d7d1e04a1fbf30abbb5",
                      "created_at": "2026-07-12T18:00:00Z",
                      "updated_at": "2026-07-12T18:02:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents/{document_id}/upload \\\n  -X PUT \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents/{document_id}/upload\", {\n  method: \"PUT\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.put(\n    \"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents/{document_id}/upload\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/customers/{customer_id}/evidence": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerId"
        }
      ],
      "get": {
        "operationId": "getCustomerEvidencePack",
        "tags": [
          "Compliance"
        ],
        "summary": "Retrieve the verification and decision record for a customer",
        "description": "Returns the **verification and decision record** for one customer: what\nOZAV verified, which lists were screened, which documents were assessed,\nwhich decisions were taken on their movements, and what happened\nafterwards. Hand it to your bank or your regulator as evidence that the\ndecision was made and can be defended.\n\nAsk for `format=html` to get a printable document instead of JSON; both\ncome from the same record, so they can never disagree.\n\n**An empty section means there is no record, never approval.** A section\nthat could not be read is listed in `secoesIndisponiveis` and the verdict\nfalls back to `em_analise` - the record never asserts what it did not\nread.\n\nThe record is assembled at request time and carries `geradoEm`. There is\nno frozen document id on purpose: a compliance document that freezes\nages silently, and an \"approved\" from three months ago shown to a bank\nwhile a new alert is open is worse than no document at all.\n",
        "x-ozav-entitlement": "compliance",
        "x-ozav-scopes": [
          "evidence:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "`json` (default) or `html` for a printable document.",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "html"
              ]
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Language of the printable document: `pt` (default) or `en`.",
            "schema": {
              "type": "string",
              "enum": [
                "pt",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The verification and decision record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvidencePack"
                }
              },
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/evidence \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/evidence\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/evidence\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/customers/{customer_id}/credential": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CustomerId"
        }
      ],
      "get": {
        "operationId": "getCustomerCredential",
        "tags": [
          "Customers"
        ],
        "summary": "Issue a portable verification credential for an approved customer",
        "description": "Returns a **portable credential** signed by OZAV that attests what OZAV\nverified about this customer, when, and how - as an IETF SD-JWT\n(`vc+sd-jwt`, ES256). Hand it to the end user: they can present it to\nany third party and choose which attributes to disclose (for example\n`over_18` without `cpf`). Any verifier checks it offline against the\npublic key at `jwks_url` and confirms it is still in force at\n`status_url`.\n\n**Selectively disclosable claims:** `cpf`, `full_name`, `birth_date`,\n`over_18`, `kyc_level` (`3` = hosted session with document, liveness and\nface match; `2` = document-based identification), `method`,\n`verified_at`, `screening`. The JWT body itself carries no personal\ndata - only salted digests - so the credential leaks nothing the holder\ndoes not disclose.\n\nEach call issues a **new** credential (new `id`). Earlier ones stay in\nforce until they expire or the customer's status changes: a credential\nis reported `revoked` at `status_url` as soon as the customer is no\nlonger approved, is frozen, or is re-verified. There is no revocation\nstep for you to call. Issuance is capped per customer per day\n(`429 CREDENTIAL_ISSUANCE_LIMIT_REACHED` past the cap): keep the\ncredential you received rather than requesting one per use.\n\nIndividual customers identified by a Brazilian CPF only. Not issued in\nthe sandbox environment. Availability is enabled per organization by\nOZAV; until then the call returns `422 FEATURE_NOT_AVAILABLE`.\n",
        "x-ozav-entitlement": "customers",
        "x-ozav-scopes": [
          "customers:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "A freshly issued credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerCredential"
                },
                "examples": {
                  "issued": {
                    "summary": "A level-3 credential from a hosted session",
                    "value": {
                      "id": "ozav_vc_Q2xhdWRlRmFibGU1MQ",
                      "object": "credential",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "format": "vc+sd-jwt",
                      "credential": "eyJhbGciOiJFUzI1NiIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Im96YXYtMjAyNi0wOSJ9.eyJpc3MiOiJodHRwczovL2FwaS5vemF2LmlvIn0.MEUCIQ~WyJzYWx0Iiwib3Zlcl8xOCIsdHJ1ZV0~",
                      "kyc_level": 3,
                      "method": "hosted_session",
                      "issued_at": "2026-09-05T12:00:00Z",
                      "expires_at": "2027-09-05T12:00:00Z",
                      "jwks_url": "https://api.ozav.io/functions/v1/credencial-well-known/jwks.json",
                      "status_url": "https://api.ozav.io/functions/v1/credencial-well-known/credential-status/ozav_vc_Q2xhdWRlRmFibGU1MQ"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`CREDENTIAL_SIGNING_UNAVAILABLE` - transient; retry with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/credential \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/credential\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/credential\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/verifications": {
      "get": {
        "operationId": "listVerifications",
        "tags": [
          "Verifications"
        ],
        "summary": "List verification sessions",
        "description": "Lists verification sessions, newest first, with the standard cursor\npagination. Filter to one customer with the `customer_id` query\nparameter to recover an existing verification id after `POST\n/verifications` or after a duplicate `VERIFICATION_ALREADY_EXISTS`\nresponse. Returns a `Page` of `Verification`.\n",
        "x-ozav-entitlement": "compliance",
        "x-ozav-scopes": [
          "verifications:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "customer_id",
            "in": "query",
            "description": "Filter to a single customer by its opaque `ozav_cus_` id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated verification sessions.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Verification"
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "page": {
                    "summary": "One page of verifications",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "object": "verification",
                          "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "status": "pending",
                          "checks": {
                            "kyc_document": {
                              "outcome": "not_started"
                            },
                            "liveness": {
                              "outcome": "not_started"
                            }
                          },
                          "created_at": "2026-07-12T18:00:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/verifications \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/verifications\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/verifications\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "post": {
        "operationId": "createVerification",
        "tags": [
          "Verifications"
        ],
        "summary": "Start a verification session for a customer",
        "description": "Starts one or more verification checks (hosted session when the check\nrequires end-user interaction). Check kinds are the canonical OZAV\ncapability taxonomy.\n\nVerification records persist and can be listed with `GET\n/verifications`. Hosted `session_url` issuance is gated by your\norganization's capabilities and may be absent.\n\nSANDBOX: a test key (`ozav_sk_test_`) NEVER receives a `session_url` and\nnever reaches the identity provider - a sandbox verification has no real\nend-user to redirect. Create the verification, then drive it to its end\nstate with `POST /verifications/{id}/simulate`.\n",
        "x-ozav-entitlement": "compliance",
        "x-ozav-scopes": [
          "verifications:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "customer_id",
                  "capabilities"
                ],
                "properties": {
                  "customer_id": {
                    "type": "string",
                    "description": "The `ozav_cus_` id of the customer to verify."
                  },
                  "capabilities": {
                    "type": "array",
                    "minItems": 1,
                    "description": "The checks to run, from the canonical capability taxonomy (see `CapabilityKind`).",
                    "items": {
                      "$ref": "#/components/schemas/CapabilityKind"
                    }
                  },
                  "redirect_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Where to return the end-user after a hosted verification session, when one is used."
                  }
                }
              },
              "examples": {
                "kyc": {
                  "summary": "Start KYC document + liveness + name screening",
                  "value": {
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "capabilities": [
                      "kyc_document",
                      "liveness",
                      "aml_name_screening"
                    ],
                    "redirect_url": "https://acme.example/kyc/return"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Verification session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Verification"
                },
                "examples": {
                  "pending": {
                    "summary": "Session created - checks not started yet",
                    "value": {
                      "id": "ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "verification",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "pending",
                      "checks": {
                        "kyc_document": {
                          "outcome": "not_started"
                        },
                        "liveness": {
                          "outcome": "not_started"
                        },
                        "aml_name_screening": {
                          "outcome": "not_started"
                        }
                      },
                      "created_at": "2026-07-12T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/verifications \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"capabilities\": [\n      \"kyc_document\",\n      \"liveness\",\n      \"aml_name_screening\"\n    ],\n    \"redirect_url\": \"https://acme.example/kyc/return\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/verifications\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"capabilities\": [\n      \"kyc_document\",\n      \"liveness\",\n      \"aml_name_screening\"\n    ],\n    \"redirect_url\": \"https://acme.example/kyc/return\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/verifications\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n        \"capabilities\": [\n            \"kyc_document\",\n            \"liveness\",\n            \"aml_name_screening\",\n        ],\n        \"redirect_url\": \"https://acme.example/kyc/return\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/verifications/{verification_id}": {
      "get": {
        "operationId": "getVerification",
        "tags": [
          "Verifications"
        ],
        "summary": "Retrieve a verification session",
        "description": "Retrieves a verification session by its `ozav_vrf_` id. Poll this after\n`POST /verifications` to watch each requested check move through its\n`CheckOutcome` and the session `status` reach `completed`.\n",
        "x-ozav-entitlement": "compliance",
        "x-ozav-scopes": [
          "verifications:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "verification_id",
            "in": "path",
            "required": true,
            "description": "The opaque verification id (`ozav_vrf_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The verification session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Verification"
                },
                "examples": {
                  "in_progress": {
                    "summary": "Checks running - poll until status is completed",
                    "value": {
                      "id": "ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "verification",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "in_progress",
                      "checks": {
                        "kyc_document": {
                          "outcome": "pass",
                          "verified_at": "2026-07-12T18:04:00Z"
                        },
                        "liveness": {
                          "outcome": "pending"
                        },
                        "aml_name_screening": {
                          "outcome": "pass",
                          "verified_at": "2026-07-12T18:04:30Z"
                        }
                      },
                      "created_at": "2026-07-12T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/verifications/ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/verifications/ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/verifications/ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/verifications/{verification_id}/simulate": {
      "post": {
        "operationId": "simulateVerification",
        "tags": [
          "Verifications"
        ],
        "summary": "Simulate a sandbox verification result",
        "description": "Sandbox-only helper for test API keys. Marks a verification as approved\nor rejected and updates the linked customer's KYC status so downstream\nsandbox flows, including simulated wallet reads, BRL/Pix account reads,\nand card issuance, can be exercised without fake identity documents.\nSandbox wallet and account records are projections only; they do not\ncreate an on-chain address or open a real bank account.\n",
        "x-ozav-entitlement": "compliance",
        "x-ozav-scopes": [
          "verifications:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "verification_id",
            "in": "path",
            "required": true,
            "description": "The opaque verification id (`ozav_vrf_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "outcome"
                ],
                "properties": {
                  "outcome": {
                    "type": "string",
                    "enum": [
                      "approved",
                      "rejected"
                    ]
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 255
                  }
                }
              },
              "examples": {
                "approved": {
                  "value": {
                    "outcome": "approved"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated verification.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Verification"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/verifications/ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1/simulate \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"outcome\": \"approved\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/verifications/ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1/simulate\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"outcome\": \"approved\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/verifications/ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1/simulate\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"outcome\": \"approved\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/screenings": {
      "post": {
        "operationId": "createScreening",
        "tags": [
          "Screenings"
        ],
        "summary": "Screen a person, business, customer, or wallet address against designated lists",
        "description": "Point-in-time screening against OZAV's in-house designated-persons\ndataset (UN, OFAC, EU, UK and national sources, refreshed daily).\nSubjects may be inline (person/business) or reference an existing\ncustomer.\n",
        "x-ozav-entitlement": "compliance",
        "x-ozav-scopes": [
          "screenings:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "subject"
                ],
                "properties": {
                  "subject": {
                    "$ref": "#/components/schemas/ScreeningSubject"
                  },
                  "lists": {
                    "type": "array",
                    "description": "Which designated lists to screen against. Each item is one of:\n- `sanctions` - consolidated sanctions / designated-persons lists\n  (UN, OFAC, EU, UK and national sources). Consulted for every subject.\n- `pep` - politically exposed persons. Consulted in-house for subjects\n  whose `country` is `BR` (the Brazilian PEP register, refreshed\n  monthly; `list_source` is `national`). For any other or missing\n  country the list cannot be consulted and the result is `review`\n  with an empty `matches` array.\n- `adverse_media` - negative-news screening. Not consultable yet: the\n  result is `review` with an empty `matches` array.\n\nDefaults to `[ sanctions ]` when omitted.\n",
                    "items": {
                      "type": "string",
                      "enum": [
                        "sanctions",
                        "pep",
                        "adverse_media"
                      ]
                    },
                    "default": [
                      "sanctions"
                    ]
                  }
                }
              },
              "examples": {
                "person": {
                  "summary": "Screen a person against sanctions + PEP",
                  "value": {
                    "subject": {
                      "person": {
                        "name": "Jane Example Doe",
                        "date_of_birth": "1980-04-12",
                        "country": "BR"
                      }
                    },
                    "lists": [
                      "sanctions",
                      "pep"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Screening result (synchronous when possible).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Screening"
                },
                "examples": {
                  "clear": {
                    "summary": "No match",
                    "value": {
                      "id": "ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "screening",
                      "subject": {
                        "person": {
                          "name": "Jane Example Doe",
                          "date_of_birth": "1980-04-12",
                          "country": "BR"
                        }
                      },
                      "lists": [
                        "sanctions",
                        "pep"
                      ],
                      "status": "completed",
                      "result": "clear",
                      "matches": [],
                      "created_at": "2026-07-12T18:00:00Z"
                    }
                  },
                  "match_found": {
                    "summary": "A match - read list_source, matched_name, and strength",
                    "value": {
                      "id": "ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "screening",
                      "subject": {
                        "person": {
                          "name": "Jane Example Doe",
                          "date_of_birth": "1980-04-12",
                          "country": "BR"
                        }
                      },
                      "lists": [
                        "sanctions",
                        "pep"
                      ],
                      "status": "completed",
                      "result": "match_found",
                      "matches": [
                        {
                          "list_source": "ofac",
                          "matched_name": "JANE E. DOE",
                          "strength": "alias",
                          "list_entry_ref": "SDN-12345"
                        }
                      ],
                      "created_at": "2026-07-12T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/screenings \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"subject\": {\n      \"person\": {\n        \"name\": \"Jane Example Doe\",\n        \"date_of_birth\": \"1980-04-12\",\n        \"country\": \"BR\"\n      }\n    },\n    \"lists\": [\n      \"sanctions\",\n      \"pep\"\n    ]\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/screenings\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"subject\": {\n      \"person\": {\n        \"name\": \"Jane Example Doe\",\n        \"date_of_birth\": \"1980-04-12\",\n        \"country\": \"BR\"\n      }\n    },\n    \"lists\": [\n      \"sanctions\",\n      \"pep\"\n    ]\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/screenings\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"subject\": {\n            \"person\": {\n                \"name\": \"Jane Example Doe\",\n                \"date_of_birth\": \"1980-04-12\",\n                \"country\": \"BR\",\n            },\n        },\n        \"lists\": [\n            \"sanctions\",\n            \"pep\",\n        ],\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listScreenings",
        "tags": [
          "Screenings"
        ],
        "summary": "List screenings",
        "description": "Lists screenings, newest first, with the standard cursor pagination.\nFilter to one subject with the `customer_id` query parameter to\nreconstruct a customer's screening trail for audit. Returns a `Page` of\n`Screening`.\n",
        "x-ozav-entitlement": "compliance",
        "x-ozav-scopes": [
          "screenings:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "customer_id",
            "in": "query",
            "description": "Filter to a single customer by its opaque `ozav_cus_` id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated screenings.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Screening"
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "page": {
                    "summary": "One page of screenings",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "object": "screening",
                          "subject": {
                            "person": {
                              "name": "Jane Example Doe",
                              "date_of_birth": "1980-04-12",
                              "country": "BR"
                            }
                          },
                          "lists": [
                            "sanctions",
                            "pep"
                          ],
                          "status": "completed",
                          "result": "clear",
                          "matches": [],
                          "created_at": "2026-07-12T18:00:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/screenings \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/screenings\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/screenings\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/screenings/{screening_id}": {
      "get": {
        "operationId": "getScreening",
        "tags": [
          "Screenings"
        ],
        "summary": "Retrieve a screening",
        "description": "Retrieves a single screening by its `ozav_scr_` id, including its\n`result` and any `matches`. Retain the id as evidence of the check.\n",
        "x-ozav-entitlement": "compliance",
        "x-ozav-scopes": [
          "screenings:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "screening_id",
            "in": "path",
            "required": true,
            "description": "The opaque screening id (`ozav_scr_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The screening.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Screening"
                },
                "examples": {
                  "match_found": {
                    "summary": "A match - read list_source, matched_name, and strength",
                    "value": {
                      "id": "ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "screening",
                      "subject": {
                        "person": {
                          "name": "Jane Example Doe",
                          "date_of_birth": "1980-04-12",
                          "country": "BR"
                        }
                      },
                      "lists": [
                        "sanctions"
                      ],
                      "status": "completed",
                      "result": "match_found",
                      "matches": [
                        {
                          "list_source": "ofac",
                          "matched_name": "JANE E. DOE",
                          "strength": "alias",
                          "list_entry_ref": "SDN-12345"
                        }
                      ],
                      "created_at": "2026-07-12T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/screenings/ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/screenings/ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/screenings/ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/quotes": {
      "post": {
        "operationId": "createQuote",
        "tags": [
          "Quotes"
        ],
        "summary": "Create an FX quote",
        "description": "Quotes are opaque, short-lived, and carry the OZAV all-in client-facing\nrate. When enabled, they also carry the additional client-selected\nspread in basis points. A quote is executed by creating a transfer that\nreferences it.\n\n**Setting your own spread (revenue share).** Pass the optional\n`spread_bps` to add basis points on top of the OZAV all-in rate for\nthis quote. This value is your additional client-selected spread; it\nrides the quote through to settlement and is echoed back as\n`exchange_conditions.spread_bps`. OZAV cost, floor, margin, and split\nanalytics are internal and are never exposed in the public response.\nAvailability of client-selected spread is gated on enablement for your\norganization.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "quotes:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "customer_id",
                  "source",
                  "target"
                ],
                "properties": {
                  "customer_id": {
                    "type": "string",
                    "description": "The opaque customer id (ozav_cus_...). Resolved scoped to the caller; an unknown or cross-tenant id returns 404."
                  },
                  "source": {
                    "type": "object",
                    "required": [
                      "currency"
                    ],
                    "properties": {
                      "currency": {
                        "$ref": "#/components/schemas/Currency"
                      },
                      "amount": {
                        "$ref": "#/components/schemas/DecimalAmount"
                      }
                    }
                  },
                  "target": {
                    "type": "object",
                    "required": [
                      "currency"
                    ],
                    "properties": {
                      "currency": {
                        "$ref": "#/components/schemas/Currency"
                      },
                      "amount": {
                        "$ref": "#/components/schemas/DecimalAmount"
                      }
                    }
                  },
                  "source_rail": {
                    "$ref": "#/components/schemas/Rail"
                  },
                  "destination_rail": {
                    "$ref": "#/components/schemas/Rail"
                  },
                  "spread_bps": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 10000,
                    "description": "Optional. Additional client-selected spread for this quote,\nin integer basis points. Available only when enabled for\nyour organization. The value is applied internally on top of\nthe OZAV all-in rate and echoed back as\n`exchange_conditions.spread_bps`. A non-integer, negative,\nor out-of-range value is rejected with 400.\n"
                  },
                  "funding_source": {
                    "type": "string",
                    "enum": [
                      "incoming_transfer",
                      "named_account_balance"
                    ],
                    "description": "Optional. How the source fiat is funded. `incoming_transfer`\nincludes the measured inbound cost (USD SWIFT US$ 35;\nEUR SEPA / GBP Faster Payments US$ 5, deducted from the\ncredit). `named_account_balance` converts funds already on\nthe named account and includes the outbound cost to the\ndesk (USD wire US$ 35 + 12 bps; EUR/GBP local US$ 5 + 12\nbps, charged on top). Omitted behaves as\n`incoming_transfer` for USD, EUR and GBP.\n"
                  },
                  "facility_preview": {
                    "type": "boolean",
                    "description": "Ask for the **FX rate lock conditions** alongside the quote. When `true` and your organization has the facility contracted, the response carries an extra `facility` block; `POST /transfers` then accepts its `terms_version` as `facility_terms_version`. This is the ONLY place that value comes from.\n\n⚠️ **Silent by design when unavailable.** A client without the facility gets a normal `201` quote with no `facility` block and no error - the preview never fails a quote, because the quote is the product. Read the ABSENCE of the block as \"not available for this account\", and use `POST /transfers` (which refuses loudly with `422 FACILITY_LOCK_UNAVAILABLE` and a `details.reason`) when you need to know why.\n\n⚠️ **It prices, it does not reserve.** `capacity_guaranteed` is always `false`: room under the shared ceiling is resolved at acceptance, inside the lock. A preview that promised capacity would be a promise the design cannot honour."
                  }
                },
                "description": "Provide exactly one of source.amount or target.amount."
              },
              "examples": {
                "brl_to_usd_with_revenue_share": {
                  "summary": "BRL → USD with 40 bps of additional client spread",
                  "value": {
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "source": {
                      "currency": "BRL",
                      "amount": "50000.00"
                    },
                    "target": {
                      "currency": "USD"
                    },
                    "spread_bps": 40
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                },
                "examples": {
                  "quote": {
                    "summary": "OZAV all-in quote with additional client spread",
                    "value": {
                      "id": "ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "quote",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "rate_type": "floating",
                      "rate": "0.1881",
                      "source": {
                        "amount": "50000.00",
                        "currency": "BRL"
                      },
                      "target": {
                        "amount": "9405.00",
                        "currency": "USD"
                      },
                      "exchange_conditions": {
                        "spread_bps": 40
                      },
                      "expires_at": "2026-07-12T18:05:00Z",
                      "created_at": "2026-07-12T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/quotes \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"source\": {\n      \"currency\": \"BRL\",\n      \"amount\": \"50000.00\"\n    },\n    \"target\": {\n      \"currency\": \"USD\"\n    },\n    \"spread_bps\": 40\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/quotes\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"source\": {\n      \"currency\": \"BRL\",\n      \"amount\": \"50000.00\"\n    },\n    \"target\": {\n      \"currency\": \"USD\"\n    },\n    \"spread_bps\": 40\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/quotes\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n        \"source\": {\n            \"currency\": \"BRL\",\n            \"amount\": \"50000.00\",\n        },\n        \"target\": {\n            \"currency\": \"USD\",\n        },\n        \"spread_bps\": 40,\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/quotes/{quote_id}": {
      "get": {
        "operationId": "getQuote",
        "tags": [
          "Quotes"
        ],
        "summary": "Retrieve a quote",
        "description": "Retrieves a quote by its `ozav_q_` id, including its `rate`,\n`exchange_conditions`, and `expires_at`. Quotes are short-lived - a quote\npast its expiry can no longer be executed into a transfer.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "quotes:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "quote_id",
            "in": "path",
            "required": true,
            "description": "The opaque quote id (`ozav_q_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The quote.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                },
                "examples": {
                  "quote": {
                    "summary": "A BRL → USD quote",
                    "value": {
                      "id": "ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "quote",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "rate_type": "floating",
                      "rate": "0.1881",
                      "source": {
                        "amount": "50000.00",
                        "currency": "BRL"
                      },
                      "target": {
                        "amount": "9405.00",
                        "currency": "USD"
                      },
                      "exchange_conditions": {
                        "spread_bps": 40
                      },
                      "expires_at": "2026-07-12T18:05:00Z",
                      "created_at": "2026-07-12T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/quotes/ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/quotes/ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/quotes/ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/transactions": {
      "get": {
        "operationId": "listTransactions",
        "tags": [
          "Transactions"
        ],
        "summary": "List every movement in one timeline",
        "description": "One feed for everything that moved value for your customers: the\ntransfers you created through this API, and the on-chain deposits\ncredited into custody. You no longer have to stitch two listings together\nand guess the order between them.\n\nOrdered newest first. Each row carries a `type` (`transfer` or\n`deposit`), so you can branch without a second lookup, and the `id` is\nthe id of the underlying resource - use it to fetch the full record.\n\nPaginated with the standard opaque cursor. The cursor spans BOTH sources\nat once: it is not one cursor per source, so a busy day on one side can\nnever hide rows from the other.\n\n**Card transactions are not in this feed.** They are read per card at\n`GET /cards/{card_id}/transactions`.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "transfers:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "customer_id",
            "in": "query",
            "description": "Filter to a single customer by its opaque `ozav_cus_` id. Applies to\nboth sources: transfers created for that customer, and the deposits\ncredited to it.\n\nAn id that does not exist, or belongs to another organization or\nanother environment, returns an empty page rather than `404` - the\nsame behaviour as `GET /transfers`, so a filter never tells you\nwhether an id exists somewhere you cannot see.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of movements, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "has_more",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Transaction"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "examples": {
                  "mixed": {
                    "summary": "A transfer and an on-chain deposit in one timeline",
                    "value": {
                      "data": [
                        {
                          "object": "transaction",
                          "id": "ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "type": "transfer",
                          "status": "completed",
                          "amount": "50000.00",
                          "currency": "BRL",
                          "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "created_at": "2026-07-12T18:01:00Z"
                        },
                        {
                          "object": "transaction",
                          "id": "6f499bc1-bf2a-49ef-a1f2-86ec5acc48d1",
                          "type": "deposit",
                          "status": "credited",
                          "amount": "250.000000",
                          "currency": "USDC",
                          "network": "base",
                          "created_at": "2026-07-11T09:20:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "503": {
            "$ref": "#/components/responses/DependencyUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/transactions \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/transactions\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/transactions\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/transfers": {
      "post": {
        "operationId": "createTransfer",
        "tags": [
          "Transfers"
        ],
        "summary": "Create a transfer",
        "description": "The single money-movement resource. The pair (`source.type`,\n`destination.type`) selects the flow - you never call a different\nendpoint for on-ramp vs off-ramp:\n\n| source          | destination          | flow            | availability |\n|-----------------|----------------------|-----------------|--------------|\n| `fiat_payment`  | `payout_destination` | FX (fiat→fiat)  | live         |\n| `fiat_payment`  | `wallet`             | on-ramp         | live         |\n| `wallet`        | `payout_destination` | off-ramp        | live         |\n| `wallet`        | `account`            | off-ramp to the customer's own account | live |\n| `wallet`        | `wallet`             | wallet transfer | live         |\n| `wallet`        | `crypto_address`     | crypto send     | live         |\n\n**Sending to an on-chain address runs on this route.** A `wallet` source\nwith a `crypto_address` destination is a custody move: OZAV signs from the\nsource wallet's vault and broadcasts. No quote is involved (it is a\nsame-asset move, not an exchange), so send `amount` and omit `quote_id`.\nIt needs the `custody` entitlement and is subject to the two controls below.\n\n**Wallet-to-wallet runs here too.** The source wallet must belong to the\n`customer_id` on the request - that is whose money moves. The destination\nonly has to be a wallet in your organization, so you can move value between\ntwo of your customers' wallets. A wallet outside your organization is not\nfound, and a deactivated wallet does not receive.\n\nThe two controls:\n\n- **Managed wallets only.** A self-custody source is refused with\n  `422 WALLET_NOT_MANAGED` - OZAV holds no key to sign with. That send\n  goes through `POST /wallets/{wallet_id}/transaction-intents`, which\n  returns unsigned calldata for the end-user's own key.\n- **Travel Rule (BCB Resolution 520)** on any send that leaves custody.\n  A `crypto_address` destination without complete Travel Rule data is\n  refused with `422 TRAVEL_RULE_REQUIRED`, and `details.missing_fields`\n  names each field that is missing. A destination that is an `ozav_wal_`\n  id is exempt: the move stays inside custody and both sides are already\n  verified.\n\n`POST /wallets/{wallet_id}/transfers` also carries a destination `memo`,\nwhich networks like Stellar need for exchange deposits (an exchange\ncredits by memo). `POST /transfers` has no `memo` field, so a memo-bearing\nsend belongs on the wallets surface even after custody lands here.\n\n**Receiving crypto needs nothing from you, and is not a transfer.** Fund an\naddress from `GET /wallets/{wallet_id}/deposit-addresses` and the credit\nlands on the wallet by itself. You see it in two places already:\n\n- `GET /wallets/{wallet_id}/balances` reflects the new amount;\n- `GET /transactions` lists the arrival alongside your transfers, in one\n  timeline, so you do not have to stitch two listings together.\n\nCreating a transfer with a `crypto_payment` source is refused with\n`422 NOT_AVAILABLE`, and that refusal is permanent rather than pending: an\narrival is observed, never requested, so there is nothing for you to create.\nThe one thing still missing is a *push* notification for it - today you\nlearn about an arrival by reading, not by being told.\n\nBRL/Pix on-ramp returns transaction-specific Pix payment\ninstructions on `source.payment_instructions`. BRL/Pix off-ramp\ncreates a provider-neutral operation only after the customer's stable\nfunds are reserved; final Pix-out remains gated until settlement rails\nare enabled. Other combinations fail closed (`422 RAIL_NOT_AVAILABLE` /\n`NOT_AVAILABLE`) until settlement is enabled - never a silent partial\nresult. See\n`x-ozav-transfer-flow-availability` for the machine-readable status.\n\n`quote_id` is required whenever source and target currencies differ, and\nis authoritative for the amount when present. For a same-currency move (no\nFX, so no quote) provide `amount` - it is how much to move. Exactly one of\n`quote_id` or `amount` MUST be present; supplying both is a 400. The\n`Idempotency-Key` header is required.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "transfers:write"
        ],
        "x-ozav-availability": "live",
        "x-ozav-transfer-flow-availability": {
          "fiat_payment__wallet": "live",
          "fiat_payment__payout_destination": "live",
          "wallet__payout_destination": "live",
          "wallet__wallet": "live",
          "wallet__crypto_address": "live"
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyRequired"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "customer_id",
                  "source",
                  "destination"
                ],
                "description": "Provide `amount` when `quote_id` is omitted (same-currency moves carry the amount here); when `quote_id` is present the quote is authoritative and `amount` is rejected. Exactly one of the two is required.\n",
                "properties": {
                  "customer_id": {
                    "type": "string",
                    "description": "The `ozav_cus_` id of the customer this transfer is for."
                  },
                  "quote_id": {
                    "type": "string",
                    "description": "The `ozav_q_` id of the quote to execute. Required when source and target currencies differ; authoritative for the amount when present."
                  },
                  "amount": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/TransferMonetaryAmount"
                      }
                    ],
                    "description": "Amount to move, for the no-quote same-currency path. Required when `quote_id` is absent; rejected when present.\n"
                  },
                  "source": {
                    "$ref": "#/components/schemas/TransferSource"
                  },
                  "destination": {
                    "$ref": "#/components/schemas/TransferDestination"
                  },
                  "travel_rule": {
                    "$ref": "#/components/schemas/TravelRule"
                  },
                  "metadata": {
                    "$ref": "#/components/schemas/Metadata"
                  },
                  "facility_terms_accepted": {
                    "type": "boolean",
                    "description": "**The FX rate lock (`trava de câmbio`), on this transfer.** Send `true`, together with `facility_terms_version`, to lock the rate at acceptance instead of converting at the price of the moment the money actually moves.\n\nBoth fields are optional and OMITTING THEM IS THE NORMAL PATH - a transfer without them behaves exactly as before and the response carries no `facility` block.\n\n🔴 **Only BRL/Pix flows honour the lock.** The fields validate on every combination, but on a `crypto_address` destination, a wallet-to-wallet move, or the BRS redemption corridor they are accepted and have no effect - no lock is taken and no `facility` block is returned. Always branch on the presence of `facility` in the response rather than assuming the request implied a lock. The lock is a contracted facility: your organization needs a signed schedule (`Termo`) and a funded reserve before it is available, and without them the request is refused with `422 FACILITY_LOCK_UNAVAILABLE`.\n\n⚠️ **Strictly `true`, not truthy.** `\"true\"`, `1` and `{}` all read as \"no lock requested\", silently - the transfer is created unlocked.\n\n⚠️ **Get the version from the preview first:** `POST /quotes` with `facility_preview: true` returns a `facility` block whose `terms_version` is the only value this route accepts. Accepting without a version is refused with `422 FACILITY_TERMS_VERSION_REQUIRED`, because an acceptance that does not say WHAT was accepted cannot support a charge later.\n\n**Where the lock actually engages differs by direction, and the off-ramp case is not obvious.** On a BRL/Pix on-ramp it engages at acceptance. On a BRL/Pix off-ramp it engages ONLY when the customer's custody balance is insufficient - with enough balance the withdrawal takes the ordinary path and the lock does not apply. That outcome is reported in the response's `facility` block rather than passed over in silence."
                  },
                  "facility_terms_version": {
                    "type": "string",
                    "minLength": 1,
                    "description": "The `terms_version` returned by the facility preview (`POST /quotes` with `facility_preview: true`). Required whenever `facility_terms_accepted` is `true`; rejected as stale when it does not match the schedule currently in force for your organization (`422 FACILITY_LOCK_UNAVAILABLE` with `details.reason = terms_version_mismatch` on the on-ramp, `409 PREFUNDING_TERMS_VERSION_STALE` on the off-ramp). Re-read the preview and accept again."
                  }
                }
              },
              "examples": {
                "brl_pix_onramp": {
                  "summary": "BRL / Pix on-ramp into a stablecoin wallet",
                  "value": {
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "quote_id": "ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "source": {
                      "type": "fiat_payment",
                      "currency": "BRL",
                      "rail": "pix"
                    },
                    "destination": {
                      "type": "wallet",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "DEGRADED SUCCESS - the transfer was dispatched, but finalizing the record afterwards failed (projection persist, owner binding, or serialization). The provider MAY have acted, so nothing is rolled back: the transfer is downgraded to `needs_review` and this reduced body is what you get. It is NOT the full Transfer resource - `source`, `destination`, `amounts` and `quote_id` are ABSENT, and a client that assumes the `201` shape here will read `undefined` on the one response that means \"money may have moved\".\n\nBranch on the status code, not on `res.ok`. On a `200`, treat the transfer as UNKNOWN and reconcile with `GET /transfers/{id}` - do not retry the create, and do not present it to the payer as failed. A retry with the same `Idempotency-Key` replays this same body.\n",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "id",
                    "object",
                    "status"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "The transfer id - use it to reconcile."
                    },
                    "object": {
                      "type": "string",
                      "const": "transfer"
                    },
                    "status": {
                      "type": "string",
                      "const": "needs_review"
                    }
                  }
                },
                "examples": {
                  "needs_review": {
                    "summary": "Dispatched, but the record could not be finalized - reconcile with GET",
                    "value": {
                      "id": "ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "transfer",
                      "status": "needs_review"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Transfer created.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transfer"
                },
                "examples": {
                  "created": {
                    "summary": "Transfer created - poll status; source_leg/destination_leg settle independently",
                    "value": {
                      "id": "ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "transfer",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "quote_id": "ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "awaiting_payment",
                      "source": {
                        "type": "fiat_payment",
                        "currency": "BRL",
                        "rail": "pix",
                        "payment_instructions": {
                          "rail": "pix",
                          "intent_id": "6f499bc1-bf2a-49ef-a1f2-86ec5acc48d1",
                          "br_code": "00020126580014br.gov.bcb.pix...",
                          "qr_code_image": "data:image/png;base64,iVBORw0KGgo...",
                          "expires_at": "2026-07-12T18:16:00Z"
                        }
                      },
                      "destination": {
                        "type": "wallet",
                        "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
                      },
                      "source_leg": {
                        "status": "pending"
                      },
                      "destination_leg": {
                        "status": "pending"
                      },
                      "amounts": {
                        "source": {
                          "amount": "50000.00",
                          "currency": "BRL"
                        },
                        "target": {
                          "amount": "9405.000000",
                          "currency": "USDT"
                        },
                        "rate": "0.1881"
                      },
                      "exchange_conditions": {
                        "spread_bps": 40
                      },
                      "created_at": "2026-07-12T18:01:00Z",
                      "updated_at": "2026-07-12T18:01:00Z"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "$ref": "#/components/responses/IdempotencyProcessing"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "description": "Well-formed but not executable. Codes: NOT_AVAILABLE (the selected non-BRL/Pix wallet or crypto leg is not available yet), TARGET_CURRENCY_NOT_AVAILABLE, UNSUPPORTED_COMBINATION (the source/destination pair is not defined), RAIL_NOT_AVAILABLE (no settlement-ready coverage for the requested rail/corridor), QUOTE_EXPIRED (request a new quote), CUSTOMER_NOT_ENABLED (the payout destination is not yet enabled for transfers), PROVISIONING_REQUIRED (the customer or destination is not yet provisioned at the settlement layer), FACILITY_TERMS_VERSION_REQUIRED (the optional FX rate lock was accepted without `facility_terms_version`), and FACILITY_LOCK_UNAVAILABLE (the optional FX rate lock was refused by account state; nothing was created, and `details.reason` names the refusal - branch on it, not on the message).\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                },
                "examples": {
                  "quote_expired": {
                    "summary": "The referenced quote is no longer valid",
                    "value": {
                      "error": {
                        "type": "invalid_request_error",
                        "code": "QUOTE_EXPIRED",
                        "message": "The quote has expired. Request a new quote before creating the transfer.",
                        "request_id": "req_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/transfers \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"quote_id\": \"ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"source\": {\n      \"type\": \"fiat_payment\",\n      \"currency\": \"BRL\",\n      \"rail\": \"pix\"\n    },\n    \"destination\": {\n      \"type\": \"wallet\",\n      \"wallet_id\": \"ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1\"\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/transfers\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"quote_id\": \"ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"source\": {\n      \"type\": \"fiat_payment\",\n      \"currency\": \"BRL\",\n      \"rail\": \"pix\"\n    },\n    \"destination\": {\n      \"type\": \"wallet\",\n      \"wallet_id\": \"ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1\"\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/transfers\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n        \"quote_id\": \"ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n        \"source\": {\n            \"type\": \"fiat_payment\",\n            \"currency\": \"BRL\",\n            \"rail\": \"pix\",\n        },\n        \"destination\": {\n            \"type\": \"wallet\",\n            \"wallet_id\": \"ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listTransfers",
        "tags": [
          "Transfers"
        ],
        "summary": "List transfers",
        "description": "Lists transfers, newest first, with the standard cursor pagination.\nFilter by `customer_id` and/or `status` (a `TransferStatus` value).\nReturns a `Page` of `Transfer`. BRL/Pix paths create real transfer\nrecords; combinations whose settlement rails are not enabled still fail\nclosed before a transfer is persisted.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "transfers:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "customer_id",
            "in": "query",
            "description": "Filter to a single customer by its opaque `ozav_cus_` id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter transfers by their current `TransferStatus`.",
            "schema": {
              "$ref": "#/components/schemas/TransferStatus"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated transfers.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Transfer"
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "page": {
                    "summary": "One page of transfers",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "object": "transfer",
                          "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "quote_id": "ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "status": "completed",
                          "source": {
                            "type": "fiat_payment",
                            "currency": "BRL",
                            "rail": "pix"
                          },
                          "destination": {
                            "type": "payout_destination",
                            "payout_destination_id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
                          },
                          "source_leg": {
                            "status": "completed",
                            "settled_at": "2026-07-12T18:06:00Z"
                          },
                          "destination_leg": {
                            "status": "completed",
                            "settled_at": "2026-07-12T18:06:30Z"
                          },
                          "amounts": {
                            "source": {
                              "amount": "50000.00",
                              "currency": "BRL"
                            },
                            "target": {
                              "amount": "9405.00",
                              "currency": "USD"
                            },
                            "rate": "0.1881"
                          },
                          "created_at": "2026-07-12T18:01:00Z",
                          "updated_at": "2026-07-12T18:06:30Z",
                          "completed_at": "2026-07-12T18:06:30Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/transfers \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/transfers\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/transfers\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/transfers/{transfer_id}": {
      "parameters": [
        {
          "name": "transfer_id",
          "in": "path",
          "required": true,
          "description": "The opaque transfer id (`ozav_tr_...`).",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getTransfer",
        "tags": [
          "Transfers"
        ],
        "summary": "Retrieve a transfer",
        "description": "Retrieves a transfer by its `ozav_tr_` id, including its `status` and the\nindependent `source_leg` / `destination_leg` settlement states. Poll this\nto track a transfer to completion. BRL/Pix paths create real\ntransfer records; other combinations may remain gated.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "transfers:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The transfer with per-leg settlement status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transfer"
                },
                "examples": {
                  "in_flight": {
                    "summary": "Payment confirmed, payout not yet initiated",
                    "value": {
                      "id": "ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "transfer",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "quote_id": "ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "payment_confirmed",
                      "source": {
                        "type": "fiat_payment",
                        "currency": "BRL",
                        "rail": "pix"
                      },
                      "destination": {
                        "type": "payout_destination",
                        "payout_destination_id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
                      },
                      "source_leg": {
                        "status": "completed"
                      },
                      "destination_leg": {
                        "status": "pending"
                      },
                      "amounts": {
                        "source": {
                          "amount": "50000.00",
                          "currency": "BRL"
                        },
                        "target": {
                          "amount": "9405.00",
                          "currency": "USD"
                        },
                        "rate": "0.1881"
                      },
                      "created_at": "2026-07-12T18:01:00Z",
                      "updated_at": "2026-07-12T18:03:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "patch": {
        "operationId": "updateTransfer",
        "tags": [
          "Transfers"
        ],
        "summary": "Update mutable transfer attributes (metadata)",
        "description": "Updates the mutable attributes of a transfer - currently just\n`metadata`. Does not alter money movement. Returns the updated\n`Transfer`.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "transfers:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "metadata": {
                    "$ref": "#/components/schemas/Metadata"
                  }
                }
              },
              "examples": {
                "metadata": {
                  "summary": "Attach your own reference",
                  "value": {
                    "metadata": {
                      "invoice": "INV-2026-0042"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated transfer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transfer"
                },
                "examples": {
                  "metadata_attached": {
                    "summary": "Metadata attached to the transfer",
                    "value": {
                      "id": "ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "transfer",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "quote_id": "ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "completed",
                      "source": {
                        "type": "fiat_payment",
                        "currency": "BRL",
                        "rail": "pix"
                      },
                      "destination": {
                        "type": "payout_destination",
                        "payout_destination_id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
                      },
                      "amounts": {
                        "source": {
                          "amount": "50000.00",
                          "currency": "BRL"
                        },
                        "target": {
                          "amount": "9405.00",
                          "currency": "USD"
                        },
                        "rate": "0.1881"
                      },
                      "created_at": "2026-07-12T18:01:00Z",
                      "updated_at": "2026-07-12T18:07:00Z",
                      "metadata": {
                        "invoice": "INV-2026-0042"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -X PATCH \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"metadata\": {\n      \"invoice\": \"INV-2026-0042\"\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"PATCH\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"metadata\": {\n      \"invoice\": \"INV-2026-0042\"\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.patch(\n    \"https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"metadata\": {\n            \"invoice\": \"INV-2026-0042\",\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/transfers/{transfer_id}/documents": {
      "post": {
        "operationId": "uploadTransferDocument",
        "tags": [
          "Transfers"
        ],
        "summary": "Attach the supporting document (invoice) for a payout",
        "description": "Attaches the commercial supporting document for a payout transfer: in\npractice the invoice the payment settles. The receiving institution\nvalidates the purpose code against this document, so a payout can be\naccepted and then sit unsettled while it is missing.\n\nOne document per transfer. Upload it before the payout is dispatched:\nthe document is forwarded to the receiving institution only after the\npayment order exists, so `provider_status` is `null` here and is not a\npromise that it has been forwarded.\n\nThe bytes travel inline as base64 in a single call, unlike customer\ndocuments, which use a two-call signed upload. A payout document is one\nsmall file per operation, and a single call makes the result\nindivisible, with no window where the document is registered and its\nbytes are absent.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "transfers:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "transfer_id",
            "in": "path",
            "required": true,
            "description": "The opaque transfer id (`ozav_tr_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutDocumentUpload"
              },
              "examples": {
                "invoice_pdf": {
                  "summary": "The invoice the payout settles",
                  "value": {
                    "file_name": "invoice-2026-08.pdf",
                    "content_type": "application/pdf",
                    "content_base64": "JVBERi0xLjQKJeLjz9MK"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Document stored and linked to the payout.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutDocument"
                },
                "examples": {
                  "stored": {
                    "summary": "Stored, not yet forwarded",
                    "value": {
                      "id": "3f1a2b4c-5d6e-4f70-8a91-b2c3d4e5f607",
                      "object": "payout_document",
                      "transfer_id": "ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "file_name": "invoice-2026-08.pdf",
                      "content_type": "application/pdf",
                      "size_bytes": 20
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "This transfer already has a supporting document."
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"file_name\": \"invoice-2026-08.pdf\",\n    \"content_type\": \"application/pdf\",\n    \"content_base64\": \"JVBERi0xLjQKJeLjz9MK\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"file_name\": \"invoice-2026-08.pdf\",\n    \"content_type\": \"application/pdf\",\n    \"content_base64\": \"JVBERi0xLjQKJeLjz9MK\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"file_name\": \"invoice-2026-08.pdf\",\n        \"content_type\": \"application/pdf\",\n        \"content_base64\": \"JVBERi0xLjQKJeLjz9MK\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/transfers/{transfer_id}/travel-rule": {
      "post": {
        "operationId": "submitTravelRule",
        "tags": [
          "Transfers"
        ],
        "summary": "Submit travel-rule data for a transfer",
        "description": "Attaches originator/beneficiary travel-rule information to a transfer\nwhose destination is an external crypto address. Required by policy above\na server-enforced amount threshold; it may also be supplied inline on\n`POST /transfers`. Returns the updated `Transfer`. Crypto-address\ntransfer legs remain gated until enabled.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "transfers:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "transfer_id",
            "in": "path",
            "required": true,
            "description": "The opaque transfer id (`ozav_tr_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TravelRule"
              },
              "examples": {
                "originator_beneficiary": {
                  "summary": "Originator + beneficiary for a crypto send",
                  "value": {
                    "originator": {
                      "name": "Acme Ltda",
                      "account": "0xA97b1f0c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70"
                    },
                    "beneficiary": {
                      "name": "Jane Roe",
                      "account": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Travel-rule data accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transfer"
                },
                "examples": {
                  "accepted": {
                    "summary": "Travel-rule data recorded on the transfer",
                    "value": {
                      "id": "ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "transfer",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "awaiting_payment",
                      "source": {
                        "type": "fiat_payment",
                        "currency": "BRL",
                        "rail": "pix"
                      },
                      "destination": {
                        "type": "payout_destination",
                        "payout_destination_id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
                      },
                      "amounts": {
                        "source": {
                          "amount": "50000.00",
                          "currency": "BRL"
                        },
                        "target": {
                          "amount": "9405.00",
                          "currency": "USD"
                        },
                        "rate": "0.1881"
                      },
                      "travel_rule": {
                        "originator": {
                          "name": "Acme Ltda",
                          "account": "0xA97b1f0c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70"
                        },
                        "beneficiary": {
                          "name": "Jane Roe",
                          "account": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
                        }
                      },
                      "created_at": "2026-07-12T18:01:00Z",
                      "updated_at": "2026-07-12T18:02:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/travel-rule \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"originator\": {\n      \"name\": \"Acme Ltda\",\n      \"account\": \"0xA97b1f0c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70\"\n    },\n    \"beneficiary\": {\n      \"name\": \"Jane Roe\",\n      \"account\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\"\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/travel-rule\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"originator\": {\n      \"name\": \"Acme Ltda\",\n      \"account\": \"0xA97b1f0c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70\"\n    },\n    \"beneficiary\": {\n      \"name\": \"Jane Roe\",\n      \"account\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\"\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/travel-rule\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"originator\": {\n            \"name\": \"Acme Ltda\",\n            \"account\": \"0xA97b1f0c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70\",\n        },\n        \"beneficiary\": {\n            \"name\": \"Jane Roe\",\n            \"account\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\",\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/transfers/{transfer_id}/simulate": {
      "post": {
        "operationId": "simulateTransferPayment",
        "tags": [
          "Transfers"
        ],
        "summary": "Confirm the pay-in for a sandbox transfer",
        "description": "Sandbox affordance. Confirms the incoming payment on a transfer that is\nsitting in `awaiting_payment`, so you can drive the state machine past\nthe one step no test key can reach on its own: in `test` there is no\nreal payer and no provider callback, so an on-ramp would otherwise wait\nin `awaiting_payment` until it expires.\n\nThe transfer advances through BOTH legs, in two steps, exactly as a real\nconfirmed payment would: the pay-in leg settles (`payment_confirmed`,\nemitting `transfer.updated`), then the destination leg settles and the\ntransfer reaches a terminal `completed`, emitting `transfer.completed`.\nBoth events are delivered and both appear on `GET /events`, in that\norder - the response body carries the terminal state. The wallet balance\non `GET /wallets/{wallet_id}/balances` moves with it.\n\n**Until 8 September 2026 this route advanced only the pay-in leg**, and\nthis paragraph described that as the design. It was not: an on-ramp\nstopped at `payment_confirmed` with `destination_leg: pending` and\nnothing ever finished it. Measured in production on that date, none of\nthe 54 sandbox on-ramps ever left that state. If you built a flow around\nthe old wording, the change to expect is that a simulated on-ramp is now\nterminal and fires a second webhook.\n\nOne case still stops at `payment_confirmed`, by design: a transfer whose\nopening leg was settled by a REAL rail rather than the sandbox executor.\nFabricating a completion there would claim money arrived and was\ndelivered when neither happened.\n\n**Only on-ramp and fx transfers can be simulated**, because they are the\nonly ones with an incoming payment to confirm. An off-ramp opens on its\npayout leg and a wallet transfer is a custody move, so neither has a\npayer to simulate; both are refused with `409\nTRANSFER_NOT_AWAITING_PAYMENT`, and so is a transfer that has already\nmoved past `awaiting_payment`. Create a new transfer to test another\noutcome.\n\n**Test keys only.** A live key is refused with `403 SANDBOX_ONLY`; the\nenvironment comes from the key prefix (`ozav_sk_test_` vs\n`ozav_sk_live_`), never from the URL.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "transfers:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "transfer_id",
            "in": "path",
            "required": true,
            "description": "The opaque transfer id (`ozav_tr_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The pay-in was confirmed and the transfer advanced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Transfer"
                },
                "examples": {
                  "confirmed": {
                    "summary": "Sandbox on-ramp advanced to completed",
                    "value": {
                      "id": "ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "transfer",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "completed",
                      "source": {
                        "type": "fiat_payment",
                        "currency": "BRL",
                        "rail": "pix"
                      },
                      "destination": {
                        "type": "wallet",
                        "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
                      },
                      "amounts": {
                        "source": {
                          "amount": "500.00",
                          "currency": "BRL"
                        },
                        "target": {
                          "amount": "89.55",
                          "currency": "USDC"
                        },
                        "rate": "0.1791"
                      },
                      "source_leg": {
                        "status": "completed",
                        "settled_at": "2026-07-12T18:03:00Z"
                      },
                      "destination_leg": {
                        "status": "completed",
                        "settled_at": "2026-07-12T18:03:00Z"
                      },
                      "created_at": "2026-07-12T18:01:00Z",
                      "updated_at": "2026-07-12T18:03:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/simulate \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/simulate\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/simulate\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/accounts": {
      "post": {
        "operationId": "createAccount",
        "tags": [
          "Accounts"
        ],
        "summary": "Request a customer's named fiat account",
        "description": "Creates or returns a customer's named fiat receiving account through one\nprovider-neutral OZAV resource. Use the same endpoint for BRL/Pix today\nand for future USD, EUR, GBP, MXN, COP, or other account rails as they\nare enabled.\n\nThe customer must already be approved. In sandbox, BRL/BR/pix\nmaterializes a simulated account with deterministic Pix and\nbank-account instructions; no real bank account is opened and no real\nfunds can move. Every corridor OZAV serves uses this same\ncontract, and a corridor OZAV cannot currently open answers a NAMED\nerror rather than a partial account, so your integration learns the\noutcome from the response instead of from a status label. The public\nresponse never exposes the banking venue behind the account.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "accounts:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountCreateRequest"
              },
              "examples": {
                "brl_pix": {
                  "summary": "BRL/Pix named account",
                  "value": {
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "account_type": "named_fiat_account",
                    "currency": "BRL",
                    "country": "BR",
                    "rail": "pix",
                    "metadata": {
                      "label": "primary"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "An account already exists for this customer and was returned idempotently.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "201": {
            "description": "Account created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                },
                "examples": {
                  "sandbox_active": {
                    "summary": "Sandbox account",
                    "value": {
                      "id": "ozav_acc_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "account",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "account_type": "named_fiat_account",
                      "currency": "BRL",
                      "country": "BR",
                      "rail": "pix",
                      "status": "active",
                      "holder": {
                        "name": "Ana Ramos",
                        "type": "individual"
                      },
                      "receiving_instructions": {
                        "pix": {
                          "key": "sandbox+01j8z9k2m3n4p5q6r7s8t9u0@developers.ozav.io",
                          "key_type": "email"
                        },
                        "bank_account": {
                          "branch": "0001",
                          "account_number": "9123456789-5",
                          "account_type": "payment"
                        }
                      },
                      "capabilities": {
                        "receive": true,
                        "send": false
                      },
                      "balance": null,
                      "metadata": {
                        "label": "primary"
                      },
                      "created_at": "2026-07-22T12:00:00Z",
                      "updated_at": "2026-07-22T12:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/accounts \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"account_type\": \"named_fiat_account\",\n    \"currency\": \"BRL\",\n    \"country\": \"BR\",\n    \"rail\": \"pix\",\n    \"metadata\": {\n      \"label\": \"primary\"\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/accounts\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"account_type\": \"named_fiat_account\",\n    \"currency\": \"BRL\",\n    \"country\": \"BR\",\n    \"rail\": \"pix\",\n    \"metadata\": {\n      \"label\": \"primary\"\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/accounts\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n        \"account_type\": \"named_fiat_account\",\n        \"currency\": \"BRL\",\n        \"country\": \"BR\",\n        \"rail\": \"pix\",\n        \"metadata\": {\n            \"label\": \"primary\",\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listAccounts",
        "tags": [
          "Accounts"
        ],
        "summary": "List named accounts",
        "description": "Lists named fiat accounts with standard cursor pagination. Filter with\n`customer_id`, `currency`, `country`, `rail`, or `status` to recover the\naccount created after sandbox KYC approval or live opening. Returns\nonly accounts visible in the current key environment.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "accounts:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "customer_id",
            "in": "query",
            "description": "Filter to a single customer by its opaque `ozav_cus_` id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "currency",
            "in": "query",
            "description": "Filter by account currency.",
            "schema": {
              "$ref": "#/components/schemas/Currency"
            }
          },
          {
            "name": "country",
            "in": "query",
            "description": "Filter by account country or region code.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Z]{2}$"
            }
          },
          {
            "name": "rail",
            "in": "query",
            "description": "Filter by receiving rail.",
            "schema": {
              "$ref": "#/components/schemas/Rail"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by public account status.",
            "schema": {
              "$ref": "#/components/schemas/AccountStatus"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated accounts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountPage"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/accounts \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/accounts\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/accounts\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/accounts/{account_id}": {
      "get": {
        "operationId": "getAccount",
        "tags": [
          "Accounts"
        ],
        "summary": "Retrieve a named account",
        "description": "Retrieves a named fiat account by its opaque `ozav_acc_` id. The\naccount must belong to the caller and match the current key environment.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "accounts:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "account_id",
            "in": "path",
            "required": true,
            "description": "The opaque account id (`ozav_acc_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/accounts/{account_id} \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/accounts/{account_id}\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/accounts/{account_id}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/accounts/{account_id}/movements": {
      "get": {
        "operationId": "listAccountMovements",
        "tags": [
          "Accounts"
        ],
        "summary": "List an account's statement",
        "description": "Lists the credits that arrived in a named fiat account, newest first, as the\naccount's institution reports them. The statement is read live at request time,\nnot from an OZAV copy, so it can lag the institution by a few seconds and it is\nnever older than the account's `balance.as_of`.\n\nEach item is a credit into the account. `reference` is the text the payer sent\nwith the transfer, when the rail carries one; `received` is present only when the\ninstitution reports the amount that actually landed separately from the amount\nthe payer sent (international wires lose correspondent charges on the way).\n\n`501 ACCOUNT_MOVEMENTS_NOT_AVAILABLE` means this account's institution does not\nexpose a statement through OZAV (sandbox accounts included); `503\nACCOUNT_MOVEMENTS_UNAVAILABLE` means it does, and the read failed - retry.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "accounts:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "account_id",
            "in": "path",
            "required": true,
            "description": "The opaque account id (`ozav_acc_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The account's credits, newest first. `has_more` is always `false` today - the full page the institution returns is served.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "data",
                    "has_more",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AccountMovement"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "9f2c1d",
                      "object": "account_movement",
                      "direction": "credit",
                      "amount": "965.00",
                      "currency": "USD",
                      "occurred_at": "2026-09-05T10:00:00Z",
                      "reference": "PAYMENT REF INVOICE 77",
                      "payer": {
                        "name": "ACME LLC"
                      },
                      "received": {
                        "amount": "930.00",
                        "currency": "USD"
                      }
                    }
                  ],
                  "has_more": false,
                  "next_cursor": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "501": {
            "$ref": "#/components/responses/NotImplementedYet"
          },
          "503": {
            "$ref": "#/components/responses/DependencyUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/accounts/{account_id}/movements \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/accounts/{account_id}/movements\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/accounts/{account_id}/movements\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/accounts/{account_id}/deactivate": {
      "post": {
        "operationId": "deactivateAccount",
        "tags": [
          "Accounts"
        ],
        "summary": "Deactivate a sandbox account",
        "description": "Suspends a sandbox BRL/Pix account so you can test inactive-account\nhandling. Live account lifecycle changes remain gated until the\noperating procedure for your organization is enabled.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "accounts:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "account_id",
            "in": "path",
            "required": true,
            "description": "The opaque account id (`ozav_acc_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Account deactivated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/accounts/{account_id}/deactivate \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/accounts/{account_id}/deactivate\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/accounts/{account_id}/deactivate\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/accounts/{account_id}/reactivate": {
      "post": {
        "operationId": "reactivateAccount",
        "tags": [
          "Accounts"
        ],
        "summary": "Reactivate a sandbox account",
        "description": "Reactivates a sandbox BRL/Pix account after a deactivate test. Live\naccount lifecycle changes remain gated until the operating procedure\nfor your organization is enabled.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "accounts:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "account_id",
            "in": "path",
            "required": true,
            "description": "The opaque account id (`ozav_acc_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Account reactivated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/accounts/{account_id}/reactivate \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/accounts/{account_id}/reactivate\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/accounts/{account_id}/reactivate\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/accounts/{account_id}/payouts": {
      "post": {
        "operationId": "createAccountPayout",
        "tags": [
          "Accounts"
        ],
        "summary": "Pay out from a named account to a payout destination",
        "description": "Sends fiat from the customer's own named account to one of that\ncustomer's registered payout destinations, in the account's currency.\nNo conversion happens on this endpoint: the destination must be in the\nsame currency as the account (use `POST /transfers` to convert first).\n\nThe payment leaves **the customer's account**, with the customer as the\nsender of record. It is refused, with a named code, when the account\ncannot send yet (`422 ACCOUNT_NOT_READY`), when the customer profile\nlacks fields the payment rail requires\n(`422 ACCOUNT_PAYOUT_SENDER_INCOMPLETE`), when the destination belongs to\na different customer (`422 PAYOUT_DESTINATION_NOT_OWNED`), or when the\namount is above the per-payout limit for your organization\n(`422 ACCOUNT_PAYOUT_ABOVE_LIMIT`). Payouts from named accounts are\nenabled per organization: until yours is, the endpoint answers\n`403 ACCOUNT_PAYOUT_NOT_ENABLED`.\n\n**`reference` is the idempotency lever.** Repeating a request with the\nsame `reference` and `amount` for the same account returns the existing\npayout with `200` instead of sending a second payment. It also travels\nto the receiving bank as the remittance information.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "accounts:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "account_id",
            "in": "path",
            "required": true,
            "description": "The opaque account id (`ozav_acc_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountPayoutCreateRequest"
              },
              "examples": {
                "usdToOwnBank": {
                  "summary": "Pay USD from the customer's account to their registered bank",
                  "value": {
                    "amount": "50.00",
                    "destination": {
                      "type": "payout_destination",
                      "payout_destination_id": "ozav_pd_01J00000000000000000000000"
                    },
                    "reference": "INV 0042"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The same payout already existed (same `reference` and `amount`); nothing was sent again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountPayout"
                }
              }
            }
          },
          "201": {
            "description": "Payout accepted by the payment institution and recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountPayout"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/accounts/{account_id}/payouts \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"amount\": \"50.00\",\n    \"destination\": {\n      \"type\": \"payout_destination\",\n      \"payout_destination_id\": \"ozav_pd_01J00000000000000000000000\"\n    },\n    \"reference\": \"INV 0042\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/accounts/{account_id}/payouts\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"amount\": \"50.00\",\n    \"destination\": {\n      \"type\": \"payout_destination\",\n      \"payout_destination_id\": \"ozav_pd_01J00000000000000000000000\"\n    },\n    \"reference\": \"INV 0042\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/accounts/{account_id}/payouts\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"amount\": \"50.00\",\n        \"destination\": {\n            \"type\": \"payout_destination\",\n            \"payout_destination_id\": \"ozav_pd_01J00000000000000000000000\",\n        },\n        \"reference\": \"INV 0042\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/payout-destinations": {
      "post": {
        "operationId": "createPayoutDestination",
        "tags": [
          "Payout Destinations"
        ],
        "summary": "Register a payout destination",
        "description": "One canonical resource for every payout rail. `details` is a\ndiscriminated union keyed by `rail`. BRL/Pix destinations use a Pix key\n(`details.pix_key`) and are stored as provider-neutral OZAV payout\ndestinations. Do not submit a Pix BR code here: BR codes are\npay-in instructions for a specific charge, not reusable payout\ndestinations. Rails that require external venue provisioning may still\nfail closed until enabled.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "destinations:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyOptional"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayoutDestinationCreateRequest"
              },
              "examples": {
                "pix": {
                  "summary": "BRL / PIX - a PIX key",
                  "value": {
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "currency": "BRL",
                    "rail": "pix",
                    "account_holder": {
                      "name": "Ana Ramos",
                      "type": "individual"
                    },
                    "details": {
                      "pix_key": "ana@example.com"
                    }
                  }
                },
                "ach": {
                  "summary": "USD / ACH - NOT SERVED, kept to show the refusal",
                  "value": {
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "currency": "USD",
                    "rail": "ach",
                    "account_holder": {
                      "name": "Acme Inc",
                      "type": "business"
                    },
                    "details": {
                      "account_number": "000123456789",
                      "routing_number": "026009593",
                      "account_type": "checking"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Payout destination registered.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutDestination"
                },
                "examples": {
                  "pix_active": {
                    "summary": "Registered and active",
                    "value": {
                      "id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "payout_destination",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "currency": "BRL",
                      "rail": "pix",
                      "status": "active",
                      "account_holder": {
                        "name": "Ana Ramos",
                        "type": "individual"
                      },
                      "details": {
                        "pix_key": "ana@example.com"
                      },
                      "created_at": "2026-07-12T18:05:00Z"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "$ref": "#/components/responses/IdempotencyProcessing"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "501": {
            "$ref": "#/components/responses/NotImplementedYet"
          },
          "503": {
            "$ref": "#/components/responses/DependencyUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/payout-destinations \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"currency\": \"BRL\",\n    \"rail\": \"pix\",\n    \"account_holder\": {\n      \"name\": \"Ana Ramos\",\n      \"type\": \"individual\"\n    },\n    \"details\": {\n      \"pix_key\": \"ana@example.com\"\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/payout-destinations\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"currency\": \"BRL\",\n    \"rail\": \"pix\",\n    \"account_holder\": {\n      \"name\": \"Ana Ramos\",\n      \"type\": \"individual\"\n    },\n    \"details\": {\n      \"pix_key\": \"ana@example.com\"\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/payout-destinations\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n        \"currency\": \"BRL\",\n        \"rail\": \"pix\",\n        \"account_holder\": {\n            \"name\": \"Ana Ramos\",\n            \"type\": \"individual\",\n        },\n        \"details\": {\n            \"pix_key\": \"ana@example.com\",\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listPayoutDestinations",
        "tags": [
          "Payout Destinations"
        ],
        "summary": "List payout destinations",
        "description": "Lists a customer's payout destinations with the standard cursor\npagination; filter with the `customer_id` query parameter. Returns a\n`Page` of `PayoutDestination`. BRL/Pix key destinations can be created\nand read; externally provisioned rails may still be gated.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "destinations:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "customer_id",
            "in": "query",
            "description": "Filter to a single customer by its opaque `ozav_cus_` id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated payout destinations.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/PayoutDestination"
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "page": {
                    "summary": "One page of payout destinations",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "object": "payout_destination",
                          "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "currency": "BRL",
                          "rail": "pix",
                          "status": "active",
                          "account_holder": {
                            "name": "Ana Ramos",
                            "type": "individual"
                          },
                          "details": {
                            "pix_key": "ana@example.com"
                          },
                          "created_at": "2026-07-12T18:05:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/payout-destinations \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/payout-destinations\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/payout-destinations\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/payout-destinations/{payout_destination_id}": {
      "parameters": [
        {
          "name": "payout_destination_id",
          "in": "path",
          "required": true,
          "description": "The opaque payout-destination id (`ozav_pd_...`).",
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getPayoutDestination",
        "tags": [
          "Payout Destinations"
        ],
        "summary": "Retrieve a payout destination",
        "description": "Retrieves a single payout destination by its `ozav_pd_` id, including its\n`status`, `rail`, and rail-specific `details`.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "destinations:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The payout destination.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutDestination"
                },
                "examples": {
                  "pix_active": {
                    "summary": "An active PIX destination",
                    "value": {
                      "id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "payout_destination",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "currency": "BRL",
                      "rail": "pix",
                      "status": "active",
                      "account_holder": {
                        "name": "Ana Ramos",
                        "type": "individual"
                      },
                      "details": {
                        "pix_key": "ana@example.com"
                      },
                      "created_at": "2026-07-12T18:05:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "patch": {
        "operationId": "updatePayoutDestination",
        "tags": [
          "Payout Destinations"
        ],
        "summary": "Update a payout destination",
        "description": "Updates a payout destination's mutable attributes (account holder,\nmetadata). Only included fields are changed. Returns the updated\n`PayoutDestination`.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "destinations:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "account_holder": {
                    "$ref": "#/components/schemas/AccountHolder"
                  },
                  "metadata": {
                    "$ref": "#/components/schemas/Metadata"
                  }
                }
              },
              "examples": {
                "holder": {
                  "summary": "Correct the account holder",
                  "value": {
                    "account_holder": {
                      "name": "Ana C. Ramos",
                      "type": "individual"
                    },
                    "metadata": {
                      "reviewed": "true"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated payout destination.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutDestination"
                },
                "examples": {
                  "holder_corrected": {
                    "summary": "Account holder corrected",
                    "value": {
                      "id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "payout_destination",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "currency": "BRL",
                      "rail": "pix",
                      "status": "active",
                      "account_holder": {
                        "name": "Ana C. Ramos",
                        "type": "individual"
                      },
                      "details": {
                        "pix_key": "ana@example.com"
                      },
                      "created_at": "2026-07-12T18:05:00Z",
                      "metadata": {
                        "reviewed": "true"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -X PATCH \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"account_holder\": {\n      \"name\": \"Ana C. Ramos\",\n      \"type\": \"individual\"\n    },\n    \"metadata\": {\n      \"reviewed\": \"true\"\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"PATCH\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"account_holder\": {\n      \"name\": \"Ana C. Ramos\",\n      \"type\": \"individual\"\n    },\n    \"metadata\": {\n      \"reviewed\": \"true\"\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.patch(\n    \"https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"account_holder\": {\n            \"name\": \"Ana C. Ramos\",\n            \"type\": \"individual\",\n        },\n        \"metadata\": {\n            \"reviewed\": \"true\",\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/payout-destinations/{payout_destination_id}/deactivate": {
      "post": {
        "operationId": "deactivatePayoutDestination",
        "tags": [
          "Payout Destinations"
        ],
        "summary": "Deactivate a payout destination",
        "description": "Deactivates a payout destination so it can no longer be used as a\ntransfer destination; existing transfers are unaffected. Idempotent -\ndeactivating an already-inactive destination is a no-op. Returns the\n`PayoutDestination` with `status: inactive`. Reverse it with the\nreactivate endpoint.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "destinations:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "payout_destination_id",
            "in": "path",
            "required": true,
            "description": "The opaque payout-destination id (`ozav_pd_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deactivated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutDestination"
                },
                "examples": {
                  "inactive": {
                    "summary": "The destination is now inactive",
                    "value": {
                      "id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "payout_destination",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "currency": "BRL",
                      "rail": "pix",
                      "status": "inactive",
                      "account_holder": {
                        "name": "Ana Ramos",
                        "type": "individual"
                      },
                      "details": {
                        "pix_key": "ana@example.com"
                      },
                      "created_at": "2026-07-12T18:05:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivate \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivate\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivate\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/payout-destinations/{payout_destination_id}/reactivate": {
      "post": {
        "operationId": "reactivatePayoutDestination",
        "tags": [
          "Payout Destinations"
        ],
        "summary": "Reactivate a payout destination",
        "description": "Reactivates a previously deactivated payout destination, making it usable\nagain as a transfer destination. Idempotent. Returns the\n`PayoutDestination` with `status: active`.\n",
        "x-ozav-entitlement": "liquidity",
        "x-ozav-scopes": [
          "destinations:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "payout_destination_id",
            "in": "path",
            "required": true,
            "description": "The opaque payout-destination id (`ozav_pd_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reactivated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PayoutDestination"
                },
                "examples": {
                  "active_again": {
                    "summary": "The destination is active again",
                    "value": {
                      "id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "payout_destination",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "currency": "BRL",
                      "rail": "pix",
                      "status": "active",
                      "account_holder": {
                        "name": "Ana Ramos",
                        "type": "individual"
                      },
                      "details": {
                        "pix_key": "ana@example.com"
                      },
                      "created_at": "2026-07-12T18:05:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1/reactivate \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1/reactivate\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1/reactivate\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/cards": {
      "post": {
        "operationId": "createCard",
        "tags": [
          "Cards"
        ],
        "summary": "Issue a virtual card",
        "description": "Issues an OZAV virtual card for a customer who is eligible for one.\nEligibility is TWO conditions, not one: `verification.status` is\n`approved` AND the customer's account is active. Approval alone is not\nenough, and a customer who never opened an account never becomes\neligible. Missing either one returns `422\nCUSTOMER_NOT_CARD_ELIGIBLE`. `Idempotency-Key` is required. Sandbox keys create a simulated active card end-to-end;\nlive keys require the organization's live card program to be enabled.\n",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyRequired"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CardCreateRequest"
              },
              "examples": {
                "virtual": {
                  "value": {
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "type": "virtual",
                    "funding_type": "prepaid_balance"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Card issued.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                },
                "examples": {
                  "card": {
                    "value": {
                      "id": "ozav_card_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "card",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "type": "virtual",
                      "status": "active",
                      "funding_type": "prepaid_balance",
                      "last4": "4242",
                      "exp_month": 7,
                      "exp_year": 2029,
                      "brand_name": "OZAV",
                      "created_at": "2026-07-12T18:00:00Z",
                      "activated_at": "2026-07-12T18:00:00Z",
                      "cancelled_at": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"type\": \"virtual\",\n    \"funding_type\": \"prepaid_balance\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"type\": \"virtual\",\n    \"funding_type\": \"prepaid_balance\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/cards\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n        \"type\": \"virtual\",\n        \"funding_type\": \"prepaid_balance\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listCards",
        "tags": [
          "Cards"
        ],
        "summary": "List cards",
        "description": "Lists cards with standard cursor pagination; filter by `customer_id` or `status`.",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "customer_id",
            "in": "query",
            "description": "Filter to a single customer by its opaque `ozav_cus_` id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "active",
                "frozen",
                "cancelled",
                "expired"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated cards.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Card"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/cards\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/cards/{card_id}": {
      "parameters": [
        {
          "name": "card_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Opaque card id, prefixed `ozav_card_`."
        }
      ],
      "get": {
        "operationId": "getCard",
        "tags": [
          "Cards"
        ],
        "summary": "Retrieve a card",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards/{card_id} \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards/{card_id}\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/cards/{card_id}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "patch": {
        "operationId": "updateCard",
        "tags": [
          "Cards"
        ],
        "summary": "Update a card's spending controls",
        "description": "Replaces the card's spending controls without re-issuing the card. The PAN, expiry and card id are unchanged, so recurring subscriptions on the card keep working.\n\n**This replaces `spending_controls` in full; it does not merge.** Send the complete object: anything you omit is cleared. Merging is deliberately not offered because with nested lists (`spending_limits`, `blocked_categories`) it is ambiguous whether sending a list adds to the stored one or replaces it, and both readings are defensible. The request body is strict, so an unrecognised key returns `400` naming it rather than being silently dropped.\n\nCard state (`freeze`, `unfreeze`, `close`) is **not** settable here: those have their own routes, with state-transition guards and issuer ordering. A closed card returns `422`.",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "spending_controls"
                ],
                "properties": {
                  "spending_controls": {
                    "$ref": "#/components/schemas/CardSpendingControls"
                  }
                }
              },
              "examples": {
                "trocar_o_limite": {
                  "summary": "Change the monthly limit WITHOUT re-issuing the card",
                  "value": {
                    "spending_controls": {
                      "spending_limits": [
                        {
                          "amount": 500000,
                          "interval": "monthly"
                        }
                      ]
                    }
                  }
                },
                "limpar_os_controles": {
                  "summary": "Remove every control - this REPLACES, it does not merge",
                  "value": {
                    "spending_controls": {}
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards/{card_id} \\\n  -X PATCH \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"spending_controls\": {\n      \"spending_limits\": [\n        {\n          \"amount\": 500000,\n          \"interval\": \"monthly\"\n        }\n      ]\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards/{card_id}\", {\n  method: \"PATCH\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"spending_controls\": {\n      \"spending_limits\": [\n        {\n          \"amount\": 500000,\n          \"interval\": \"monthly\"\n        }\n      ]\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.patch(\n    \"https://developers.ozav.io/v2/cards/{card_id}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"spending_controls\": {\n            \"spending_limits\": [\n                {\n                    \"amount\": 500000,\n                    \"interval\": \"monthly\",\n                },\n            ],\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/cards/{card_id}/freeze": {
      "post": {
        "operationId": "freezeCard",
        "tags": [
          "Cards"
        ],
        "summary": "Freeze a card",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "card_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards/{card_id}/freeze \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards/{card_id}/freeze\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/cards/{card_id}/freeze\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/cards/{card_id}/unfreeze": {
      "post": {
        "operationId": "unfreezeCard",
        "tags": [
          "Cards"
        ],
        "summary": "Unfreeze a card",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "card_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards/{card_id}/unfreeze \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards/{card_id}/unfreeze\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/cards/{card_id}/unfreeze\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/cards/{card_id}/close": {
      "post": {
        "operationId": "closeCard",
        "tags": [
          "Cards"
        ],
        "summary": "Close a card",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "card_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated card.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Card"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards/{card_id}/close \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards/{card_id}/close\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/cards/{card_id}/close\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/cards/{card_id}/secure-display-session": {
      "post": {
        "operationId": "createCardSecureDisplaySession",
        "tags": [
          "Cards"
        ],
        "summary": "Create a secure card display session",
        "description": "Returns a short-lived OZAV-hosted URL for sensitive card display. PAN, CVV, and PIN are never returned by the API.",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "card_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "purpose": {
                    "type": "string",
                    "enum": [
                      "card_display"
                    ],
                    "default": "card_display"
                  }
                }
              },
              "examples": {
                "abrir_sessao": {
                  "summary": "Open a session so the cardholder can see the full PAN",
                  "value": {
                    "purpose": "card_display"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Secure display session.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CardDisplaySession"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards/{card_id}/secure-display-session \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"purpose\": \"card_display\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards/{card_id}/secure-display-session\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"purpose\": \"card_display\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/cards/{card_id}/secure-display-session\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"purpose\": \"card_display\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/cards/{card_id}/simulate/funding": {
      "post": {
        "operationId": "simulateCardFunding",
        "tags": [
          "Cards"
        ],
        "summary": "Load the prepaid balance (sandbox only)",
        "description": "Credits the card holder's prepaid balance so you can exercise authorisation, limits and\ndeclines. **Sandbox only** - a `live` key gets `404 SIMULATION_NOT_AVAILABLE`, because in\nproduction the balance arrives through settlement, never through a client call.\n\nWithout this, a prepaid card is created `active` and declines every purchase with\n`Insufficient balance` forever, so authorisation, spending limits, declines and interchange\ncould not be exercised at all.\n\nThe credit lands on the same balance the authorisation debits, and the amount is yours to\nchoose - so **declining for insufficient balance stays testable**: fund a small amount and\nsimulate a purchase above it.\n\nThe balance belongs to the **customer**, not to the card: funding here also funds that\ncustomer's other cards.\n",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "card_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "amount_usd"
                ],
                "properties": {
                  "amount_usd": {
                    "type": "number",
                    "exclusiveMinimum": 0,
                    "description": "How much to credit, in USD. Must be positive."
                  }
                }
              },
              "example": {
                "amount_usd": 50
              },
              "examples": {
                "creditar": {
                  "summary": "Credit the customer's balance so a card can be used",
                  "value": {
                    "amount_usd": 250
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Balance credited.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "object",
                    "card_id",
                    "amount_usd",
                    "balance_after_usd"
                  ],
                  "properties": {
                    "object": {
                      "type": "string",
                      "description": "Always `card_funding_simulation`."
                    },
                    "card_id": {
                      "type": "string",
                      "description": "The `ozav_card_` id that was funded."
                    },
                    "amount_usd": {
                      "type": "number",
                      "description": "The amount credited."
                    },
                    "balance_after_usd": {
                      "type": "number",
                      "description": "The holder's balance after the credit, read back from the ledger."
                    }
                  }
                },
                "example": {
                  "object": "card_funding_simulation",
                  "card_id": "ozav_card_01J8XY0000000000000000000",
                  "amount_usd": 50,
                  "balance_after_usd": 50
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "503": {
            "$ref": "#/components/responses/DependencyUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards/{card_id}/simulate/funding \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"amount_usd\": 250\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards/{card_id}/simulate/funding\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"amount_usd\": 250\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/cards/{card_id}/simulate/funding\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"amount_usd\": 250,\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/cards/{card_id}/simulate/purchase": {
      "post": {
        "operationId": "simulateCardPurchase",
        "tags": [
          "Cards"
        ],
        "summary": "Simulate a card purchase (sandbox only)",
        "description": "Runs a purchase through the REAL authorisation decision and settles it if approved. A card purchase is initiated by a merchant, so unlike a transfer there is no client call to hang a simulator off; this route is that trigger. Available ONLY to test-environment API keys: a live key receives 404 SIMULATION_NOT_AVAILABLE. The decision is not fabricated, so a card with a configured spending limit is genuinely declined here, which is the only way to test that your limits work before they matter.",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "card_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amount_usd"
                ],
                "properties": {
                  "amount_usd": {
                    "type": "number",
                    "description": "Purchase amount in USD. Must be positive.",
                    "example": 49.9
                  },
                  "merchant": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "example": "Cafe Lisboa"
                      },
                      "category_code": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "MCC.",
                        "example": "5812"
                      },
                      "country": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "example": "PT"
                      }
                    }
                  },
                  "is_card_present": {
                    "type": "boolean",
                    "default": false
                  },
                  "is_refund": {
                    "type": "boolean",
                    "default": false,
                    "description": "Settles as a credit. A refund earns no interchange."
                  },
                  "presentment": {
                    "type": "object",
                    "description": "Simulate an INTERNATIONAL purchase: what it cost in the merchant's currency. Omit it to simulate a domestic one.\n\nSend the amount in the currency's MINOR units, as the card networks do - `5000` with `JPY` is five thousand yen, not fifty. You do not send the number of decimal places: it comes from the same ISO 4217 table the issuer adapter uses, so sandbox and production agree on what `5000 JPY` is worth.\n\nThe settled transaction then carries `presentment` on `GET /cards/{card_id}/transactions`, which is what lets you build and test your international-transaction handling before a real issuer is feeding the programme.",
                    "required": [
                      "amount_minor",
                      "currency"
                    ],
                    "properties": {
                      "amount_minor": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "Minor units of `currency`. Non-negative integer.",
                        "example": 5000
                      },
                      "currency": {
                        "type": "string",
                        "description": "ISO 4217 code of the merchant's currency.",
                        "example": "JPY"
                      }
                    }
                  }
                }
              },
              "examples": {
                "internacional": {
                  "summary": "An INTERNATIONAL purchase (JPY merchant, USD card)",
                  "value": {
                    "amount_usd": 33.12,
                    "merchant": {
                      "name": "Ramen Ichiban",
                      "category_code": "5812",
                      "country": "JP"
                    },
                    "presentment": {
                      "amount_minor": 5000,
                      "currency": "JPY"
                    }
                  }
                },
                "domestica": {
                  "summary": "A domestic purchase - omit `presentment`",
                  "value": {
                    "amount_usd": 49.9,
                    "merchant": {
                      "name": "Cafe Lisboa",
                      "category_code": "5812",
                      "country": "PT"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The decision, and the settled transaction reference when approved. A declined purchase settles nothing and returns a null transaction_ref.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "example": "card_purchase_simulation"
                    },
                    "card_id": {
                      "type": "string"
                    },
                    "approved": {
                      "type": "boolean"
                    },
                    "decline_reason": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "authorization_ref": {
                      "type": "string",
                      "example": "sim_auth_9k2m..."
                    },
                    "transaction_ref": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "example": "sim_txn_4b8x..."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards/{card_id}/simulate/purchase \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"amount_usd\": 33.12,\n    \"merchant\": {\n      \"name\": \"Ramen Ichiban\",\n      \"category_code\": \"5812\",\n      \"country\": \"JP\"\n    },\n    \"presentment\": {\n      \"amount_minor\": 5000,\n      \"currency\": \"JPY\"\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards/{card_id}/simulate/purchase\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"amount_usd\": 33.12,\n    \"merchant\": {\n      \"name\": \"Ramen Ichiban\",\n      \"category_code\": \"5812\",\n      \"country\": \"JP\"\n    },\n    \"presentment\": {\n      \"amount_minor\": 5000,\n      \"currency\": \"JPY\"\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/cards/{card_id}/simulate/purchase\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"amount_usd\": 33.12,\n        \"merchant\": {\n            \"name\": \"Ramen Ichiban\",\n            \"category_code\": \"5812\",\n            \"country\": \"JP\",\n        },\n        \"presentment\": {\n            \"amount_minor\": 5000,\n            \"currency\": \"JPY\",\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/cards/{card_id}/transactions": {
      "get": {
        "operationId": "listCardTransactions",
        "tags": [
          "Cards"
        ],
        "summary": "List card transactions",
        "description": "Settled card movement - money that actually cleared. The sandbox response is an empty list until simulated card activity is added.\n\n**This is how you reconcile card spend: there is no per-transaction webhook.** The card events (`card.created`, `card.updated`) are lifecycle only; nothing fires when a card is used. Poll this route (and `/authorizations` for holds that have not settled yet).\n\nTo tell an international purchase from a domestic one, compare `presentment.currency` with `currency` - not `merchant.country`, which answers a different question.",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "card_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Card transactions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CardActivityPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards/{card_id}/transactions \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards/{card_id}/transactions\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/cards/{card_id}/transactions\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/cards/{card_id}/authorizations": {
      "get": {
        "operationId": "listCardAuthorizations",
        "tags": [
          "Cards"
        ],
        "summary": "List card authorizations",
        "description": "Lists pending card authorizations. The sandbox response is an empty list until simulated authorization activity is added.",
        "x-ozav-entitlement": "cards",
        "x-ozav-scopes": [
          "cards:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "card_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Card authorizations.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CardActivityPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/cards/{card_id}/authorizations \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/cards/{card_id}/authorizations\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/cards/{card_id}/authorizations\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets": {
      "post": {
        "operationId": "registerSelfCustodyWallet",
        "tags": [
          "Wallets"
        ],
        "summary": "Register a self-custody wallet the customer already controls",
        "description": "Records the wallet address your customer already controls, so OZAV can\nshow its balances and build unsigned transactions for it.\n\n**This does not create a wallet.** In self-custody there is nothing to\ncreate: your customer holds the key and the address already exists. OZAV\nrecords it. That is also why `custody_model` accepts only `self_custody`\nhere - a `managed` wallet is provisioned as part of the customer, never\nrequested through this route.\n\nUntil now the only way to register an address was the `self_custody`\nblock on `POST /customers`, so a customer that already existed had no\npath at all.\n\n**Recording is not proof of control.** OZAV checks that the address has\nthe SHAPE of an address on the declared network, which catches the real\nand frequent mistake of pasting an address from another chain. Proving\nthat your customer holds the key would require a signed challenge, which\nthis route does not do.\n\nOne address per customer: registering again REPLACES the previous one,\nand the response says so in `replaced_previous_address`. It is never\nsilent - a customer who believes they still have two wallets could send\nfunds to the old one.\n",
        "x-ozav-entitlement": "self_custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "customer_id",
                  "custody_model",
                  "chain",
                  "address"
                ],
                "properties": {
                  "customer_id": {
                    "type": "string",
                    "description": "The customer this wallet belongs to (`ozav_cus_...`)."
                  },
                  "custody_model": {
                    "type": "string",
                    "const": "self_custody",
                    "description": "Only `self_custody`. A managed wallet is not requested here."
                  },
                  "chain": {
                    "type": "string",
                    "description": "The network your customer's address lives on.\n\n**`tron` and `stellar` are refused with `422 CHAIN_HAS_NO_SELF_CUSTODY_CAPABILITY`.** They stay in this enum because they are still valid networks elsewhere in the API (managed custody serves both), but no endpoint can move a *self-custody* wallet on them today: `POST /transfers` only accepts a managed source, and `POST /wallets/{wallet_id}/transaction-intents` builds `{to, data, value, chain_id}`, which is an EVM transaction shape. Registering one would hand you a wallet with no way out, so the refusal is deliberate - use a managed-custody wallet on those networks instead.\n\n`solana` **is** accepted even though it has no transaction-intents support: a self-custody Solana wallet is served by the yield line, so it is not a dead end.",
                    "enum": [
                      "ethereum",
                      "base",
                      "polygon",
                      "arbitrum",
                      "optimism",
                      "avalanche_c_chain",
                      "solana",
                      "tron",
                      "stellar"
                    ]
                  },
                  "address": {
                    "type": "string",
                    "description": "The address your customer controls. Sent back exactly as supplied - OZAV does not normalise case, because on EVM the mixed case IS the checksum that catches a mistyped digit."
                  }
                }
              },
              "examples": {
                "evm": {
                  "summary": "Register a Base address",
                  "value": {
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "custody_model": "self_custody",
                    "chain": "base",
                    "address": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The recorded self-custody wallet.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "object",
                    "customer_id",
                    "custody_model",
                    "network",
                    "address",
                    "replaced_previous_address"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Opaque wallet identifier, prefixed `ozav_wal_`. This is the id to pass to `GET /wallets/{wallet_id}/balances` and every other wallet route. It is the SAME id `GET /wallets` returns for this wallet, so you do not need to list back after creating. Passing a `ozav_cus_` id to a wallet route answers `404 WALLET_NOT_FOUND`."
                    },
                    "object": {
                      "type": "string",
                      "const": "wallet"
                    },
                    "customer_id": {
                      "type": "string"
                    },
                    "custody_model": {
                      "type": "string",
                      "const": "self_custody"
                    },
                    "network": {
                      "type": "string"
                    },
                    "address": {
                      "type": "string"
                    },
                    "replaced_previous_address": {
                      "type": "boolean",
                      "description": "`true` when a DIFFERENT address was on file before. Stop using the old one. Re-registering the same address is idempotent and reports `false`."
                    }
                  }
                },
                "examples": {
                  "recorded": {
                    "summary": "First address recorded for this customer",
                    "value": {
                      "id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0W2",
                      "object": "wallet",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "custody_model": "self_custody",
                      "network": "base",
                      "address": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                      "replaced_previous_address": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "Well-formed but not recordable. `ADDRESS_NOT_VALID_FOR_CHAIN` - the address does not have the shape of an address on the declared network, which is almost always an address pasted from another chain.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DependencyUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"custody_model\": \"self_custody\",\n    \"chain\": \"base\",\n    \"address\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    \"custody_model\": \"self_custody\",\n    \"chain\": \"base\",\n    \"address\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"customer_id\": \"ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n        \"custody_model\": \"self_custody\",\n        \"chain\": \"base\",\n        \"address\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listWallets",
        "tags": [
          "Wallets"
        ],
        "summary": "List wallets",
        "description": "Lists wallets with the standard cursor pagination; filter with the\n`customer_id` query parameter. Each wallet reports its `custody_model`.\nReads are tenant-scoped. Returns a `Page` of `Wallet`.\nSandbox verification simulation creates a simulated managed wallet\nprojection for approved customers when no wallet or self-custody address\nalready exists. Live managed wallet provisioning, on-chain addresses,\nand managed sends remain gated. Use `GET /accounts?customer_id=...`\nfor customer BRL/Pix account instructions.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          },
          {
            "name": "customer_id",
            "in": "query",
            "description": "Filter to a single customer by its opaque `ozav_cus_` id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated wallets.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Wallet"
                          }
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "page": {
                    "summary": "One page of wallets",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "object": "wallet",
                          "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "custody_model": "managed",
                          "status": "active",
                          "created_at": "2026-07-12T18:00:00Z"
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}": {
      "get": {
        "operationId": "getWallet",
        "tags": [
          "Wallets"
        ],
        "summary": "Retrieve a wallet",
        "description": "Retrieves a single wallet by its `ozav_wal_` id, including its\n`custody_model` and `status`. A wallet outside your organization returns\n`404`, never a leak.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The wallet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Wallet"
                },
                "examples": {
                  "managed": {
                    "summary": "A managed wallet",
                    "value": {
                      "id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "wallet",
                      "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "custody_model": "managed",
                      "status": "active",
                      "created_at": "2026-07-12T18:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/deposit-addresses": {
      "get": {
        "operationId": "listWalletDepositAddresses",
        "tags": [
          "Wallets"
        ],
        "summary": "List the deposit addresses of a wallet",
        "description": "The on-chain addresses to send funds to so they credit this wallet. Tenant-scoped: a `wallet_id` outside the caller's tenant answers 404 and never leaks existence.\n\nRead `memo_required` before you build a deposit instruction. Some networks attribute a deposit by MEMO rather than by address: on those, every customer shares one account and the memo is the only thing that routes the funds to the right one. A deposit sent without it, or with the wrong one, cannot be attributed. Where `memo_required` is true the `memo` field is always present; where it is false, `memo` is null and must not be sent.\n\nA wallet in self-custody returns an empty `data` array: the funds are the end user's, on their own address, and OZAV has no address to offer.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One deposit address per supported network.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DepositAddress"
                      }
                    }
                  }
                },
                "examples": {
                  "deposit_addresses": {
                    "summary": "An address-per-user network and a memo-based one",
                    "value": {
                      "data": [
                        {
                          "object": "deposit_address",
                          "network": "ethereum",
                          "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
                          "memo": null,
                          "memo_required": false,
                          "created_at": "2026-08-12T10:00:00Z"
                        },
                        {
                          "object": "deposit_address",
                          "network": "stellar",
                          "address": "GBTUMXPEAPKTFIZAI47GKGUWMHWUONVX6PXGBPLSCC6WM3MYDPHQ3VSL",
                          "memo": "mA1b2C3d4E5f6G7h8I9j0K1l2M3n",
                          "memo_required": true,
                          "created_at": "2026-08-12T10:00:00Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deposit-addresses \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deposit-addresses\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deposit-addresses\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/balances": {
      "get": {
        "operationId": "listWalletBalances",
        "tags": [
          "Wallets"
        ],
        "summary": "List balances of a wallet",
        "description": "This read is wired and tenant-scoped, and returns an empty `data` array until custody accounts exist - no balances are projected yet. Tenant-scoped read. The handler MUST verify the wallet belongs to the authenticated client and return 404 (never leak existence) for a `wallet_id` outside the caller's tenant - a valid `wallets:read` scope alone does NOT grant cross-tenant balance reads. This ownership check is the load-bearing control and MUST live in the auth kernel's request scope, not be left as a per-handler implementation detail. NOTE (entitlement precision): the persona matrix grants balances to neobank + liquidity (settlement wallet) + custody, but NOT compliance - which the single-value `x-ozav-entitlement` cannot express (`custody` would drop liquidity; `any` admits compliance). Left `any` because the `wallets:read` scope grant plus the ownership check already bound access; resolve to a proper entitlement SET once the auth model supports one.\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Balances by currency (and network where applicable).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Balance"
                      }
                    }
                  }
                },
                "examples": {
                  "balances": {
                    "summary": "Balances by currency and network",
                    "value": {
                      "data": [
                        {
                          "amount": "1500.000000",
                          "currency": "USDC",
                          "network": "base"
                        },
                        {
                          "amount": "250.500000",
                          "currency": "USDT",
                          "network": "tron"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/balances \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/balances\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/balances\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/onchain-balances": {
      "get": {
        "operationId": "listWalletOnchainBalances",
        "tags": [
          "Wallets"
        ],
        "summary": "List on-chain balances of a wallet, per network",
        "description": "What is AT THE ADDRESS, on each network, right now. This is NOT the ledger balance returned by `/balances`, and the two do not add up to the same number - read both descriptions before reconciling. `/balances` reports the AVAILABLE ledger balance (posted credits minus posted debits minus pending debits) per CURRENCY, with no network, because the custody ledger account is per asset and shared across chains. This route reports the raw on-chain balance of each deposit address: it does not subtract in-flight outbound, does not wait for finality, and DOES include a deposit that arrived but has not been credited yet. Use it to answer \"how much can I move out on network X\" and to see uncredited inbound; use `/balances` to reconcile against OZAV's books. TWO RULES DECIDE WHICH NETWORKS APPEAR, and both omit rather than report a zero. FIRST, ONLY THE NETWORKS OFFERED TO YOUR ORGANIZATION APPEAR. That set is the proven-withdrawal ceiling intersected with your own configuration, and it is the SAME set `/deposit-addresses` serves, so the two routes always agree on which networks exist for you. `x-ozav-network-availability` on the `Network` schema is the machine-readable source: `available` means every client, `opt_in` means per-client on request, `deposit_paused` means nobody. A network outside your set is absent from this response even when an address was provisioned on it in the past. Nothing is hidden by that: a deposit arriving at such an address is still watched and credited, so it appears in `/balances`, the ledger route. What is absent here is the raw on-chain view of a network you are not offered, never the balance itself. SECOND, NETWORKS WITH A SHARED DEPOSIT ADDRESS ARE OMITTED. On networks that attribute by memo, every customer shares one account, so the on-chain balance of that address belongs to all customers at once and is not attributable to the caller - reporting it would show other customers' funds. `/deposit-addresses` omits the same networks, for a related but distinct reason (an address without its memo is an unusable instruction). `amount` is `null` when the balance could NOT BE READ (an unreachable node, a network with no configured endpoint) - never `0`. \"Unreadable\" and \"empty\" call for opposite actions, and a fabricated zero is indistinguishable from an empty wallet. When `amount` is `null`, `unreadable_reason` says what happened, and the other networks in the same response are unaffected. Tenant-scoped read, identical control to `/balances`: the handler verifies the wallet belongs to the authenticated client and returns 404 (never leak existence) for a `wallet_id` outside the caller's tenant. A valid `wallets:read` scope alone does NOT grant cross-tenant reads. For a self-custody wallet OZAV holds no addresses at all, so the response is an empty `data` array.\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "On-chain balances, one entry per (network, currency).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OnchainBalance"
                      }
                    }
                  }
                },
                "examples": {
                  "onchain_balances": {
                    "summary": "Two networks read, one unreadable",
                    "value": {
                      "data": [
                        {
                          "object": "onchain_balance",
                          "network": "tron",
                          "currency": "USDT",
                          "address": "TLkDwwyqdTXjaUnCKTTYJzxtKMh8R492EF",
                          "amount": "5.750742"
                        },
                        {
                          "object": "onchain_balance",
                          "network": "ethereum",
                          "currency": "USDT",
                          "address": "0x761c6FdE080a2A386834527a0Eb4eDbDc7f6B378",
                          "amount": "2.2"
                        },
                        {
                          "object": "onchain_balance",
                          "network": "polygon",
                          "currency": "USDC",
                          "address": "0x761c6FdE080a2A386834527a0Eb4eDbDc7f6B378",
                          "amount": null,
                          "unreadable_reason": "leitura de saldo passou de 4000ms"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/onchain-balances \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/onchain-balances\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/onchain-balances\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/transaction-intents": {
      "post": {
        "operationId": "createTransactionIntent",
        "tags": [
          "Transaction Intents"
        ],
        "summary": "Build an unsigned transaction for a self-custody wallet",
        "description": "For `self_custody` wallets only. The server builds the calldata and\nreturns an **unsigned** transaction; signing and broadcasting are done\nby the client (or their end-user's wallet). **OZAV never holds keys for\nself-custody wallets, and never signs this transaction.**\n\nThe token contract comes from OZAV's verified registry, and the amount is\nconverted to the token's smallest unit without floating point. Anything\nthat cannot be built exactly is REFUSED (`422`) rather than approximated:\na network whose transaction format is not `{to, data, value, chain_id}`,\nan asset not verified on that network, or an amount smaller than one unit\nof the token (which would build a transaction that moves nothing).\n\nRequires a wallet whose `custody_model` is `self_custody`; a managed\nwallet answers `422 WALLET_NOT_SELF_CUSTODY`, because there is no client\nkey to sign with.\n",
        "x-ozav-entitlement": "self_custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "intent"
                ],
                "properties": {
                  "intent": {
                    "type": "string",
                    "description": "The transaction to build - a SEND from this wallet:\n- `withdraw` - send funds out to an external destination.\n- `transfer` - move funds to another address.\n- `deposit` - rejected: inbound deposits are signed by the external payer, not built here.\n",
                    "enum": [
                      "deposit",
                      "withdraw",
                      "transfer"
                    ]
                  },
                  "currency": {
                    "$ref": "#/components/schemas/Currency"
                  },
                  "amount": {
                    "$ref": "#/components/schemas/DecimalAmount"
                  },
                  "destination": {
                    "type": "string",
                    "description": "The recipient address the send goes to (must be valid for the wallet's chain)."
                  }
                }
              },
              "examples": {
                "transfer": {
                  "summary": "Build an unsigned USDC transfer",
                  "value": {
                    "intent": "transfer",
                    "currency": "USDC",
                    "amount": "250.000000",
                    "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Unsigned transaction payload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransactionIntent"
                },
                "examples": {
                  "unsigned": {
                    "summary": "An unsigned USDC transfer for the client to sign",
                    "value": {
                      "id": "ozav_txi_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "transaction_intent",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "intent": "transfer",
                      "unsigned_transaction": {
                        "to": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                        "data": "0xa9059cbb0000000000000000000000001d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8000000000000000000000000000000000000000000000000000000000ee6b280",
                        "value": "0",
                        "chain_id": 8453
                      },
                      "expires_at": "2026-07-12T18:05:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transaction-intents \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"intent\": \"transfer\",\n    \"currency\": \"USDC\",\n    \"amount\": \"250.000000\",\n    \"destination\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transaction-intents\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"intent\": \"transfer\",\n    \"currency\": \"USDC\",\n    \"amount\": \"250.000000\",\n    \"destination\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transaction-intents\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"intent\": \"transfer\",\n        \"currency\": \"USDC\",\n        \"amount\": \"250.000000\",\n        \"destination\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/approvals": {
      "get": {
        "operationId": "listApprovals",
        "tags": [
          "Approvals"
        ],
        "summary": "List approvals",
        "description": "The decisions your approver keys owe, newest first. Filter with\n`?status=pending` to build an approval inbox. A `pending` approval past\n`expires_at` reads as `expired` here, and the held operation is released\non that first read.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "approvals:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "approved",
                "rejected",
                "expired",
                "executed",
                "failed"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of approvals (without per-key decisions; read one for those).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "has_more",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Approval"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "next_cursor": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  }
                },
                "examples": {
                  "inbox": {
                    "summary": "One send waiting for a second approval",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_apr_01J8Z9K2M3N4P5Q6R7S8T9U0V3",
                          "object": "approval",
                          "kind": "wallet_transfer",
                          "status": "pending",
                          "resource": {
                            "type": "wallet_transfer",
                            "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V2"
                          },
                          "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "required_approvals": 2,
                          "approvals_received": 0,
                          "summary": {
                            "amount": "2500.000000",
                            "currency": "USDC",
                            "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                            "network": "polygon"
                          },
                          "decisions": [],
                          "requested_by_api_key_id": "ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V9",
                          "failure_reason": null,
                          "expires_at": "2026-09-12T12:00:00Z",
                          "created_at": "2026-09-09T12:00:00Z",
                          "decided_at": null,
                          "executed_at": null
                        }
                      ],
                      "has_more": false,
                      "next_cursor": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/approvals \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/approvals\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/approvals\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/approvals/{approval_id}": {
      "get": {
        "operationId": "getApproval",
        "tags": [
          "Approvals"
        ],
        "summary": "Retrieve an approval",
        "description": "One approval with every decision recorded so far. Tenant- and environment-scoped; an id from another tenant or environment answers 404.",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "approvals:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "approval_id",
            "in": "path",
            "required": true,
            "description": "The opaque approval id (`ozav_apr_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The approval.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Approval"
                },
                "examples": {
                  "one_of_two": {
                    "summary": "One approval received, one still owed",
                    "value": {
                      "id": "ozav_apr_01J8Z9K2M3N4P5Q6R7S8T9U0V3",
                      "object": "approval",
                      "kind": "wallet_transfer",
                      "status": "pending",
                      "resource": {
                        "type": "wallet_transfer",
                        "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V2"
                      },
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "required_approvals": 2,
                      "approvals_received": 1,
                      "summary": {
                        "amount": "2500.000000",
                        "currency": "USDC",
                        "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                        "network": "polygon"
                      },
                      "decisions": [
                        {
                          "api_key_id": "ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0VA",
                          "decision": "approve",
                          "reason": null,
                          "decided_at": "2026-09-09T12:10:00Z"
                        }
                      ],
                      "requested_by_api_key_id": "ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V9",
                      "failure_reason": null,
                      "expires_at": "2026-09-12T12:00:00Z",
                      "created_at": "2026-09-09T12:00:00Z",
                      "decided_at": null,
                      "executed_at": null
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/approvals/{approval_id} \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/approvals/{approval_id}\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/approvals/{approval_id}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/approvals/{approval_id}/approve": {
      "post": {
        "operationId": "approveApproval",
        "tags": [
          "Approvals"
        ],
        "summary": "Approve",
        "description": "Record this key's approval. Rules, all enforced: the key must hold\n`approvals:write`; it must not be the key that requested the operation\n(`403 APPROVER_IS_REQUESTER`); it decides once (`409\nAPPROVER_ALREADY_DECIDED`); a decided or expired approval does not move\n(`409 APPROVAL_NOT_PENDING` / `APPROVAL_EXPIRED`).\n\nWhen this approval is the last one required, **the held operation\nexecutes in this same request**: a `POST /wallets/{id}/transfers` send is\nsigned and broadcast, a `POST /transfers` send moves to `converting`, a\nquorum loosening is applied. The response then carries `status:\nexecuted` (or `failed`, with `failure_reason`, when execution was refused\n- nothing moved) and a `result` object with the executed resource.\nMint one API key per approver; a quorum of one key is not a quorum.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "approvals:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "approval_id",
            "in": "path",
            "required": true,
            "description": "The opaque approval id (`ozav_apr_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApprovalDecisionRequest"
              },
              "examples": {
                "plain": {
                  "summary": "Approve without a note",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The approval after this decision; with `result` when it executed.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Approval"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "result": {
                          "type": "object",
                          "additionalProperties": true,
                          "description": "Present when this decision executed the operation - the resulting `wallet_transfer`, `transfer` or `wallet_policy_change`."
                        }
                      }
                    }
                  ]
                },
                "examples": {
                  "executed": {
                    "summary": "The second approval executed the send",
                    "value": {
                      "id": "ozav_apr_01J8Z9K2M3N4P5Q6R7S8T9U0V3",
                      "object": "approval",
                      "kind": "wallet_transfer",
                      "status": "executed",
                      "resource": {
                        "type": "wallet_transfer",
                        "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V2"
                      },
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "required_approvals": 2,
                      "approvals_received": 2,
                      "summary": {
                        "amount": "2500.000000",
                        "currency": "USDC",
                        "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                        "network": "polygon"
                      },
                      "decisions": [
                        {
                          "api_key_id": "ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0VA",
                          "decision": "approve",
                          "reason": null,
                          "decided_at": "2026-09-09T12:10:00Z"
                        },
                        {
                          "api_key_id": "ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0VB",
                          "decision": "approve",
                          "reason": null,
                          "decided_at": "2026-09-09T12:20:00Z"
                        }
                      ],
                      "requested_by_api_key_id": "ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V9",
                      "failure_reason": null,
                      "expires_at": "2026-09-12T12:00:00Z",
                      "created_at": "2026-09-09T12:00:00Z",
                      "decided_at": "2026-09-09T12:20:00Z",
                      "executed_at": "2026-09-09T12:20:00Z",
                      "result": {
                        "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                        "object": "wallet_transfer",
                        "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                        "status": "completed",
                        "amount": "2500.000000",
                        "currency": "USDC",
                        "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                        "transaction_hash": "0x6c984fe16f15aa9865e7ca950c5038542e5789c8bd6dfc2fd95f649514a88b47",
                        "settled_at": "2026-09-09T12:20:03Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`APPROVER_ALREADY_DECIDED`, `APPROVAL_NOT_PENDING` or `APPROVAL_EXPIRED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/approvals/{approval_id}/approve \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{}'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/approvals/{approval_id}/approve\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({}),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/approvals/{approval_id}/approve\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={},\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/approvals/{approval_id}/reject": {
      "post": {
        "operationId": "rejectApproval",
        "tags": [
          "Approvals"
        ],
        "summary": "Reject",
        "description": "Record this key's rejection. One rejection is final: the approval becomes\n`rejected`, the held send is released (a `POST /transfers` send reads\n`cancelled`; a `POST /wallets/{id}/transfers` send reads `rejected`),\n`transfer.rejected` is emitted with your `reason`, and nothing moves.\nSame key rules as approve.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "approvals:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "approval_id",
            "in": "path",
            "required": true,
            "description": "The opaque approval id (`ozav_apr_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApprovalDecisionRequest"
              },
              "examples": {
                "with_reason": {
                  "summary": "Reject, saying why",
                  "value": {
                    "reason": "Destination not in the vendor list"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The approval, now `rejected`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Approval"
                },
                "examples": {
                  "rejected": {
                    "summary": "Rejected by one approver",
                    "value": {
                      "id": "ozav_apr_01J8Z9K2M3N4P5Q6R7S8T9U0V3",
                      "object": "approval",
                      "kind": "wallet_transfer",
                      "status": "rejected",
                      "resource": {
                        "type": "wallet_transfer",
                        "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V2"
                      },
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "required_approvals": 2,
                      "approvals_received": 0,
                      "summary": {
                        "amount": "2500.000000",
                        "currency": "USDC",
                        "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                        "network": "polygon"
                      },
                      "decisions": [
                        {
                          "api_key_id": "ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0VA",
                          "decision": "reject",
                          "reason": "Destination not in the vendor list",
                          "decided_at": "2026-09-09T12:10:00Z"
                        }
                      ],
                      "requested_by_api_key_id": "ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V9",
                      "failure_reason": null,
                      "expires_at": "2026-09-12T12:00:00Z",
                      "created_at": "2026-09-09T12:00:00Z",
                      "decided_at": "2026-09-09T12:10:00Z",
                      "executed_at": null
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`APPROVER_ALREADY_DECIDED`, `APPROVAL_NOT_PENDING` or `APPROVAL_EXPIRED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/approvals/{approval_id}/reject \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"reason\": \"Destination not in the vendor list\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/approvals/{approval_id}/reject\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"reason\": \"Destination not in the vendor list\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/approvals/{approval_id}/reject\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"reason\": \"Destination not in the vendor list\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/addresses": {
      "get": {
        "operationId": "listWalletAddresses",
        "tags": [
          "Wallets"
        ],
        "summary": "List the address book of a wallet",
        "description": "The destinations of this managed wallet with your labels: what is\n`allowed` (leaves without human review), what you proposed and is still\n`pending_review`, and what was `rejected`. This is the same list as\n`destinations.allowed` on the policy, plus the pending proposals - one\nsource, so what you see is what the custody plane applies.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The address book.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WalletAddress"
                      }
                    }
                  }
                },
                "examples": {
                  "book": {
                    "summary": "One allowed destination and one awaiting review",
                    "value": {
                      "data": [
                        {
                          "object": "wallet_address",
                          "network": "ethereum",
                          "address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
                          "label": "cold vault",
                          "status": "allowed",
                          "added_at": "2026-08-01T00:00:00Z",
                          "requested_at": "2026-08-01T00:00:00Z"
                        },
                        {
                          "object": "wallet_address",
                          "network": "solana",
                          "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin",
                          "label": "treasury exchange",
                          "status": "pending_review",
                          "added_at": null,
                          "requested_at": "2026-09-09T12:00:00Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "post": {
        "operationId": "createWalletAddress",
        "tags": [
          "Wallets"
        ],
        "summary": "Add a destination to the address book",
        "description": "Propose a destination for the allowlist, with a label. **Nothing changes\non this call**: the entry answers `202 pending_review` and joins the same\nreview queue as `PATCH /wallets/{id}/policy`; when OZAV approves it,\n`wallet_policy.updated` fires and the entry reads `allowed`. A destination\nalready allowed answers `200` with the existing entry and proposes nothing.\nAddresses are validated for their network; EVM addresses are stored\nlower-cased. `Idempotency-Key` is required. Read-only in the sandbox\n(`422 POLICY_SANDBOX_READ_ONLY`).\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required idempotency key (16-256 characters of `[A-Za-z0-9_-]`) - the request enters a review queue and must be safely retryable.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WalletAddressCreateRequest"
              },
              "examples": {
                "labelled": {
                  "summary": "A labelled payout address on Ethereum",
                  "value": {
                    "network": "ethereum",
                    "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
                    "label": "Binance treasury"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already allowed; the existing entry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletAddress"
                }
              }
            }
          },
          "202": {
            "description": "Queued for review.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletAddress"
                },
                "examples": {
                  "queued": {
                    "summary": "Awaiting review",
                    "value": {
                      "object": "wallet_address",
                      "network": "ethereum",
                      "address": "0x742d35cc6634c0532925a3b844bc454e4438f44e",
                      "label": "Binance treasury",
                      "status": "pending_review",
                      "added_at": null,
                      "requested_at": "2026-09-09T12:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`IDEMPOTENCY_KEY_REUSED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "`DESTINATION_ADDRESS_INVALID`, `NETWORK_NOT_SUPPORTED`, `WALLET_NOT_MANAGED` or `POLICY_SANDBOX_READ_ONLY`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"network\": \"ethereum\",\n    \"address\": \"0x742d35Cc6634C0532925a3b844Bc454e4438f44e\",\n    \"label\": \"Binance treasury\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"network\": \"ethereum\",\n    \"address\": \"0x742d35Cc6634C0532925a3b844Bc454e4438f44e\",\n    \"label\": \"Binance treasury\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"network\": \"ethereum\",\n        \"address\": \"0x742d35Cc6634C0532925a3b844Bc454e4438f44e\",\n        \"label\": \"Binance treasury\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/addresses/{network}/{address}": {
      "delete": {
        "operationId": "deleteWalletAddress",
        "tags": [
          "Wallets"
        ],
        "summary": "Remove a destination from the address book",
        "description": "Take an `allowed` destination out of the allowlist **now** (`204`). Removing\nonly reduces risk, so no one has to approve it; from then on a send to that\naddress above `limits.auto_approve_below_usd` goes to review like any\nunlisted destination. `wallet_policy.updated` fires with\n`kind: destination.remove`. A `pending_review` proposal cannot be withdrawn\nhere - OZAV decides it; a destination not in the allowlist answers `404`.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "network",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/Network"
            }
          },
          {
            "name": "address",
            "in": "path",
            "required": true,
            "description": "The destination address (URL-encoded). EVM addresses match case-insensitively.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "`NETWORK_NOT_SUPPORTED` or `WALLET_NOT_MANAGED`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses/{network}/{address} \\\n  -X DELETE \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses/{network}/{address}\", {\n  method: \"DELETE\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.delete(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses/{network}/{address}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/freeze": {
      "get": {
        "operationId": "getWalletFreeze",
        "tags": [
          "Wallets"
        ],
        "summary": "Read whether a wallet is frozen",
        "description": "Whether sends from this managed wallet are paused, by whom, and since when.\nA `self_custody` wallet answers `422 WALLET_NOT_MANAGED`.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The freeze state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletFreeze"
                },
                "examples": {
                  "frozen_by_you": {
                    "summary": "Frozen by your organization",
                    "value": {
                      "object": "wallet_freeze",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "frozen": true,
                      "frozen_by": "customer",
                      "reason": "customer:ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V9: key possibly leaked",
                      "since": "2026-09-09T12:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/freeze \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/freeze\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/freeze\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "post": {
        "operationId": "freezeWallet",
        "tags": [
          "Wallets"
        ],
        "summary": "Freeze a wallet",
        "description": "Pause every send from this managed wallet **now**. From this response on,\n`POST /wallets/{id}/transfers` and `POST /transfers` from it answer\n`409 WALLET_FROZEN` before anything is signed. Deposits keep arriving.\nAny key with `wallets:write` can freeze: freezing only reduces risk, so no\none has to approve it. Freezing an already frozen wallet answers `200` with\nthe existing freeze and emits nothing; the first freeze answers `201` and\nemits `wallet.frozen`. Unfreezing is the guarded direction - see below.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WalletFreezeRequest"
              },
              "examples": {
                "suspected_leak": {
                  "summary": "Freeze with a note",
                  "value": {
                    "reason": "key possibly leaked"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already frozen; the existing freeze.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletFreeze"
                }
              }
            }
          },
          "201": {
            "description": "Frozen now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletFreeze"
                },
                "examples": {
                  "frozen": {
                    "summary": "Frozen now",
                    "value": {
                      "object": "wallet_freeze",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "frozen": true,
                      "frozen_by": "customer",
                      "reason": "customer:ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V9: key possibly leaked",
                      "since": "2026-09-09T12:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/freeze \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"reason\": \"key possibly leaked\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/freeze\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"reason\": \"key possibly leaked\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/freeze\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"reason\": \"key possibly leaked\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/unfreeze": {
      "post": {
        "operationId": "unfreezeWallet",
        "tags": [
          "Wallets"
        ],
        "summary": "Unfreeze a wallet",
        "description": "Lift the freeze **your organization** placed. Without an approval quorum on\nthe wallet it lifts at once (`200`, `wallet.unfrozen`). With\n`approvals.mode: customer_quorum` on the wallet's policy, unfreezing is a\nloosening: it answers `202` with `unfreeze_approval_id`, and the wallet stays\nfrozen until `required_approvals` approver keys approve that approval\n(`409 APPROVAL_PENDING` while one already waits). A freeze placed by OZAV\n(`frozen_by: ozav`) is not yours to lift: the call answers `200` with the\nstate unchanged - contact support.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WalletFreezeRequest"
              },
              "examples": {
                "false_alarm": {
                  "summary": "Lift with a note",
                  "value": {
                    "reason": "false alarm"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lifted (or nothing of yours to lift).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletFreeze"
                },
                "examples": {
                  "lifted": {
                    "summary": "Lifted now",
                    "value": {
                      "object": "wallet_freeze",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "frozen": false,
                      "frozen_by": null,
                      "reason": null,
                      "since": null
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Your quorum must approve the unfreeze; the wallet stays frozen meanwhile.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletFreeze"
                },
                "examples": {
                  "awaiting_quorum": {
                    "summary": "Waiting for two approver keys",
                    "value": {
                      "object": "wallet_freeze",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "frozen": true,
                      "frozen_by": "customer",
                      "reason": "customer:ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V9: key possibly leaked",
                      "since": "2026-09-09T12:00:00Z",
                      "unfreeze_approval_id": "ozav_apr_01J8Z9K2M3N4P5Q6R7S8T9U0V4"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`APPROVAL_PENDING` - an unfreeze already awaits your quorum.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/unfreeze \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"reason\": \"false alarm\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/unfreeze\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"reason\": \"false alarm\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/unfreeze\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"reason\": \"false alarm\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/policy": {
      "get": {
        "operationId": "getWalletPolicy",
        "tags": [
          "Wallets"
        ],
        "summary": "Read the policy of a managed wallet",
        "description": "What OZAV applies before signing a send from this `managed` wallet, as one\nobject: the destinations that leave without human review, the limits in\nforce and where they come from, who reviews what falls outside them, and\nthe changes you asked for that are still being decided.\n\n**Read `limits.source` before you read the numbers.** `wallet` means a\nsigned per-wallet record is in force; `organization` means no per-wallet\nrecord exists and your organization's default limits apply;\n`unconfigured` means neither exists, and **every** send from this wallet\ngoes to human review until one does.\n\nA destination outside `destinations.allowed` is not refused: a send to it\nabove `limits.auto_approve_below_usd` is parked for review (`needs_review`)\nand resumes when a reviewer accepts it. Below that amount a clean send to\nan unlisted destination leaves on its own. Tenant-scoped: a `wallet_id`\noutside the caller's tenant answers 404 and never leaks existence. A\n`self_custody` wallet has no policy here (its owner holds the key) and\nanswers `422 WALLET_NOT_MANAGED`.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The policy in force, and the changes still being decided.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletPolicy"
                },
                "examples": {
                  "policy": {
                    "summary": "A wallet with a signed limit record and one destination awaiting review",
                    "value": {
                      "object": "wallet_policy",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "destinations": {
                        "mode": "allowlist",
                        "allowed": [
                          {
                            "network": "ethereum",
                            "address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
                            "added_at": "2026-08-01T00:00:00Z"
                          },
                          {
                            "network": "tron",
                            "address": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
                            "added_at": "2026-08-02T00:00:00Z"
                          }
                        ]
                      },
                      "limits": {
                        "per_transaction_usd": 50000,
                        "daily_usd": 200000,
                        "auto_approve_below_usd": 1000,
                        "source": "wallet",
                        "valid_until": "2027-02-01T00:00:00Z"
                      },
                      "approvals": {
                        "mode": "ozav_review"
                      },
                      "pending_changes": [
                        {
                          "id": "6f1d2c3b-4a5e-4f60-9a1b-2c3d4e5f6a7b",
                          "kind": "destination.add",
                          "status": "pending_review",
                          "requested_at": "2026-09-09T12:00:00Z",
                          "decided_at": null,
                          "network": "solana",
                          "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
                        }
                      ],
                      "updated_at": "2026-08-02T00:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/policy \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/policy\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/policy\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "patch": {
        "operationId": "proposeWalletPolicyChange",
        "tags": [
          "Wallets"
        ],
        "summary": "Change the policy of a managed wallet",
        "description": "Five fields, and they do not all behave the same way. **Read this split\nbefore you read anything else** - two of the five are requests, three take\neffect on this call.\n\n**`approvals`, `delay` and `cosigner` are yours to set, and TIGHTENING\nAPPLIES IMMEDIATELY.** Requiring a second approval, holding sends for N\nhours, or putting your own co-signer in the path does not wait on OZAV:\nthe object read by `GET /wallets/{wallet_id}/policy` moves on this\nresponse. Loosening is the direction that waits - a smaller quorum goes\nthrough the quorum in force, and a shorter delay is queued and only comes\ninto force after the delay in force has elapsed. You cannot shorten a\n24-hour hold in less than 24 hours, which is the entire point of it.\n\n**`destinations` and `limits` are proposals.** They enter review by OZAV\nand nothing changes on this call; each appears at once in\n`pending_changes`, and when decided you receive `wallet_policy.updated`. A\ndestination becomes effective the moment it is approved. A limit change is\napproved first and comes into force minutes later, when its signed record\nis issued - confirm it by reading `limits.valid_until`.\n\n`operationId` still says `propose`: it was named when the endpoint only\ncarried the two proposal fields, and renaming it would break every\ngenerated client.\n\nSend at least one of `destinations` or `limits`. A destination already\nallowed, or already awaiting review, is not proposed again - the response\nstill answers `202` with the policy as it stands. Missing limit fields\ninherit the values in force; when nothing is in force yet, both\n`per_transaction_usd` and `daily_usd` are required. A limit change needs\na provisioned custody vault (`409 POLICY_LIMITS_REQUIRE_VAULT` before that);\ndestinations can be proposed at any time.\n\n`Idempotency-Key` is **required** on every call: a proposal must not be\nenqueued twice by a retry, and a queued loosening must not have its clock\nrestarted by one. In the sandbox the policy is read-only\n(`422 POLICY_SANDBOX_READ_ONLY`); use a live key.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required idempotency key (16-256 characters of `[A-Za-z0-9_-]`) - a policy change is either enqueued or applied, and both must be safely retryable.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WalletPolicyChangeRequest"
              },
              "examples": {
                "add_destination": {
                  "summary": "Allow a new payout address on Ethereum",
                  "value": {
                    "destinations": {
                      "add": [
                        {
                          "network": "ethereum",
                          "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
                        }
                      ]
                    }
                  }
                },
                "raise_daily_limit": {
                  "summary": "Raise the daily limit, keeping the other numbers in force",
                  "value": {
                    "limits": {
                      "daily_usd": 500000
                    }
                  }
                },
                "require_two_approvals": {
                  "summary": "Require a second approval above USD 10,000 - applies on this call",
                  "description": "Two of your API keys with `approvals:write` must accept before a send above the threshold leaves. Tightening applies immediately; lowering the number later goes through the quorum in force.\n",
                  "value": {
                    "approvals": {
                      "required_approvals": 2,
                      "above_usd": 10000
                    }
                  }
                },
                "hold_every_send_for_24h": {
                  "summary": "Hold sends for 24 hours before they leave - applies on this call",
                  "description": "The transfer answers `awaiting_delay` and you can stop it with `POST /wallets/{wallet_id}/transfers/{transfer_id}/cancel` at any point in the window. Shortening the delay later is queued and only comes into force after the delay in force has elapsed.\n",
                  "value": {
                    "delay": {
                      "hours": 24,
                      "above_usd": null
                    }
                  }
                },
                "add_your_own_cosigner": {
                  "summary": "Put your own service in the signing path - applies on this call",
                  "description": "OZAV calls your endpoint before signing and will not sign without your approval. `on_unavailable` is the decision that matters: `hold` means a send waits when your service is unreachable, `proceed` means it does not. The secret goes in and never comes out - `GET /policy` shows the endpoint and never the secret.\n",
                  "value": {
                    "cosigner": {
                      "url": "https://cosigner.example.com/ozav",
                      "secret": "a-shared-secret-of-at-least-16-chars",
                      "on_unavailable": "hold",
                      "timeout_ms": 3000
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The change is queued for review. The body is the policy as it stands now - the request appears in `pending_changes`; nothing in force has moved yet.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletPolicy"
                },
                "examples": {
                  "queued": {
                    "summary": "The destination is awaiting review; the allowlist in force is unchanged",
                    "value": {
                      "object": "wallet_policy",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "destinations": {
                        "mode": "allowlist",
                        "allowed": [
                          {
                            "network": "ethereum",
                            "address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
                            "added_at": "2026-08-01T00:00:00Z"
                          }
                        ]
                      },
                      "limits": {
                        "per_transaction_usd": 50000,
                        "daily_usd": 200000,
                        "auto_approve_below_usd": 1000,
                        "source": "wallet",
                        "valid_until": "2027-02-01T00:00:00Z"
                      },
                      "approvals": {
                        "mode": "ozav_review"
                      },
                      "pending_changes": [
                        {
                          "id": "7a2e3d4c-5b6f-4071-8b2c-3d4e5f6a7b8c",
                          "kind": "destination.add",
                          "status": "pending_review",
                          "requested_at": "2026-09-09T12:00:00Z",
                          "decided_at": null,
                          "network": "ethereum",
                          "address": "0x742d35cc6634c0532925a3b844bc454e4438f44e"
                        }
                      ],
                      "updated_at": "2026-08-01T00:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`POLICY_LIMITS_REQUIRE_VAULT` - the wallet has no custody vault yet, so it has no per-wallet limits to change; or `IDEMPOTENCY_KEY_REUSED`.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "`DESTINATION_ADDRESS_INVALID`, `NETWORK_NOT_SUPPORTED`, `POLICY_LIMITS_INCOMPLETE`, `POLICY_LIMITS_INVALID`, `WALLET_NOT_MANAGED` or `POLICY_SANDBOX_READ_ONLY`. `param` names the field.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/policy \\\n  -X PATCH \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"destinations\": {\n      \"add\": [\n        {\n          \"network\": \"ethereum\",\n          \"address\": \"0x742d35Cc6634C0532925a3b844Bc454e4438f44e\"\n        }\n      ]\n    }\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/policy\", {\n  method: \"PATCH\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"destinations\": {\n      \"add\": [\n        {\n          \"network\": \"ethereum\",\n          \"address\": \"0x742d35Cc6634C0532925a3b844Bc454e4438f44e\"\n        }\n      ]\n    }\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.patch(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/policy\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"destinations\": {\n            \"add\": [\n                {\n                    \"network\": \"ethereum\",\n                    \"address\": \"0x742d35Cc6634C0532925a3b844Bc454e4438f44e\",\n                },\n            ],\n        },\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/transfers": {
      "post": {
        "operationId": "createManagedTransfer",
        "tags": [
          "Wallets"
        ],
        "summary": "Send from a managed-custody wallet (OZAV signs)",
        "description": "For `managed` wallets only, where OZAV custodies the key. **OZAV signs and\nbroadcasts this send.** It is the inverse of `transaction-intents`, which\nis for `self_custody` wallets, where the client signs.\n\nThis is a real, irreversible money movement, so `Idempotency-Key` is\n**required** - a request without one is rejected rather than risking a\nsecond broadcast. Treat a timeout as UNKNOWN, never as failed: retry with\nthe SAME key, which replays the stored result instead of sending again.\n\nThe send is refused before anything is signed when the wallet's asset does\nnot match `currency`, when the destination is not valid for the wallet's\nnetwork, or when the corridor is not enabled for your organization.\nNothing is broadcast on a refusal.\n\n**The same move is also available on `POST /transfers`**, with a `wallet`\nsource and a `wallet` or `crypto_address` destination. Both routes drive\nthe same executor and apply the same two controls (managed wallets only;\nTravel Rule on any send that leaves custody), so the choice is about which\nresource you want back, not about what happens on-chain:\n\n- Use **this route** when the send needs a destination `memo`. Networks\n  that carry one (Stellar) need it for exchange deposits - an exchange\n  credits by memo, and a deposit without one arrives unattributed.\n  `POST /transfers` has no `memo` field.\n- Use **`POST /transfers`** when you want a `Transfer` resource, so the\n  send appears in `GET /transfers` alongside your on-ramp, off-ramp and\n  FX movements and is reconciled through the same status model.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required idempotency key (16-256 characters of `[A-Za-z0-9_-]`) - a managed transfer is a money movement and must be safely retryable.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "currency",
                  "amount",
                  "destination"
                ],
                "properties": {
                  "currency": {
                    "$ref": "#/components/schemas/Currency"
                  },
                  "amount": {
                    "$ref": "#/components/schemas/DecimalAmount"
                  },
                  "destination": {
                    "type": "string",
                    "description": "The recipient - a client-owned wallet id (ozav_wal_) or an on-chain address valid for the wallet's chain."
                  },
                  "token_id": {
                    "type": "string",
                    "pattern": "^ozav_tok_[0-9A-Za-z]{16,}$",
                    "description": "Send a token you registered, instead of the asset OZAV backs in this wallet. The id comes from `POST /wallets/{wallet_id}/tokens`.\n\n**Three things change, and you should plan for all three.** `currency` must be the symbol you registered, not a currency code. **Every send of a registered token needs approval from your quorum** - OZAV does not price the token, so there is no threshold to stay under, and a wallet with no approvals quorum set is refused with `CLIENT_TOKEN_REQUIRES_QUORUM`. And the token must be `active`: one still `pending_signature` is refused, because OZAV's independent co-signer does not know it yet.\n\nEverything else is the same request it always was - the same destination allowlist, the same Travel Rule, the same mandatory delay, the same idempotency.",
                    "example": "ozav_tok_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
                  },
                  "nft_collection_id": {
                    "type": "string",
                    "pattern": "^ozav_nft_[0-9A-Za-z]{16,}$",
                    "description": "Send a piece from a collection you registered, instead of a fungible asset. Comes with `nft_token_id`, and is mutually exclusive with `token_id`.\n\n**Every NFT send needs approval from your quorum** - OZAV does not price a piece, so there is no threshold to stay under, and a wallet with no approvals quorum is refused with `NFT_TRANSFER_REQUIRES_QUORUM`. The collection must be `active`; one still `pending_signature` is refused.",
                    "example": "ozav_nft_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
                  },
                  "nft_token_id": {
                    "type": "string",
                    "pattern": "^(0|[1-9][0-9]{0,77})$",
                    "description": "Which piece, as a decimal **string**. It is a string and not a number on purpose: token ids routinely exceed 2^53, and a JSON number would silently lose precision - sending a different piece than you asked for.",
                    "example": "1234567890123456789012345"
                  },
                  "nft_quantity": {
                    "type": "string",
                    "pattern": "^[1-9][0-9]{0,29}$",
                    "description": "ERC-1155 only: how many units of that id. Omitted means 1. An ERC-721 piece is indivisible, so any value other than 1 is refused.",
                    "example": "1"
                  },
                  "chain": {
                    "type": "string",
                    "description": "Optional explicit chain for the send. When present, both the source and the destination are resolved on THIS chain only, and a mismatch fails before anything is signed or broadcast. Supply it whenever the wallet holds the same asset on more than one chain - omitting it lets the executor choose.",
                    "example": "stellar"
                  },
                  "memo": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 28,
                    "description": "Destination memo/tag, for networks whose transaction format carries one (Stellar). **On a memo-based network the recipient credits by memo: a send without it arrives and is attributed to nobody.** Every major exchange deposit address on Stellar is shared across its users, so sending there without the memo the exchange gave you loses the deposit. Read `memo_required` on `GET /wallets/{wallet_id}/deposit-addresses` for the receiving side of the same rule. 28 characters is the Stellar `memo_text` ceiling; a longer value is rejected with `400` naming the field, never truncated.",
                    "example": "2importmemo9"
                  },
                  "travel_rule": {
                    "$ref": "#/components/schemas/TravelRule"
                  }
                }
              },
              "examples": {
                "send": {
                  "summary": "Send 250 USDT from a managed wallet",
                  "value": {
                    "currency": "USDT",
                    "amount": "250.000000",
                    "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
                  }
                },
                "send_memo_network": {
                  "summary": "Send on a memo-based network (Stellar) - the memo is what attributes it",
                  "value": {
                    "currency": "USDC",
                    "amount": "250.000000",
                    "chain": "stellar",
                    "destination": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
                    "memo": "2importmemo9"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The settled managed transfer.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "object": {
                      "type": "string",
                      "enum": [
                        "wallet_transfer"
                      ]
                    },
                    "wallet_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "completed"
                      ]
                    },
                    "amount": {
                      "$ref": "#/components/schemas/DecimalAmount"
                    },
                    "currency": {
                      "$ref": "#/components/schemas/Currency"
                    },
                    "destination": {
                      "type": "string"
                    },
                    "transaction_hash": {
                      "type": "string"
                    },
                    "settled_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                },
                "examples": {
                  "settled": {
                    "summary": "A settled managed USDT transfer",
                    "value": {
                      "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "wallet_transfer",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "completed",
                      "amount": "250.000000",
                      "currency": "USDT",
                      "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                      "transaction_hash": "0x6c984fe16f15aa9865e7ca950c5038542e5789c8bd6dfc2fd95f649514a88b47",
                      "settled_at": "2026-07-17T00:39:47Z"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "**Held by your approval quorum.** The wallet's policy (`approvals.mode: customer_quorum`) requires approver keys to approve this send before OZAV signs it. Nothing has been signed. `approval.id` is what an approver key (`approvals:write`, a different key from the one that sent this request) passes to `POST /approvals/{approval_id}/approve`; the last required approval executes the send and you receive `transfer.approved`. Unapproved after `approval.expires_at` (72 h), it expires and nothing moves. A retry with the same `Idempotency-Key` returns this same held transfer.\n",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "object",
                    "wallet_id",
                    "status",
                    "amount",
                    "currency",
                    "destination",
                    "approval"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "object": {
                      "type": "string",
                      "enum": [
                        "wallet_transfer"
                      ]
                    },
                    "wallet_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "awaiting_approval"
                      ]
                    },
                    "amount": {
                      "$ref": "#/components/schemas/DecimalAmount"
                    },
                    "currency": {
                      "$ref": "#/components/schemas/Currency"
                    },
                    "destination": {
                      "type": "string"
                    },
                    "transaction_hash": {
                      "type": "null"
                    },
                    "settled_at": {
                      "type": "null"
                    },
                    "approval": {
                      "type": "object",
                      "required": [
                        "id",
                        "required_approvals",
                        "expires_at"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "The `ozav_apr_` id to approve or reject."
                        },
                        "required_approvals": {
                          "type": "integer"
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "held": {
                    "summary": "Held until two approver keys approve",
                    "value": {
                      "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                      "object": "wallet_transfer",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "awaiting_approval",
                      "amount": "2500.000000",
                      "currency": "USDC",
                      "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                      "transaction_hash": null,
                      "settled_at": null,
                      "approval": {
                        "id": "ozav_apr_01J8Z9K2M3N4P5Q6R7S8T9U0V3",
                        "required_approvals": 2,
                        "expires_at": "2026-09-12T12:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfers \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"currency\": \"USDT\",\n    \"amount\": \"250.000000\",\n    \"destination\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfers\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"currency\": \"USDT\",\n    \"amount\": \"250.000000\",\n    \"destination\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfers\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"currency\": \"USDT\",\n        \"amount\": \"250.000000\",\n        \"destination\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/nft-collections": {
      "get": {
        "operationId": "listNftCollections",
        "tags": [
          "Wallets"
        ],
        "summary": "List the NFT collections registered on a managed wallet",
        "description": "Lists the ERC-721 / ERC-1155 collections this wallet has registered.\n\nEach carries a `status`, and the middle one matters most:\n\n- `pending_signature` - registered here, but OZAV's independent co-signer\n  does not know it yet, so **no transfer of a piece can be signed**.\n- `active` - the signed record exists.\n- `disabled` - switched off through this API, which is not revocation.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "coming_soon",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The wallet id (`ozav_wal_...`)."
          }
        ],
        "responses": {
          "200": {
            "description": "The registered collections.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NftCollection"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/nft-collections \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/nft-collections\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/nft-collections\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "post": {
        "operationId": "registerNftCollection",
        "tags": [
          "Wallets"
        ],
        "summary": "Register an NFT collection",
        "description": "Registers an ERC-721 or ERC-1155 contract so this wallet can hold and move\npieces from it.\n\n**Registering does not enable anything.** The record is created as\n`pending_signature`. OZAV's independent co-signer accepts a contract it\ndoes not already know only from a record signed on a machine the API\ncannot reach.\n\n**`padrao` has no default, on purpose.** It decides which function\nselector the co-signer requires - that is, how the same calldata bytes are\nread. Choosing one for you would let a 1155 collection be registered\nsilently wrong, and you would find out as a refused transfer, far from the\ncause.\n\n**What this collection is not.** OZAV does not price it, does not credit\nit to your ledger balance, and does not include it in proof of reserves.\nBecause it has no dollar value, **every transfer of a piece requires human\napproval from your quorum**, with no ceiling to configure.\n\n**Availability:** registration works end to end. The route that *moves* a\npiece does not exist yet.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "coming_soon",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The wallet id (`ozav_wal_...`)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "chain",
                  "contract",
                  "padrao",
                  "nome"
                ],
                "additionalProperties": false,
                "properties": {
                  "chain": {
                    "type": "string",
                    "description": "Chain key, e.g. `ethereum`. EVM chains only for now.",
                    "example": "ethereum"
                  },
                  "contract": {
                    "type": "string",
                    "pattern": "^0x[0-9a-fA-F]{40}$",
                    "description": "The collection contract address.",
                    "example": "0x1111111111111111111111111111111111111111"
                  },
                  "padrao": {
                    "type": "string",
                    "enum": [
                      "erc721",
                      "erc1155"
                    ],
                    "description": "The token standard. Required, with no default: it decides which selector the co-signer requires.",
                    "example": "erc721"
                  },
                  "nome": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64,
                    "description": "Display name. Read by people; it never decides anything.",
                    "example": "Acervo do Cliente"
                  }
                }
              },
              "examples": {
                "erc721": {
                  "summary": "An ERC-721 collection",
                  "value": {
                    "chain": "ethereum",
                    "contract": "0x1111111111111111111111111111111111111111",
                    "padrao": "erc721",
                    "nome": "Acervo do Cliente"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Registered as a proposal. `202` and not `201`: the collection exists but cannot be used until the signed record is issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NftCollection"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "This contract is already registered for this wallet on this chain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "The collection cannot be registered - a contract that already belongs to an asset OZAV backs is refused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/nft-collections \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"chain\": \"ethereum\",\n    \"contract\": \"0x1111111111111111111111111111111111111111\",\n    \"padrao\": \"erc721\",\n    \"nome\": \"Acervo do Cliente\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/nft-collections\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"chain\": \"ethereum\",\n    \"contract\": \"0x1111111111111111111111111111111111111111\",\n    \"padrao\": \"erc721\",\n    \"nome\": \"Acervo do Cliente\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/nft-collections\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"chain\": \"ethereum\",\n        \"contract\": \"0x1111111111111111111111111111111111111111\",\n        \"padrao\": \"erc721\",\n        \"nome\": \"Acervo do Cliente\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/nft-collections/{collection_id}/disable": {
      "post": {
        "operationId": "disableNftCollection",
        "tags": [
          "Wallets"
        ],
        "summary": "Stop accepting a registered NFT collection",
        "description": "Switches a registered collection off. New transfers from it are no longer\naccepted through this API.\n\n**This is not revocation.** The signed record OZAV's co-signer holds keeps\nits own validity window. The row is kept, not deleted, so past transfers\nthat reference this collection id stay explainable.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "coming_soon",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The wallet id (`ozav_wal_...`)."
          },
          {
            "name": "collection_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The collection id (`ozav_nft_...`)."
          }
        ],
        "responses": {
          "200": {
            "description": "The collection, now `disabled`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NftCollection"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/nft-collections/{collection_id}/disable \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/nft-collections/{collection_id}/disable\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/nft-collections/{collection_id}/disable\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/tokens": {
      "get": {
        "operationId": "listClientTokens",
        "tags": [
          "Wallets"
        ],
        "summary": "List the tokens registered on a managed wallet",
        "description": "Lists the ERC-20 tokens this wallet has registered beyond the assets OZAV\nprices and backs.\n\nEach token carries a `status`, and the middle one matters most:\n\n- `pending_signature` - registered here, but OZAV's independent co-signer\n  does not know it yet, so **no transfer of it can be signed**.\n- `active` - the signed record exists.\n- `disabled` - switched off through this API. This is not revocation: the\n  signed record keeps its own validity window.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "coming_soon",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The wallet id (`ozav_wal_...`)."
          }
        ],
        "responses": {
          "200": {
            "description": "The registered tokens.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "list"
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ClientToken"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/tokens \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/tokens\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/tokens\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "post": {
        "operationId": "registerClientToken",
        "tags": [
          "Wallets"
        ],
        "summary": "Register a token OZAV does not price or back",
        "description": "Registers an ERC-20 contract so this wallet can hold and move it.\n\n**Registering does not enable anything, and that is the point.** The\nrecord is created as `pending_signature`. OZAV's independent co-signer\naccepts a contract it does not already know only from a record signed on\na machine the API cannot reach - so until that record is issued, every\ntransfer of this token is refused.\n\n**What OZAV will not accept.** A symbol OZAV prices and backs (`USDC`,\n`USDT`, `BRS`, and the native coins) is refused; so is a contract that\nalready belongs to one of those assets, whatever symbol you give it. Both\nwould let real, priced money move under rules written for an asset with\nno price.\n\n**What this token is not.** OZAV does not price it, does not credit it to\nyour ledger balance, and does not include it in proof of reserves - the\non-chain balance is the only record. Because it has no dollar value,\n**every transfer of it requires human approval from your quorum**, with\nno ceiling to configure.\n\n**Availability:** registration works end to end. The route that *moves*\none of these tokens does not exist yet. Read `GET /capabilities` rather\nthan discovering this on your first transfer.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "coming_soon",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The wallet id (`ozav_wal_...`)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "chain",
                  "contract",
                  "symbol",
                  "decimals"
                ],
                "additionalProperties": false,
                "properties": {
                  "chain": {
                    "type": "string",
                    "description": "Chain key, e.g. `ethereum`. EVM chains only for now.",
                    "example": "ethereum"
                  },
                  "contract": {
                    "type": "string",
                    "pattern": "^0x[0-9a-fA-F]{40}$",
                    "description": "The ERC-20 contract address.",
                    "example": "0x1111111111111111111111111111111111111111"
                  },
                  "symbol": {
                    "type": "string",
                    "pattern": "^[A-Z0-9][A-Z0-9._-]{0,15}$",
                    "description": "Display symbol, uppercase. Used for reading, never to decide\nanything. A symbol OZAV prices and backs is refused.\n",
                    "example": "MEUTOKEN"
                  },
                  "decimals": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 36,
                    "description": "Decimal places. Signed together with the contract - OZAV never\nreads `decimals()` from a node at send time.\n",
                    "example": 18
                  }
                }
              },
              "examples": {
                "erc20": {
                  "summary": "An ERC-20 the client issued",
                  "value": {
                    "chain": "ethereum",
                    "contract": "0x1111111111111111111111111111111111111111",
                    "symbol": "MEUTOKEN",
                    "decimals": 18
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Registered as a proposal. `202` and not `201`: the token exists but\ncannot be used until the signed record is issued.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientToken"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "This contract is already registered for this wallet on this chain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "The token cannot be registered - a symbol OZAV prices and backs, or a\ncontract that already belongs to one of those assets.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/tokens \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"chain\": \"ethereum\",\n    \"contract\": \"0x1111111111111111111111111111111111111111\",\n    \"symbol\": \"MEUTOKEN\",\n    \"decimals\": 18\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/tokens\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"chain\": \"ethereum\",\n    \"contract\": \"0x1111111111111111111111111111111111111111\",\n    \"symbol\": \"MEUTOKEN\",\n    \"decimals\": 18\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/tokens\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"chain\": \"ethereum\",\n        \"contract\": \"0x1111111111111111111111111111111111111111\",\n        \"symbol\": \"MEUTOKEN\",\n        \"decimals\": 18,\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/tokens/{token_id}/disable": {
      "post": {
        "operationId": "disableClientToken",
        "tags": [
          "Wallets"
        ],
        "summary": "Stop accepting a registered token",
        "description": "Switches a registered token off. New transfers of it are no longer\naccepted through this API.\n\n**This is not revocation.** The signed record OZAV's co-signer holds keeps\nits own validity window; switching off here governs what this API accepts\nfrom now on. The row is kept, not deleted, so past transfers that\nreference this token id stay explainable.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "coming_soon",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The wallet id (`ozav_wal_...`)."
          },
          {
            "name": "token_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The token id (`ozav_tok_...`)."
          }
        ],
        "responses": {
          "200": {
            "description": "The token, now `disabled`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClientToken"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/tokens/{token_id}/disable \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/tokens/{token_id}/disable\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/tokens/{token_id}/disable\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/contract-calls": {
      "post": {
        "operationId": "createContractCall",
        "tags": [
          "Wallets"
        ],
        "summary": "Call a contract from a managed wallet (allowed calls only)",
        "description": "Calls a smart contract from a managed-custody wallet - but **only a call\nthat is on OZAV's signed allowed-call list, and only with the arguments\nthat list left open.** The list is a single OZAV-wide list, not a\nper-organization one.\n\n**There is no `data` field, and that is the point.** You do not build the\ncalldata; you *name* the call. OZAV looks up the signed rule for\n(`chain`, `contract`, `selector`), and assembles the calldata itself:\nevery argument the rule pinned comes from the rule, and `arguments` fills\nonly the positions the rule left free. There is no request that produces\ncalldata outside the list.\n\nA pinned argument is how \"only this vault, only `deposit`, only to me\"\nis expressed: the rule can fix an argument to an exact value, or to the\ncalling wallet's own address. You cannot reach those positions.\n\n**Two whole classes of function can never be on the list**, no matter who\nsigns it: the ones that move value on their own (`transfer`,\n`transferFrom`, the `safeTransferFrom` family) - those go through the\ntransfer route, with its destination allowlist, asset and ceiling - and\nthe ones that delegate spending power (`approve`, `increaseAllowance`,\n`setApprovalForAll`), which are accepted only with the spender pinned.\n\n`Idempotency-Key` is **required**: a contract call moves money.\n\n**Availability:** the allowed-call list starts empty, so every call is\nrefused with `CONTRACT_CALL_NOT_ALLOWED` until an entry is added. And\ndispatch is not wired yet: a fully valid request answers `503\nCONTRACT_CALL_NOT_CONFIGURED` and **nothing is signed**. Read\n`GET /capabilities` rather than discovering this on your first call.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "coming_soon",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required idempotency key (16-256 characters of `[A-Za-z0-9_-]`) - a contract call is a money movement and must be safely retryable.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "chain",
                  "contract",
                  "selector",
                  "arguments"
                ],
                "properties": {
                  "chain": {
                    "type": "string",
                    "description": "The EVM network. The rule is pinned to a chain id, not to a name: with CREATE2 the same address on another chain can be an entirely different contract.",
                    "example": "polygon"
                  },
                  "contract": {
                    "type": "string",
                    "pattern": "^0x[0-9a-fA-F]{40}$",
                    "description": "The contract to call. It must be on the signed list for this chain."
                  },
                  "selector": {
                    "type": "string",
                    "pattern": "^0x[0-9a-fA-F]{8}$",
                    "description": "The 4-byte function selector. You name the function; you do not encode the call.",
                    "example": "0x6e553f65"
                  },
                  "arguments": {
                    "type": "array",
                    "maxItems": 16,
                    "items": {
                      "type": "string",
                      "pattern": "^0x[0-9a-fA-F]{64}$"
                    },
                    "description": "One 32-byte hex word per **free** position in the rule, in the order those positions appear. Too many or too few is rejected - a missing value is never filled with zeros, because that would be OZAV choosing an argument for you."
                  }
                }
              },
              "examples": {
                "deposit_to_self": {
                  "summary": "Deposit into an allowed vault, with the receiver pinned to your own wallet",
                  "value": {
                    "chain": "polygon",
                    "contract": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                    "selector": "0x6e553f65",
                    "arguments": [
                      "0x00000000000000000000000000000000000000000000000000000000000f4240"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "503": {
            "$ref": "#/components/responses/DependencyUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/contract-calls \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"chain\": \"polygon\",\n    \"contract\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\n    \"selector\": \"0x6e553f65\",\n    \"arguments\": [\n      \"0x00000000000000000000000000000000000000000000000000000000000f4240\"\n    ]\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/contract-calls\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"chain\": \"polygon\",\n    \"contract\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\n    \"selector\": \"0x6e553f65\",\n    \"arguments\": [\n      \"0x00000000000000000000000000000000000000000000000000000000000f4240\"\n    ]\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/contract-calls\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"chain\": \"polygon\",\n        \"contract\": \"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\n        \"selector\": \"0x6e553f65\",\n        \"arguments\": [\n            \"0x00000000000000000000000000000000000000000000000000000000000f4240\",\n        ],\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/typed-messages": {
      "post": {
        "operationId": "createTypedMessage",
        "tags": [
          "Wallets"
        ],
        "summary": "Sign an EIP-712 typed message from a managed wallet (allowed messages only)",
        "description": "Signs an EIP-712 typed message with a managed-custody wallet's key - but\n**only a message that is on OZAV's signed allowed-message list, and only\nwith the fields that list left open.** The list is a single OZAV-wide\nlist, not a per-organization one.\n\n**There is no digest field, and that is the whole point.** An EIP-712\nsignature has no envelope, no `to` and no `value`: what gets signed is a\nraw 32-byte payload, and on a secp256k1 key raw bytes are\nindistinguishable from the hash of a transaction. An endpoint that\naccepted \"sign these bytes\" would be an endpoint that drains wallets\nunder another name. You *name* the message; OZAV derives the digest from\nthe signed rule, and the independent co-signer **re-derives it** and\nrefuses unless it matches what the key is actually about to sign.\n\nA pinned field is how \"only this spender, only to me\" is expressed: the\nrule can fix a field to an exact value, or to the signing wallet's own\naddress. You cannot reach those positions.\n\n**Message types that delegate spending power can never be listed with a\nfree beneficiary** - the ERC-2612 and DAI `Permit`, and all three Permit2\nforms (`PermitSingle`, `PermitBatch`, `PermitTransferFrom`). A `Permit`\nwith a free `spender` is an `approve` signed for free: no gas, no block,\nno envelope guard.\n\n`Idempotency-Key` is **required**: a signed permit delegates money.\n\n**Availability:** the allowed-message list starts empty, so every request\nis refused with `TYPED_MESSAGE_NOT_ALLOWED` until an entry is added. And\ndispatch is not wired yet: a fully valid request answers `503\nTYPED_MESSAGE_NOT_CONFIGURED` and **nothing is signed**. Read\n`GET /capabilities` rather than discovering this on your first call.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "coming_soon",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required idempotency key (16-256 characters of `[A-Za-z0-9_-]`) - a signed permit delegates spending power and must be safely retryable.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "chain",
                  "domain_separator",
                  "type_hash",
                  "values"
                ],
                "properties": {
                  "chain": {
                    "type": "string",
                    "description": "The EVM network. The rule is pinned to a chain id, not to a name.",
                    "example": "polygon"
                  },
                  "domain_separator": {
                    "type": "string",
                    "pattern": "^0x[0-9a-fA-F]{64}$",
                    "description": "The EIP-712 `domainSeparator` - `hashStruct(EIP712Domain)`, which binds name, version, chain and verifying contract. It must be on the signed list for this chain."
                  },
                  "type_hash": {
                    "type": "string",
                    "pattern": "^0x[0-9a-fA-F]{64}$",
                    "description": "The `typeHash` of the primary type. You name the message type; you do not encode the message."
                  },
                  "values": {
                    "type": "array",
                    "maxItems": 16,
                    "items": {
                      "type": "string",
                      "pattern": "^0x[0-9a-fA-F]{64}$"
                    },
                    "description": "One 32-byte hex word per **free** position in the rule, in the order those positions appear. Dynamic types (`string`, `bytes`, arrays) enter `encodeData` already hashed, so they are words too. Too many or too few is rejected - a missing value is never filled with zeros, because that would be OZAV choosing a field for you."
                  }
                }
              },
              "examples": {
                "permit_with_spender_pinned": {
                  "summary": "An ERC-2612 permit whose owner and spender the rule pinned; you supply value, nonce and deadline",
                  "value": {
                    "chain": "polygon",
                    "domain_separator": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
                    "type_hash": "0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9",
                    "values": [
                      "0x00000000000000000000000000000000000000000000000000000000000f4240"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "503": {
            "$ref": "#/components/responses/DependencyUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/typed-messages \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"chain\": \"polygon\",\n    \"domain_separator\": \"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\n    \"type_hash\": \"0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9\",\n    \"values\": [\n      \"0x00000000000000000000000000000000000000000000000000000000000f4240\"\n    ]\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/typed-messages\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"chain\": \"polygon\",\n    \"domain_separator\": \"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\n    \"type_hash\": \"0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9\",\n    \"values\": [\n      \"0x00000000000000000000000000000000000000000000000000000000000f4240\"\n    ]\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/typed-messages\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"chain\": \"polygon\",\n        \"domain_separator\": \"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\n        \"type_hash\": \"0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9\",\n        \"values\": [\n            \"0x00000000000000000000000000000000000000000000000000000000000f4240\",\n        ],\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/transfers/{transfer_id}/cancel": {
      "post": {
        "operationId": "cancelDelayedWalletTransfer",
        "tags": [
          "Wallets"
        ],
        "summary": "Cancel a transfer that is waiting on its delay",
        "description": "Stops a transfer that is `awaiting_delay` before it leaves. **This is the\nwhole point of the delay**: holding a send for N hours only helps if\nsomeone can read the statement, not recognise the withdrawal, and stop\nit.\n\nOnly a transfer waiting on the CLOCK can be cancelled here. One waiting\non your approval quorum is stopped by rejecting its approval\n(`POST /approvals/{approval_id}/reject`), where the rule about who may\nreject already lives.\n\nAnything else - already sent, already cancelled, or an id that is not\nyours - answers `409 TRANSFER_NOT_CANCELLABLE`. The four are one refusal\non purpose: telling them apart would tell you which ids exist in other\norganizations.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "transfer_id",
            "in": "path",
            "required": true,
            "description": "The transfer to cancel (`ozav_wtr_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The transfer is cancelled. Nothing was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "object": {
                      "type": "string",
                      "enum": [
                        "wallet_transfer"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "cancelled"
                      ]
                    }
                  }
                },
                "example": {
                  "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                  "object": "wallet_transfer",
                  "status": "cancelled"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`TRANSFER_NOT_CANCELLABLE` - the transfer is not waiting on a delay.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/cancel \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/cancel\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/cancel\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/transfer-batches": {
      "post": {
        "operationId": "createManagedTransferBatch",
        "tags": [
          "Wallets"
        ],
        "summary": "Send to many destinations in one call (OZAV signs each leg)",
        "description": "One call, one `Idempotency-Key`, one policy decision, one approval - for\nup to 50 sends from the same managed wallet, on the same network, in the\nsame currency.\n\n**What this replaces.** Without it you write the loop yourself, and each\nturn of your loop is a separate policy decision: a separate approval to\nchase, a separate idempotency key to track, and a separate answer to\nreconcile. A batch is one decision over the whole payout run.\n\n**The legs run one at a time, in order, and that is deliberate.** A\ntransaction is built against the sending address's pending nonce, so two\nsends from the same wallet cannot be in flight at once without building\nthe same transaction twice. The batch is not faster than your own loop -\nit is one decision instead of many.\n\n**It stops at the first leg that does not settle.** Travel Rule is\nchecked for **every** leg before the first one is signed, so an\nattestation gap cannot strand a half-finished run. Everything else -\naddress validity, the destination wallet, currency against the wallet's\nasset, balance, freezes, gas, compliance holds - is checked per leg, as\nit is for a single send, so **a bad destination in leg 5 can stop a run\nwhose first four legs already moved real money.** Read `legs[]` for what\nactually happened; never assume a batch is all-or-nothing.\n\nLegs after the failure come back `cancelled`, which means **not\nattempted** - never `failed`.\n\n**`needs_review` is not a failure.** It means the send may have been\nbroadcast and the record did not close. Never re-send that leg on the\nstrength of this response: read the batch back, or reconcile from the\nleg's own `wallet_transfer` id.\n\n**Your approval quorum sees the TOTAL.** If the wallet's policy sets\n`approvals.above_usd`, the threshold is applied to the sum of the legs,\nnot to each leg. Ten legs of 9,000 under a 10,000 threshold require\napproval, because the batch is a 90,000 movement.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Required idempotency key (16-256 characters of `[A-Za-z0-9_-]`). One key covers the whole batch: a retry with the same key returns the same batch and never re-sends a leg that already settled.\n",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "currency",
                  "legs"
                ],
                "properties": {
                  "currency": {
                    "$ref": "#/components/schemas/Currency"
                  },
                  "chain": {
                    "type": "string",
                    "description": "Optional explicit chain for the whole batch. Every leg is resolved on THIS chain - a batch never spans networks, because the pending nonce that orders the legs belongs to one address on one chain.",
                    "example": "polygon"
                  },
                  "legs": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 50,
                    "description": "The sends, in the order they will be attempted. 50 is the ceiling because the legs run inside one request: a longer batch would run out of time mid-run, with part of the money already sent and no answer returned to you.",
                    "items": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "destination",
                        "amount"
                      ],
                      "properties": {
                        "destination": {
                          "type": "string",
                          "description": "The recipient - a client-owned wallet id (ozav_wal_) or an on-chain address valid for the batch's chain."
                        },
                        "amount": {
                          "$ref": "#/components/schemas/DecimalAmount"
                        },
                        "memo": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 28,
                          "description": "Destination memo/tag, per leg, for networks that carry one (Stellar). On a memo-based network the recipient credits by memo, so a leg without it arrives attributed to nobody."
                        },
                        "travel_rule": {
                          "$ref": "#/components/schemas/TravelRule"
                        }
                      }
                    }
                  }
                }
              },
              "examples": {
                "payout_run": {
                  "summary": "Pay three suppliers in one decision",
                  "value": {
                    "currency": "USDC",
                    "chain": "polygon",
                    "legs": [
                      {
                        "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                        "amount": "1200.000000"
                      },
                      {
                        "destination": "0x2e5daaf3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
                        "amount": "845.500000"
                      },
                      {
                        "destination": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V7",
                        "amount": "300.000000"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The batch ran. Read `status` for the batch and `legs[].status` for each send. `partially_completed` means some legs settled and some did not - the settled ones moved real money and are not reversible.\n",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletTransferBatch"
                },
                "examples": {
                  "completed": {
                    "summary": "All three legs settled",
                    "value": {
                      "id": "ozav_wtb_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "wallet_transfer_batch",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "completed",
                      "currency": "USDC",
                      "total_amount": "2345.5",
                      "leg_count": 3,
                      "legs": [
                        {
                          "index": 0,
                          "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                          "status": "completed",
                          "amount": "1200.000000",
                          "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                          "transaction_hash": "0x6c984fe16f15aa9865e7ca950c5038542e5789c8bd6dfc2fd95f649514a88b47"
                        },
                        {
                          "index": 1,
                          "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V3",
                          "status": "completed",
                          "amount": "845.500000",
                          "destination": "0x2e5daaf3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
                          "transaction_hash": "0x8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f9"
                        },
                        {
                          "index": 2,
                          "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V4",
                          "status": "completed",
                          "amount": "300.000000",
                          "destination": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V7",
                          "transaction_hash": "0x9b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90"
                        }
                      ]
                    }
                  },
                  "partially_completed": {
                    "summary": "Leg 1 ran out of balance; leg 2 was never attempted. `cancelled` is not `failed` - nothing was sent for it.",
                    "value": {
                      "id": "ozav_wtb_01J8Z9K2M3N4P5Q6R7S8T9U0V5",
                      "object": "wallet_transfer_batch",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "partially_completed",
                      "currency": "USDC",
                      "total_amount": "2345.5",
                      "leg_count": 3,
                      "legs": [
                        {
                          "index": 0,
                          "id": "ozav_wtr_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                          "status": "completed",
                          "amount": "1200.000000",
                          "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                          "transaction_hash": "0x6c984fe16f15aa9865e7ca950c5038542e5789c8bd6dfc2fd95f649514a88b47"
                        },
                        {
                          "index": 1,
                          "id": null,
                          "status": "failed",
                          "amount": "845.500000",
                          "destination": "0x2e5daaf3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
                          "transaction_hash": null,
                          "failure": {
                            "code": "INSUFFICIENT_BALANCE",
                            "message": "The wallet does not hold enough of this asset for this send."
                          }
                        },
                        {
                          "index": 2,
                          "id": null,
                          "status": "cancelled",
                          "amount": "300.000000",
                          "destination": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V7",
                          "transaction_hash": null
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "**Held by your approval quorum, as one decision over the whole batch.** The threshold was applied to `total_amount`, not to any single leg. Nothing has been signed and every leg reads `pending`. `approval.id` is what an approver key (`approvals:write`, a different key from the one that sent this request) passes to `POST /approvals/{approval_id}/approve`; the last required approval runs the whole batch. Unapproved after `approval.expires_at` (72 h) it expires and nothing moves.\n",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletTransferBatch"
                },
                "examples": {
                  "held": {
                    "summary": "Ten legs of 9,000 sum to 90,000 and cross a 10,000 threshold",
                    "value": {
                      "id": "ozav_wtb_01J8Z9K2M3N4P5Q6R7S8T9U0V6",
                      "object": "wallet_transfer_batch",
                      "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "status": "awaiting_approval",
                      "currency": "USDC",
                      "total_amount": "90000",
                      "leg_count": 2,
                      "legs": [
                        {
                          "index": 0,
                          "id": null,
                          "status": "pending",
                          "amount": "45000.000000",
                          "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                          "transaction_hash": null
                        },
                        {
                          "index": 1,
                          "id": null,
                          "status": "pending",
                          "amount": "45000.000000",
                          "destination": "0x2e5daaf3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9",
                          "transaction_hash": null
                        }
                      ],
                      "approval": {
                        "id": "ozav_apr_01J8Z9K2M3N4P5Q6R7S8T9U0V3",
                        "required_approvals": 2,
                        "expires_at": "2026-09-13T12:00:00Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfer-batches \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Idempotency-Key: idem-6f9c24e5b1a74d38\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"currency\": \"USDC\",\n    \"chain\": \"polygon\",\n    \"legs\": [\n      {\n        \"destination\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\",\n        \"amount\": \"1200.000000\"\n      },\n      {\n        \"destination\": \"0x2e5daaf3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9\",\n        \"amount\": \"845.500000\"\n      },\n      {\n        \"destination\": \"ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V7\",\n        \"amount\": \"300.000000\"\n      }\n    ]\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfer-batches\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"currency\": \"USDC\",\n    \"chain\": \"polygon\",\n    \"legs\": [\n      {\n        \"destination\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\",\n        \"amount\": \"1200.000000\"\n      },\n      {\n        \"destination\": \"0x2e5daaf3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9\",\n        \"amount\": \"845.500000\"\n      },\n      {\n        \"destination\": \"ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V7\",\n        \"amount\": \"300.000000\"\n      }\n    ]\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfer-batches\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Idempotency-Key\": \"idem-6f9c24e5b1a74d38\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"currency\": \"USDC\",\n        \"chain\": \"polygon\",\n        \"legs\": [\n            {\n                \"destination\": \"0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8\",\n                \"amount\": \"1200.000000\",\n            },\n            {\n                \"destination\": \"0x2e5daaf3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9\",\n                \"amount\": \"845.500000\",\n            },\n            {\n                \"destination\": \"ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V7\",\n                \"amount\": \"300.000000\",\n            },\n        ],\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/wallets/{wallet_id}/transfer-batches/{batch_id}": {
      "get": {
        "operationId": "getManagedTransferBatch",
        "tags": [
          "Wallets"
        ],
        "summary": "Read a transfer batch",
        "description": "The batch as it stands, with one entry per leg you asked for - including\nthe legs that were never attempted, so the count always matches what you\nsent. Read this after a timeout, or while a batch waits on your approval\nquorum.\n\nA leg with `id: null` does not exist anywhere: nothing was sent for it.\nA leg with an `id` is a real `wallet_transfer` you can reconcile.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "wallets:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "wallet_id",
            "in": "path",
            "required": true,
            "description": "The opaque wallet id (`ozav_wal_...`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "batch_id",
            "in": "path",
            "required": true,
            "description": "The opaque batch id (`ozav_wtb_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The batch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WalletTransferBatch"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfer-batches/{batch_id} \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfer-batches/{batch_id}\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfer-batches/{batch_id}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/api-keys": {
      "post": {
        "operationId": "createApiKey",
        "tags": [
          "API Keys"
        ],
        "summary": "Create an API key (self-serve)",
        "description": "Creates a new API key scoped to a subset of your entitlements. The full\n`secret` is returned exactly once, in this response - store it securely,\nas it cannot be retrieved again.\n\n**Give it either `role` or `scopes` - exactly one.**\n\n- **`role`** is the shortcut: `admin`, `operator` or `viewer`. Use it to\n  separate powers between the people and systems on your side without\n  picking 32 scopes by hand. A role is a **subset of what the minting key\n  already holds** - never a way to get more - so a key minted with\n  `role: admin` by a narrow key is still narrow.\n- **`scopes`** is the explicit list, intersected with the organization's\n  entitlements at request time; an empty set is rejected.\n\nSending both, or neither, is a `400`: a permission object should not have\nan implicit default, and there is no sensible rule for which one wins.\n",
        "x-ozav-entitlement": "developer",
        "x-ozav-scopes": [
          "keys:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "oneOf": [
                  {
                    "required": [
                      "role"
                    ],
                    "not": {
                      "required": [
                        "scopes"
                      ]
                    }
                  },
                  {
                    "required": [
                      "scopes"
                    ],
                    "not": {
                      "required": [
                        "role"
                      ]
                    }
                  }
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "A human-readable label for the key, to identify it later."
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "operator",
                      "viewer"
                    ],
                    "description": "The key's role - a template over the scopes the minting key\nalready holds. Mutually exclusive with `scopes`.\n\n- `admin` - everything the minting key holds.\n- `operator` - everything except `keys:write`, so the key\n  cannot mint or revoke other keys. It keeps `keys:read` (it\n  still sees which keys exist) and `approvals:write`, because\n  approving a held send is the operational act this role\n  exists to name.\n- `viewer` - read scopes only. A viewer key cannot approve.\n\nA role is a **subset** of the minting key's scopes: it never\ngrants more than the caller already had.\n\nThe role is stored and echoed back on the key, and a rotated\nkey keeps it. It is a label and a mint-time template - it is\n**not** part of the permission decision, which stays\n`entitlements ∩ scopes`.\n"
                  },
                  "environment": {
                    "type": "string",
                    "description": "Which environment the new key is for - `test` (sandbox) or\n`live` (production). The prefix of the returned key reflects this.\n",
                    "enum": [
                      "test",
                      "live"
                    ]
                  },
                  "scopes": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "customers:read",
                        "customers:write",
                        "accounts:read",
                        "accounts:write",
                        "quotes:read",
                        "quotes:write",
                        "transfers:read",
                        "transfers:write",
                        "destinations:read",
                        "destinations:write",
                        "wallets:read",
                        "wallets:write",
                        "screenings:read",
                        "screenings:write",
                        "verifications:read",
                        "verifications:write",
                        "cards:read",
                        "cards:write",
                        "webhooks:read",
                        "webhooks:write",
                        "events:read",
                        "keys:read",
                        "keys:write",
                        "transactions:read",
                        "yield:read",
                        "yield:write",
                        "yield:apply",
                        "swap:read",
                        "network_transfers:read",
                        "network_transfers:write",
                        "approvals:read",
                        "approvals:write",
                        "evidence:read"
                      ]
                    },
                    "description": "Scopes are intersected with the organization's entitlements at request time; an\nempty scope set is invalid. This is a **closed set**: a value outside the enum\nabove is **rejected** with `400 INVALID_SCOPE`, and the error lists every valid\nscope - a typo fails loudly at mint time rather than quietly minting a key with\na narrower scope set than you asked for. You can only grant scopes the minting\nkey itself holds (`403 SCOPE_NOT_HELD` otherwise).\n"
                  }
                }
              },
              "examples": {
                "operator_role": {
                  "summary": "An operator key, without listing scopes by hand",
                  "value": {
                    "name": "payments operator",
                    "environment": "live",
                    "role": "operator"
                  }
                },
                "live_key": {
                  "summary": "A live key scoped to move money",
                  "value": {
                    "name": "server-side production key",
                    "environment": "live",
                    "scopes": [
                      "quotes:write",
                      "transfers:write",
                      "transfers:read",
                      "customers:read"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created; the secret is returned once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyWithSecret"
                },
                "examples": {
                  "created": {
                    "summary": "Secret returned once - store it securely",
                    "value": {
                      "id": "ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                      "object": "api_key",
                      "name": "server-side production key",
                      "key_prefix": "ozav_sk_live_a1b2",
                      "environment": "live",
                      "scopes": [
                        "quotes:write",
                        "transfers:write",
                        "transfers:read",
                        "customers:read"
                      ],
                      "created_at": "2026-07-12T18:00:00Z",
                      "secret": "ozav_sk_live_a1b2c3d4e5f60718293a4b5c6d7e8f90"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/api-keys \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"payments operator\",\n    \"environment\": \"live\",\n    \"role\": \"operator\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/api-keys\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"name\": \"payments operator\",\n    \"environment\": \"live\",\n    \"role\": \"operator\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/api-keys\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"name\": \"payments operator\",\n        \"environment\": \"live\",\n        \"role\": \"operator\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listApiKeys",
        "tags": [
          "API Keys"
        ],
        "summary": "List API keys (prefixes only)",
        "description": "Lists your API keys with their non-secret `key_prefix`, scopes, and\nenvironment. Secrets are never returned here - a secret is shown only at\ncreation and rotation. Live.\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "keys:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "Keys (no secrets).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApiKey"
                      }
                    }
                  }
                },
                "examples": {
                  "keys": {
                    "summary": "Your keys, prefixes only",
                    "value": {
                      "data": [
                        {
                          "id": "ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                          "object": "api_key",
                          "name": "server-side production key",
                          "key_prefix": "ozav_sk_live_a1b2",
                          "environment": "live",
                          "scopes": [
                            "quotes:write",
                            "transfers:write",
                            "transfers:read",
                            "customers:read"
                          ],
                          "last_used_at": "2026-07-12T17:59:00Z",
                          "created_at": "2026-07-12T18:00:00Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/api-keys \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/api-keys\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/api-keys\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/api-keys/{api_key_id}": {
      "delete": {
        "operationId": "revokeApiKey",
        "tags": [
          "API Keys"
        ],
        "summary": "Revoke an API key",
        "description": "Revokes an API key immediately; subsequent requests presenting that key\nfail authentication. Returns `204 No Content`. Irreversible - mint a new\nkey to replace a revoked one. Live.\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "keys:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "api_key_id",
            "in": "path",
            "required": true,
            "description": "The opaque API-key id (`ozav_key_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/api-keys/ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -X DELETE \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/api-keys/ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"DELETE\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.delete(\n    \"https://developers.ozav.io/v2/api-keys/ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/api-keys/{api_key_id}/rotate": {
      "post": {
        "operationId": "rotateApiKey",
        "tags": [
          "API Keys"
        ],
        "summary": "Rotate an API key with an overlap window",
        "description": "Mints a successor key with identical scopes. The previous secret\nremains valid for the overlap window (default 24 hours, maximum 7\ndays), then is revoked automatically. Live.\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "keys:write"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "api_key_id",
            "in": "path",
            "required": true,
            "description": "The opaque API-key id (`ozav_key_...`).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "overlap_hours": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 168,
                    "default": 24,
                    "description": "How long the previous secret stays valid after rotation, in hours (0-168; default 24). It is revoked automatically when the window elapses."
                  }
                }
              },
              "examples": {
                "overlap": {
                  "summary": "48-hour overlap window",
                  "value": {
                    "overlap_hours": 48
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successor key; secret returned once.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyWithSecret"
                },
                "examples": {
                  "rotated": {
                    "summary": "Successor key - store the new secret; the old one stays valid for the overlap window",
                    "value": {
                      "id": "ozav_key_02J8Z9K2M3N4P5Q6R7S8T9U0V2",
                      "object": "api_key",
                      "name": "server-side production key",
                      "key_prefix": "ozav_sk_live_c3d4",
                      "environment": "live",
                      "scopes": [
                        "quotes:write",
                        "transfers:write",
                        "transfers:read",
                        "customers:read"
                      ],
                      "created_at": "2026-07-12T18:20:00Z",
                      "secret": "ozav_sk_live_c3d4e5f60718293a4b5c6d7e8f90a1b2"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/api-keys/ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1/rotate \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"overlap_hours\": 48\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/api-keys/ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1/rotate\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"overlap_hours\": 48\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/api-keys/ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1/rotate\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"overlap_hours\": 48,\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/webhook-endpoints": {
      "post": {
        "operationId": "createWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Register a webhook endpoint",
        "description": "Registers an HTTPS endpoint to receive signed event notifications for the\nsubscribed `enabled_events`. The signing `secret` (`ozav_whsec_`) is\nreturned exactly once, in this response - store it securely; it is used to\nverify delivery signatures and cannot be retrieved again.\n\nThe hostname is checked at registration. A host that does not resolve is\nrefused with `422 WEBHOOK_URL_HOST_NOT_FOUND`, because a typo there fails\nsilently forever: deliveries never reach anything and the only trace is a\nnull `response_status` in `GET /webhook-endpoints/{id}/deliveries`. Only a\nnon-existent hostname is refused - an endpoint behind a firewall, not yet\ndeployed, or slow to answer registers normally.\n",
        "x-ozav-entitlement": "developer",
        "x-ozav-scopes": [
          "webhooks:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEndpointCreateRequest"
              },
              "examples": {
                "basic": {
                  "summary": "Subscribe to customer + screening events",
                  "value": {
                    "url": "https://api.acme.com/ozav/webhooks",
                    "enabled_events": [
                      "customer.created",
                      "screening.completed"
                    ],
                    "description": "production"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Endpoint registered; the signing secret is in `secret` (shown once).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointWithSecret"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/webhook-endpoints \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://api.acme.com/ozav/webhooks\",\n    \"enabled_events\": [\n      \"customer.created\",\n      \"screening.completed\"\n    ],\n    \"description\": \"production\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/webhook-endpoints\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"url\": \"https://api.acme.com/ozav/webhooks\",\n    \"enabled_events\": [\n      \"customer.created\",\n      \"screening.completed\"\n    ],\n    \"description\": \"production\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/webhook-endpoints\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"url\": \"https://api.acme.com/ozav/webhooks\",\n        \"enabled_events\": [\n            \"customer.created\",\n            \"screening.completed\",\n        ],\n        \"description\": \"production\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "get": {
        "operationId": "listWebhookEndpoints",
        "tags": [
          "Webhooks"
        ],
        "summary": "List webhook endpoints",
        "description": "Lists your webhook endpoints using the standard cursor pagination. Secrets are never returned.",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "webhooks:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/WebhookEndpoint"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/webhook-endpoints \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/webhook-endpoints\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/webhook-endpoints\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/webhook-endpoints/{webhook_endpoint_id}": {
      "parameters": [
        {
          "name": "webhook_endpoint_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Opaque webhook endpoint id, prefixed `ozav_wh_`."
        }
      ],
      "get": {
        "operationId": "getWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Retrieve a webhook endpoint",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "webhooks:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The webhook endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id} \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "patch": {
        "operationId": "updateWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Update a webhook endpoint",
        "description": "Update any subset of url, enabled_events, status, description. The signing secret is unchanged (use rotate-secret).",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "webhooks:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEndpointUpdateRequest"
              },
              "examples": {
                "trocar_eventos": {
                  "summary": "Subscribe to a different set of events",
                  "value": {
                    "enabled_events": [
                      "transfer.completed",
                      "transfer.failed",
                      "deposit.credited"
                    ]
                  }
                },
                "desligar": {
                  "summary": "Disable an endpoint without deleting it",
                  "value": {
                    "status": "disabled"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated webhook endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id} \\\n  -X PATCH \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"enabled_events\": [\n      \"transfer.completed\",\n      \"transfer.failed\",\n      \"deposit.credited\"\n    ]\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}\", {\n  method: \"PATCH\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"enabled_events\": [\n      \"transfer.completed\",\n      \"transfer.failed\",\n      \"deposit.credited\"\n    ]\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.patch(\n    \"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"enabled_events\": [\n            \"transfer.completed\",\n            \"transfer.failed\",\n            \"deposit.credited\",\n        ],\n    },\n)\ndata = response.json()"
          }
        ]
      },
      "delete": {
        "operationId": "deleteWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook endpoint",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "webhooks:write"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "object": {
                      "type": "string",
                      "enum": [
                        "webhook_endpoint"
                      ]
                    },
                    "deleted": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id} \\\n  -X DELETE \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}\", {\n  method: \"DELETE\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.delete(\n    \"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/webhook-endpoints/{webhook_endpoint_id}/rotate-secret": {
      "parameters": [
        {
          "name": "webhook_endpoint_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Opaque webhook endpoint id, prefixed `ozav_wh_`."
        }
      ],
      "post": {
        "operationId": "rotateWebhookEndpointSecret",
        "tags": [
          "Webhooks"
        ],
        "summary": "Rotate a webhook endpoint's signing secret",
        "description": "Generates a new signing secret and returns it once. The previous secret\nstops signing immediately, so update your verifier before rotating.\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "webhooks:write"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The endpoint with its new `secret` (shown once).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointWithSecret"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/rotate-secret \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/rotate-secret\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/rotate-secret\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/webhook-endpoints/{webhook_endpoint_id}/test": {
      "parameters": [
        {
          "name": "webhook_endpoint_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Opaque webhook endpoint id, prefixed `ozav_wh_`."
        }
      ],
      "post": {
        "operationId": "testWebhookEndpoint",
        "tags": [
          "Webhooks"
        ],
        "summary": "Send a test event",
        "description": "Queues a signed `webhook.test` event for this endpoint, even if\n`webhook.test` is not in `enabled_events`. The response is the queued\ndelivery row; delivery itself is asynchronous and retried by OZAV.\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "webhooks:write"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The queued test delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDelivery"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/test \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/test\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/test\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/webhook-endpoints/{webhook_endpoint_id}/deliveries": {
      "parameters": [
        {
          "name": "webhook_endpoint_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Opaque webhook endpoint id, prefixed `ozav_wh_`."
        }
      ],
      "get": {
        "operationId": "listWebhookDeliveries",
        "tags": [
          "Webhooks"
        ],
        "summary": "List webhook deliveries",
        "description": "Lists recent delivery attempts for one webhook endpoint. Response bodies\nare truncated previews; endpoint secrets, secret hashes, Vault refs, and\ninternal ids are never returned.\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "webhooks:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          }
        ],
        "responses": {
          "200": {
            "description": "Delivery attempts.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/WebhookDelivery"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/deliveries \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/deliveries\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/deliveries\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/events": {
      "get": {
        "operationId": "listEvents",
        "tags": [
          "Webhooks"
        ],
        "summary": "List events",
        "description": "Lists your account's events, newest first - the pull side of the event\nlog. Each item is the same canonical envelope a webhook delivers, so you\ncan reconcile or backfill a missed delivery without a webhook endpoint.\nClient-scoped; keyset-paginated.\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "events:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/StartingAfter"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of events.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Page"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "data": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/WebhookEvent"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/events \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/events\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/events\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/events/{event_id}": {
      "parameters": [
        {
          "name": "event_id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Opaque event id, prefixed `ozav_evt_`."
        }
      ],
      "get": {
        "operationId": "getEvent",
        "tags": [
          "Webhooks"
        ],
        "summary": "Retrieve an event",
        "description": "Fetches a single event by id - the same canonical envelope a webhook\ndelivers.\n",
        "x-ozav-entitlement": "any",
        "x-ozav-scopes": [
          "events:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The event.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEvent"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/events/{event_id} \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/events/{event_id}\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/events/{event_id}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/yield/realized-gains": {
      "get": {
        "operationId": "listRealizedGains",
        "tags": [
          "Yield"
        ],
        "summary": "Realized gain per month",
        "description": "The gain already MATERIALIZED by redemption, grouped by month and by\nposition. This is the input a tax computation consumes: it is not a tax\nfiling, and the name says so on purpose.\n\nTwo things it deliberately does NOT do:\n\n* **No fiat conversion.** Converting needs the rate of the day of each\n  operation, and which rate source is valid for tax purposes is a\n  compliance decision. A number invented here would look like a filing\n  and not be one. `currency` is always `asset_minor_units`.\n* **No filing format.** What a tax authority requires is a separate\n  decision from what the gain IS.\n\nEvery amount is a decimal integer STRING in the asset smallest unit.\nSending it as a JSON number would lose precision above 2^53, and this\nnumber ends up in a declaration.\n\nThe competence month is the month of the BLOCK, never of when the\nmovement was recorded. Those differ when ingestion lags, and on a month\nboundary the difference is the wrong filing period.\n\nPositions are computed separately: merging two would let a redemption in\none consume the cost basis of the other, and both figures would be\nplausible and wrong.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "yield:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "Realized gain per month and position.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "currency": {
                          "type": "string",
                          "enum": [
                            "asset_minor_units"
                          ],
                          "description": "Always the asset smallest unit; no fiat conversion is applied."
                        },
                        "note": {
                          "type": "string"
                        },
                        "months": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "month": {
                                "type": "string",
                                "example": "2026-02"
                              },
                              "chain": {
                                "type": "string"
                              },
                              "vault_address": {
                                "type": "string"
                              },
                              "asset": {
                                "type": "string"
                              },
                              "realized_gain": {
                                "type": "string",
                                "description": "Decimal integer string; may be negative.",
                                "example": "200000"
                              },
                              "remaining_cost_basis": {
                                "type": "string",
                                "example": "0"
                              },
                              "remaining_shares": {
                                "type": "string",
                                "example": "0"
                              },
                              "movements": {
                                "type": "integer",
                                "example": 1
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "dois_meses": {
                    "summary": "Deposit in January, redemption with gain in February",
                    "value": {
                      "data": {
                        "currency": "asset_minor_units",
                        "note": "realized gain in the asset minor unit; no fiat conversion is applied",
                        "months": [
                          {
                            "month": "2026-01",
                            "chain": "base",
                            "asset": "USDC",
                            "vault_address": "0x1234",
                            "realized_gain": "0",
                            "remaining_cost_basis": "1000000",
                            "remaining_shares": "1000000",
                            "movements": 1
                          },
                          {
                            "month": "2026-02",
                            "chain": "base",
                            "asset": "USDC",
                            "vault_address": "0x1234",
                            "realized_gain": "200000",
                            "remaining_cost_basis": "0",
                            "remaining_shares": "0",
                            "movements": 1
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "description": "The movement reader is not wired in this deployment.",
            "content": {
              "application/json": {
                "examples": {
                  "indisponivel": {
                    "summary": "Refusal, never an empty list",
                    "value": {
                      "error": {
                        "type": "api_error",
                        "code": "YIELD_MOVEMENTS_UNAVAILABLE",
                        "message": "realized gains are unavailable in this deployment"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/yield/realized-gains \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/yield/realized-gains\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/yield/realized-gains\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/yield/strategies": {
      "get": {
        "operationId": "listYieldStrategies",
        "tags": [
          "Yield"
        ],
        "summary": "List yield strategies",
        "description": "Lists the yield strategies open to the client: the asset, the network\nand the indicative rate. Read-only, and the same catalogue for every\nclient holding the entitlement.\n\n`apy_bps` is INDICATIVE and refreshed off-chain; it is `null` when no\nrecent figure exists, which is not the same as zero.\n\n`realized_apy_7d` and `realized_apy_30d` are MEASURED from the on-chain\nshare price we record hourly. They sit beside the indicative rate and\nnever replace it: the indicative figure says what the protocol pays now,\nthe realized figure says what actually accrued.\n\nWhen a realized figure cannot be stated, `known` is `false` and `reason`\nsays why, rather than the field being null or zero:\n  * `no_data`: no series for this strategy.\n  * `window_not_covered`: the series began INSIDE the window, so a\n    figure would be labelled for a period it did not measure.\n  * `window_too_short`: under 12 hours between readings; annualising\n    that multiplies noise by thousands.\n  * `price_went_backwards`: the share price fell. That is an alarm in a\n    lending vault, not a negative yield.\n  * `unusable_price`: a reading could not be used.\n\nWhen `known` is `true`, `observed_hours` reports what was actually\nmeasured; the `7d`/`30d` in the field name is what was REQUESTED.\n\nApplying and redeeming are not exposed. Those move funds on-chain and\nremain gated.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "yield:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The strategies open to the client.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "chain": {
                            "type": "string"
                          },
                          "asset": {
                            "type": "string"
                          },
                          "apy_bps": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "realized_apy_7d": {
                            "$ref": "#/components/schemas/RealizedApy"
                          },
                          "realized_apy_30d": {
                            "$ref": "#/components/schemas/RealizedApy"
                          },
                          "risk": {
                            "$ref": "#/components/schemas/YieldRisk"
                          }
                        }
                      }
                    }
                  }
                },
                "examples": {
                  "one": {
                    "summary": "One strategy, 7d measured and 30d not yet covered",
                    "value": {
                      "data": [
                        {
                          "id": "base-usdc-prime",
                          "chain": "base",
                          "asset": "USDC",
                          "apy_bps": 445,
                          "realized_apy_7d": {
                            "known": true,
                            "bps": 512,
                            "observed_hours": 167,
                            "points": 168
                          },
                          "realized_apy_30d": {
                            "known": false,
                            "reason": "window_not_covered"
                          },
                          "risk": {
                            "management": "curated",
                            "curator_ref": "curator-1",
                            "issuer_freeze": "none",
                            "redemption_form": "immediate",
                            "principal_guaranteed": false,
                            "smart_contract_risk": true,
                            "measured_at": "2026-08-06"
                          }
                        }
                      ]
                    }
                  },
                  "queued": {
                    "summary": "A strategy whose redemption is a request, and whose issuer holds a key",
                    "value": {
                      "data": [
                        {
                          "id": "solana-usd-treasury",
                          "chain": "solana",
                          "asset": "USDC",
                          "apy_bps": null,
                          "realized_apy_7d": {
                            "known": false,
                            "reason": "no_data"
                          },
                          "realized_apy_30d": {
                            "known": false,
                            "reason": "no_data"
                          },
                          "risk": {
                            "management": "direct",
                            "curator_ref": null,
                            "issuer_freeze": "unilateral_key",
                            "redemption_form": "by_request",
                            "principal_guaranteed": false,
                            "smart_contract_risk": true,
                            "measured_at": "2026-08-09"
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/yield/strategies \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/yield/strategies\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/yield/strategies\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/yield/wallets/{address}/positions": {
      "get": {
        "operationId": "listYieldPositionsByAddress",
        "tags": [
          "Yield"
        ],
        "summary": "List yield positions for a wallet you control",
        "description": "Lists the yield positions held by a wallet address you supply. Use this\nwhen the holder keeps their own key and is not under OZAV custody.\n\nThe address is read from the path. That is safe and deliberate: an\naddress balance is public data on the chain, readable by anyone without\nanyone's permission. The account making the call still comes from your\nAPI key and can never be supplied in a request, on this route or any\nother.\n\nThe address is matched to the networks of its own family. An EVM address\nis not queried on Solana and the reverse, so a network that cannot apply\nnever lands in `unreadable_chains` and never turns `complete` to false\nfor a reason that is not real.\n\n**`cost_basis` is `null` here, and that is the honest answer.** The\ndeposit did not pass through OZAV, so there is no book to read. Those\npositions report `accounting: \"untracked\"` with the reason attached.\nWhat the position is worth today is read from the chain and is exact.\n",
        "x-ozav-entitlement": "self_custody",
        "x-ozav-scopes": [
          "yield:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "address",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The wallet address. EVM (`0x` + 40 hex) or Solana (base58). An\naddress matching neither is refused with `WALLET_ADDRESS_INVALID`\nrather than searched for, because \"not found\" and \"mistyped\" read\nthe same and mean opposite things.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "The positions held by that address.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "positions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "chain": {
                            "type": "string"
                          },
                          "asset": {
                            "type": "string"
                          },
                          "balance": {
                            "type": "string"
                          },
                          "apy_bps": {
                            "type": "integer"
                          },
                          "cost_basis": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "description": "Always `null` on this route. The deposit did not pass\nthrough OZAV, so there is no book. `null` is the\nabsence of the number, never zero, which would be a\nclaim that nothing was paid.\n"
                          },
                          "unrealized": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "realized": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "accounting": {
                            "type": "string",
                            "description": "`untracked` on this route, with the reason attached.\n"
                          },
                          "redemption": {
                            "$ref": "#/components/schemas/YieldRedemption"
                          }
                        }
                      }
                    },
                    "unreadable_chains": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "complete": {
                      "type": "boolean"
                    }
                  }
                },
                "examples": {
                  "one": {
                    "summary": "A wallet OZAV has never seen, so no acquisition cost",
                    "value": {
                      "positions": [
                        {
                          "chain": "base",
                          "asset": "USDC",
                          "balance": "42180550",
                          "apy_bps": 445,
                          "cost_basis": null,
                          "unrealized": null,
                          "realized": null,
                          "accounting": "untracked",
                          "redemption": {
                            "kind": "available",
                            "available_to_withdraw": "42180550"
                          }
                        }
                      ],
                      "unreadable_chains": [],
                      "complete": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/yield/wallets/{address}/positions \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/yield/wallets/{address}/positions\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/yield/wallets/{address}/positions\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/yield/applications": {
      "post": {
        "operationId": "createYieldApplication",
        "tags": [
          "Yield"
        ],
        "summary": "Apply custodied balance to a yield strategy",
        "description": "Instructs OZAV to apply this client's custodied balance to a strategy.\nOZAV signs with its own key, under a two-party quorum.\n\nTHIS IS THE OPPOSITE OF `/yield/deposits/build`. That route hands back an\nUNSIGNED transaction for a client that holds its own key. This one moves\nthe balance OZAV custodies, on the client's instruction. The two carry\ndifferent scopes for that reason: `yield:write` builds, `yield:apply`\ninstructs.\n\nCLOSED PILOT. `yield:apply` cannot be self-served: `POST /api-keys`\nrefuses to grant a scope the caller does not already hold, and no\nentitlement bootstraps this one. It is granted per account, by OZAV,\nwhile the pilot is closed. Asking for it on a key mint answers\n`SCOPE_NOT_HELD` (403), and that is the designed answer, not a defect.\n\nTWO SWITCHES BEHIND IT, both off by default. Even with the scope, the\norder is refused with `MANAGED_YIELD_NOT_ENABLED` until the surface is\nenabled for the environment, and with `MANAGED_YIELD_CLIENT_NOT_ENABLED`\nuntil it is enabled for the account. The two are separate codes because\nthey call for opposite actions: wait for a launch, or ask to be enrolled.\nBoth arrive as the application's terminal `refused` state with a\n`detail`, never as an error on this call, because this call only records\nthe instruction.\n\nANSWERS 202, NEVER 200. The signature needs a second approval that can\nwait on a person, so a synchronous answer would time out before there\nwas one. The response carries an `id`; the state is read from\n`GET /yield/applications/{application_id}`. This is not a lesser\ncontract, it is an honest one about what custody means.\n\nONE APPLICATION IN FLIGHT per strategy. A second call while one is still\n`pending` or `running` answers 202 with the EXISTING application and\n`duplicate: true` rather than opening another: two in flight would race\nfor the same free balance and one would revert on-chain, with gas paid.\n\nTHE GATES ARE CHECKED WHEN IT RUNS, not here. This route verifies that\nthe strategy exists and the amount is positive; authorisation and the\nexposure limits are enforced by the executor, and a refusal surfaces as\n`status: refused` with the cause in `detail`. Duplicating them here\nwould create two answers to \"may this apply?\", and the looser one would\nwin on the path nobody watches.\n\nAUTHORISATION IS NOT A CLIENT ACTION. Consent lives in the custody\nagreement; whether this client may have its balance applied is an OZAV\noperational control, not something the caller can turn on.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "yield:apply"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "strategy_id",
                  "amount"
                ],
                "properties": {
                  "strategy_id": {
                    "type": "string",
                    "description": "The strategy to apply to, as listed by `GET /yield/strategies`."
                  },
                  "amount": {
                    "type": "string",
                    "description": "Amount in the asset's smallest unit, as a decimal integer\nSTRING. A JSON number would lose precision above 2^53.\n"
                  }
                }
              },
              "examples": {
                "aplicar": {
                  "summary": "Apply 100 USDC (smallest unit) to a strategy",
                  "value": {
                    "strategy_id": "base-usdc-prime",
                    "amount": "100000000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. The application is recorded; read its state by id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/YieldApplication"
                    },
                    "duplicate": {
                      "type": "boolean",
                      "description": "True when an application was already in flight for this\nstrategy and `data` is that one, not a new one. `data.amount`\nis that order's amount and MAY DIFFER from the one just sent:\nthe request was accepted as a no-op, not as a new instruction.\n"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/yield/applications \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"strategy_id\": \"base-usdc-prime\",\n    \"amount\": \"100000000\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/yield/applications\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"strategy_id\": \"base-usdc-prime\",\n    \"amount\": \"100000000\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/yield/applications\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"strategy_id\": \"base-usdc-prime\",\n        \"amount\": \"100000000\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/yield/applications/{application_id}": {
      "get": {
        "operationId": "getYieldApplication",
        "tags": [
          "Yield"
        ],
        "summary": "Read the state of a yield application",
        "description": "The state of an application created by `POST /yield/applications`.\n\n`pending` means it is recorded and not yet executed; `running` means the\nexecutor took it; `applied` means it is confirmed on-chain; `refused`\nmeans a gate declined it and NO money moved; `failed` means it was\nattempted and did not complete. `detail` carries the cause for the last\ntwo, and `tx_hash` is present only when a transaction existed.\n\nAN APPLICATION BELONGING TO ANOTHER CLIENT ANSWERS 404, never 403. A 403\nwould confirm the id exists, and that is information about someone\nelse's account.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "yield:read"
        ],
        "x-ozav-availability": "live",
        "parameters": [
          {
            "name": "application_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The application.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/YieldApplication"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/yield/applications/{application_id} \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/yield/applications/{application_id}\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/yield/applications/{application_id}\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/yield/positions": {
      "get": {
        "operationId": "listYieldPositions",
        "tags": [
          "Yield"
        ],
        "summary": "List yield positions",
        "description": "Lists the authenticated client's yield positions, with the amount\napplied, the acquisition cost and the accrued gain. Read-only.\n\nThe owner is derived from the authenticated key and is never read from\nthe request: a position can only ever be read by the client that holds\nit.\n\n`unreadable_chains` is reported separately and is not folded into the\nposition list. An empty list with a non-empty `unreadable_chains` means\na chain could not be read, which is not the same as holding nothing;\ncheck `complete` before treating the result as a full picture.\n\n`redemption` reports what can leave the position RIGHT NOW, which is a\ndifferent question from `balance` (what the position is worth). The two\ndiverge when the underlying market is fully utilised. It is discriminated\nby cause, because the causes call for opposite actions:\n  * `available`: the amount in `available_to_withdraw` can be redeemed.\n  * `held`: the balance is there and nothing can be redeemed right now\n    (queue, pause or illiquidity). The funds exist and are still earning;\n    this is never the same as holding nothing.\n  * `unreadable`: the limit could not be read. `available_to_withdraw`\n    is `null`, never `\"0\"`: zero would assert that nothing can leave.\n\nApplying and redeeming are not exposed. Those move funds on-chain and\nremain gated.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "yield:read"
        ],
        "x-ozav-availability": "live",
        "responses": {
          "200": {
            "description": "The client's yield positions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "positions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "chain": {
                            "type": "string"
                          },
                          "asset": {
                            "type": "string"
                          },
                          "balance": {
                            "type": "string"
                          },
                          "apy_bps": {
                            "type": "integer"
                          },
                          "cost_basis": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "unrealized": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "realized": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "accounting": {
                            "type": "string"
                          },
                          "redemption": {
                            "$ref": "#/components/schemas/YieldRedemption"
                          }
                        }
                      }
                    },
                    "unreadable_chains": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "complete": {
                      "type": "boolean"
                    }
                  }
                },
                "examples": {
                  "one": {
                    "summary": "One position, every chain read",
                    "value": {
                      "positions": [
                        {
                          "chain": "base",
                          "asset": "USDC",
                          "balance": "42180550",
                          "apy_bps": 445,
                          "cost_basis": "41000000",
                          "unrealized": "1180550",
                          "realized": "0",
                          "accounting": "complete",
                          "redemption": {
                            "kind": "available",
                            "available_to_withdraw": "42180550"
                          }
                        }
                      ],
                      "unreadable_chains": [],
                      "complete": true
                    }
                  },
                  "held": {
                    "summary": "The balance is there and cannot be redeemed right now",
                    "value": {
                      "positions": [
                        {
                          "chain": "base",
                          "asset": "USDC",
                          "balance": "42180550",
                          "apy_bps": 445,
                          "cost_basis": "41000000",
                          "unrealized": "1180550",
                          "realized": "0",
                          "accounting": "complete",
                          "redemption": {
                            "kind": "held",
                            "available_to_withdraw": "0"
                          }
                        }
                      ],
                      "unreadable_chains": [],
                      "complete": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/yield/positions \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/yield/positions\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/yield/positions\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/yield/preview": {
      "post": {
        "operationId": "previewYieldDeposit",
        "tags": [
          "Yield"
        ],
        "summary": "Preview applying to a yield strategy",
        "description": "Prices an application before it is made: the venue cap, and the network\ncost of every on-chain step the operation requires.\n\nREAD-ONLY. It is a POST because it carries a body (what to apply and how\nmuch), not because it writes. Nothing leaves any wallet, and the answer\nis returned immediately.\n\nThe owner is derived from the authenticated key. The body carries WHAT\nand HOW MUCH, never WHOSE.\n\nThere is no published minimum, and that is deliberate: a minimum is not a\nfact of documentation, it is a fact of quotation. It moves with the\nvenue, the asset and the moment. Send an amount and read the typed\nrefusal:\n  * `AMOUNT_IN_TOO_HIGH` : above the cap the venue will currently accept,\n    read live from the contract.\n  * `AMOUNT_IN_TOO_LOW` : the network cost of the operation is not less\n    than the amount being applied, so applying would destroy value. This\n    floor is live, and rises on its own when the network gets expensive.\n\n`network_cost` sums EVERY step. Applying is normally two transactions\n(an allowance, then the deposit), and the second cannot be measured\nbefore the first exists on-chain. When any step cannot be measured the\nwhole total is withheld rather than partially summed, because a cost that\nis quietly too low is worse than no cost at all: it is what the client\ndecides on. `network_cost_unknown_reason` then names the cause.\n\n`network_cost` is what the blockchain network charges to transact. It is\nnot a charge by OZAV.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "yield:read"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "strategy_id",
                  "amount"
                ],
                "properties": {
                  "strategy_id": {
                    "type": "string",
                    "description": "The `id` from `GET /yield/strategies`.",
                    "example": "base-usdc-prime"
                  },
                  "amount": {
                    "type": "string",
                    "description": "Decimal integer string, in the asset's smallest unit. Never a\nfloat: floats lose precision in money silently.\n",
                    "pattern": "^[0-9]{1,32}$",
                    "example": "100000000"
                  }
                }
              },
              "examples": {
                "prever": {
                  "summary": "Preview the yield on 100 USDC before committing",
                  "value": {
                    "strategy_id": "base-usdc-prime",
                    "amount": "100000000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The application priced.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "amount": {
                      "type": "string"
                    },
                    "network_cost": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/NetworkCost"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "network_cost_unknown_reason": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "enum": [
                        "gas_unknown",
                        "price_unknown",
                        "unusable_value",
                        null
                      ],
                      "description": "Why the cost is not stated, when it is not. `null` when it\nis. A null cost with no reason would not distinguish \"the\nnetwork charges nothing\" from \"it could not be measured\".\n"
                    }
                  }
                },
                "examples": {
                  "measured": {
                    "summary": "Both steps measured",
                    "value": {
                      "ok": true,
                      "amount": "100000000",
                      "network_cost": {
                        "amount": "0.60",
                        "currency": "USD"
                      },
                      "network_cost_unknown_reason": null
                    }
                  },
                  "unmeasured": {
                    "summary": "First application, so the deposit step cannot be estimated yet",
                    "value": {
                      "ok": true,
                      "amount": "100000000",
                      "network_cost": null,
                      "network_cost_unknown_reason": "gas_unknown"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/yield/preview \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"strategy_id\": \"base-usdc-prime\",\n    \"amount\": \"100000000\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/yield/preview\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"strategy_id\": \"base-usdc-prime\",\n    \"amount\": \"100000000\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/yield/preview\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"strategy_id\": \"base-usdc-prime\",\n        \"amount\": \"100000000\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/yield/deposits/build": {
      "post": {
        "operationId": "buildYieldDeposit",
        "tags": [
          "Yield"
        ],
        "summary": "Build an unsigned yield application",
        "description": "Returns the ordered, UNSIGNED transactions that apply to a strategy, for\nthe client's own wallet to sign and broadcast.\n\nSELF-CUSTODY ONLY. A client whose funds sit under managed custody does\nnot hold a key, so unsigned transactions would be unusable to it, and\nOZAV does not sign on its behalf here. That request is refused with\n`CUSTODY_MODE_NOT_SELF_CUSTODY` rather than answered with something\nunsignable.\n\nBREAKING CHANGE, 2026-08-24. If `address` is supplied it MUST BE THE\nWALLET ALREADY REGISTERED for this client on that network. Any other\naddress answers `WALLET_ADDRESS_NOT_REGISTERED` (403), even when the\naddress is perfectly valid, and even though this call takes funds OUT.\n\nThe reason is the same one that governs the deposit side, and it is not\nsymmetry for its own sake: the shares being burned are held by the\nregistered wallet. An unregistered address either holds nothing, in which\ncase the transaction reverts after the client pays the gas, or holds a\nposition OZAV never recorded, in which case the withdrawal would not\nreconcile against any book entry.\n\nOne address per network. `POST /wallets` REPLACES the address registered\nfor a network rather than adding to it, so a client that registers a\nsecond address on the same network can no longer build for the first.\n\nOZAV NEITHER SIGNS NOR BROADCASTS on this route. The response states so\nin `signed_by`, and `signer` reports the wallet the transactions were\nbuilt for so it can be checked against the wallet about to sign, before\nsigning rather than after.\n\nThe transactions are ordered, and the order is the signing order: the\ndeposit reverts without the allowance granted by the step before it.\n\nThe vault address appears in `to`, and that is deliberate. Every other\nyield surface withholds it, because the holder never needs it; here the\nclient signs a transaction TO that contract, and asking for a signature\nto a destination we decline to name would be worse than naming it. The\nprotocol NAME is still withheld, on this route as on every other: nothing\nin the signature depends on it.\n\nTwo gates, in series. While the environment gate is closed the route\nanswers `YIELD_BUILD_NOT_ENABLED` (503). Once it is open, building is a\nclosed pilot: an account that is not enrolled answers\n`YIELD_BUILD_NOT_IN_PILOT` (403). If enrolment cannot be read, the route\nanswers `YIELD_PILOT_STATE_UNREADABLE` (503) and never a 403 it cannot\njustify, so retrying is the right response and asking for access is not.\n\n`address` MUST BE THE WALLET ALREADY REGISTERED for this client on that\nnetwork. Any other address answers `WALLET_ADDRESS_NOT_REGISTERED` (403),\neven when the address is perfectly valid. Register the wallet first\n(`POST /wallets`), then build. Omitting `address` uses the registered\nwallet and is the simpler call.\n\nWHY THIS IS A REFUSAL AND NOT A CONVENIENCE. OZAV records what enters a\nvault by watching the addresses it knows. An address it has never been\ntold about is not watched, so the position it holds would exist on-chain\nand not in the book, and a platform-wide exposure limit cannot be\nhonoured from a total that is missing part of itself. Building for an\nunwatched address would make every later limit answer smaller than the\ntruth.\n\nREADS ARE UNAFFECTED. `GET /yield/wallets/{address}/positions` still\naccepts any address: reading public chain state creates no position that\nthe book could miss.\n",
        "x-ozav-entitlement": "self_custody",
        "x-ozav-scopes": [
          "yield:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "strategy_id",
                  "amount"
                ],
                "properties": {
                  "strategy_id": {
                    "type": "string",
                    "example": "base-usdc-prime"
                  },
                  "amount": {
                    "type": "string",
                    "pattern": "^[0-9]{1,32}$",
                    "example": "100000000"
                  }
                }
              },
              "examples": {
                "montar_deposito": {
                  "summary": "Build the unsigned deposit for 100 USDC",
                  "value": {
                    "strategy_id": "base-usdc-prime",
                    "amount": "100000000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The unsigned transactions, in signing order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transactions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "approve",
                              "deposit"
                            ]
                          },
                          "chain": {
                            "type": "string"
                          },
                          "to": {
                            "type": "string",
                            "description": "The contract to call. The token for `approve`, the vault for `deposit`."
                          },
                          "data": {
                            "type": "string",
                            "description": "Unsigned calldata."
                          },
                          "value": {
                            "type": "string",
                            "description": "Native value. Always \"0\" in ERC-20 flows."
                          }
                        }
                      }
                    },
                    "signer": {
                      "type": "string",
                      "description": "The wallet these transactions were built for, derived from\nthe authenticated client. Check it against the wallet about\nto sign.\n"
                    },
                    "signed_by": {
                      "type": "string",
                      "enum": [
                        "client"
                      ],
                      "description": "OZAV does not sign and does not broadcast here."
                    }
                  }
                },
                "examples": {
                  "two_steps": {
                    "summary": "Allowance then deposit",
                    "value": {
                      "transactions": [
                        {
                          "kind": "approve",
                          "chain": "base",
                          "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
                          "data": "0x095ea7b3",
                          "value": "0"
                        },
                        {
                          "kind": "deposit",
                          "chain": "base",
                          "to": "0xeE8F4eC5672F09119b96Ab6fB59C27E1b7e44b61",
                          "data": "0x6e553f65",
                          "value": "0"
                        }
                      ],
                      "signer": "0x1111111111111111111111111111111111111111",
                      "signed_by": "client"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/yield/deposits/build \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"strategy_id\": \"base-usdc-prime\",\n    \"amount\": \"100000000\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/yield/deposits/build\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"strategy_id\": \"base-usdc-prime\",\n    \"amount\": \"100000000\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/yield/deposits/build\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"strategy_id\": \"base-usdc-prime\",\n        \"amount\": \"100000000\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/yield/withdrawals/build": {
      "post": {
        "operationId": "buildYieldWithdrawal",
        "tags": [
          "Yield"
        ],
        "summary": "Build an unsigned yield withdrawal",
        "description": "Returns the UNSIGNED transaction that withdraws from a strategy, for the\nclient's own wallet to sign and broadcast. The other half of the cycle:\noffering only the application would leave a client able to put funds in\nthrough this API and unable to take them out through it.\n\nONE transaction, not two. Burning your own shares needs no allowance.\n\nDenominated in ASSETS, not shares. Honouring an asset-denominated request\nthrough a share-denominated call would mean converting and rounding, and\nthe rounding would land against the holder.\n\nBUILT ONLY IF THE VENUE WILL RELEASE IT. How much can leave right now is\nread before the transaction is built. Without that read the client would\nsign, pay the gas, and watch the chain revert it, which is the worst place\nto find out. `WITHDRAW_EXCEEDS_AVAILABLE` says the balance is still there\nand still earning; `WITHDRAW_LIMIT_UNREADABLE` says the limit could not be\nread at all. Those call for opposite actions, so they are never merged.\n\nSELF-CUSTODY ONLY. A client whose funds sit under managed custody does\nnot hold a key, so unsigned transactions would be unusable to it, and\nOZAV does not sign on its behalf here. That request is refused with\n`CUSTODY_MODE_NOT_SELF_CUSTODY` rather than answered with something\nunsignable.\n\nBREAKING CHANGE, 2026-08-24. If `address` is supplied it MUST BE THE\nWALLET ALREADY REGISTERED for this client on that network. Any other\naddress answers `WALLET_ADDRESS_NOT_REGISTERED` (403), even when the\naddress is perfectly valid, and even though this call takes funds OUT.\n\nThe reason is the same one that governs the deposit side, and it is not\nsymmetry for its own sake: the shares being burned are held by the\nregistered wallet. An unregistered address either holds nothing, in which\ncase the transaction reverts after the client pays the gas, or holds a\nposition OZAV never recorded, in which case the withdrawal would not\nreconcile against any book entry.\n\nOne address per network. `POST /wallets` REPLACES the address registered\nfor a network rather than adding to it, so a client that registers a\nsecond address on the same network can no longer build for the first.\n\nOZAV NEITHER SIGNS NOR BROADCASTS on this route. The response states so\nin `signed_by`, and `signer` reports the wallet the transactions were\nbuilt for so it can be checked against the wallet about to sign, before\nsigning rather than after.\n\nThe transactions are ordered, and the order is the signing order: the\ndeposit reverts without the allowance granted by the step before it.\n\nThe vault address appears in `to`, and that is deliberate. Every other\nyield surface withholds it, because the holder never needs it; here the\nclient signs a transaction TO that contract, and asking for a signature\nto a destination we decline to name would be worse than naming it. The\nprotocol NAME is still withheld, on this route as on every other: nothing\nin the signature depends on it.\n\nTwo gates, in series. While the environment gate is closed the route\nanswers `YIELD_BUILD_NOT_ENABLED` (503). Once it is open, building is a\nclosed pilot: an account that is not enrolled answers\n`YIELD_BUILD_NOT_IN_PILOT` (403). If enrolment cannot be read, the route\nanswers `YIELD_PILOT_STATE_UNREADABLE` (503) and never a 403 it cannot\njustify, so retrying is the right response and asking for access is not.\n",
        "x-ozav-entitlement": "self_custody",
        "x-ozav-scopes": [
          "yield:write"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "strategy_id",
                  "amount"
                ],
                "properties": {
                  "strategy_id": {
                    "type": "string",
                    "example": "base-usdc-prime"
                  },
                  "amount": {
                    "type": "string",
                    "pattern": "^[0-9]{1,32}$",
                    "example": "100000000"
                  }
                }
              },
              "examples": {
                "montar_saque": {
                  "summary": "Build the unsigned withdrawal for 100 USDC",
                  "value": {
                    "strategy_id": "base-usdc-prime",
                    "amount": "100000000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The unsigned withdrawal transaction.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "transactions": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "withdraw"
                            ]
                          },
                          "chain": {
                            "type": "string"
                          },
                          "to": {
                            "type": "string",
                            "description": "The contract to call. The token for `approve`, the vault for `deposit`."
                          },
                          "data": {
                            "type": "string",
                            "description": "Unsigned calldata."
                          },
                          "value": {
                            "type": "string",
                            "description": "Native value. Always \"0\" in ERC-20 flows."
                          }
                        }
                      }
                    },
                    "signer": {
                      "type": "string",
                      "description": "The wallet these transactions were built for, derived from\nthe authenticated client. Check it against the wallet about\nto sign.\n"
                    },
                    "signed_by": {
                      "type": "string",
                      "enum": [
                        "client"
                      ],
                      "description": "OZAV does not sign and does not broadcast here."
                    }
                  }
                },
                "examples": {
                  "one_step": {
                    "summary": "A single withdrawal call",
                    "value": {
                      "transactions": [
                        {
                          "kind": "withdraw",
                          "chain": "base",
                          "to": "0xeE8F4eC5672F09119b96Ab6fB59C27E1b7e44b61",
                          "data": "0xb460af94",
                          "value": "0"
                        }
                      ],
                      "signer": "0x1111111111111111111111111111111111111111",
                      "signed_by": "client"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/yield/withdrawals/build \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"strategy_id\": \"base-usdc-prime\",\n    \"amount\": \"100000000\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/yield/withdrawals/build\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"strategy_id\": \"base-usdc-prime\",\n    \"amount\": \"100000000\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/yield/withdrawals/build\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"strategy_id\": \"base-usdc-prime\",\n        \"amount\": \"100000000\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/network-transfers": {
      "post": {
        "operationId": "createNetworkTransfer",
        "tags": [
          "Network Transfers"
        ],
        "summary": "Move your own USDC to another network",
        "description": "Moves USDC you already hold from one network to another, in the same\nwallet. It is not a payment: origin and destination are the same holder.\n\nNOT CALLABLE TODAY, and the reason is structural rather than a missing\nfeature. The write scope this route requires is absent from the only\nscope bootstrap, so no API key can hold it; and pricing is unconfigured,\nwhich refuses rather than charging zero. Both are opened by a decision.\n\nACCEPTS AND RETURNS 202, NEVER 200. The transfer burns on the origin\nnetwork under a two-party signing quorum, waits for attestation, and only\nthen mints on the destination. That is minutes, and the middle can wait on\na person. A synchronous answer would be a promise this cannot keep, so the\nroute hands back an id and the state is read separately.\n\nONE TRANSFER IN FLIGHT PER SOURCE NETWORK. Two transfers leaving the same\nnetwork compete for the same free balance; one of them reverts on chain\nwith gas already paid. A second request while one is in flight answers 202\nwith the transfer already running and `duplicate: true` - not an error,\nbecause you asked for a transfer and there is one.\n\nTHE HOLDER COMES FROM THE KEY, never from the body. Accepting a holder\nwould let a caller move someone else's balance, and the burn is\nirreversible.\n\nThe amount is a decimal integer string in the asset's smallest unit. A\nJSON number above 2^53 has already lost precision before it arrives, and\nthe amount moved would stop being the amount asked for.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "network_transfers:write"
        ],
        "x-ozav-availability": "coming_soon",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "source_chain",
                  "destination_chain",
                  "asset",
                  "amount"
                ],
                "additionalProperties": false,
                "properties": {
                  "source_chain": {
                    "type": "string",
                    "example": "ethereum"
                  },
                  "destination_chain": {
                    "type": "string",
                    "example": "base"
                  },
                  "asset": {
                    "type": "string",
                    "description": "Only USDC crosses today.",
                    "example": "USDC"
                  },
                  "amount": {
                    "type": "string",
                    "description": "Decimal integer string, smallest unit of `asset`. Never a float.",
                    "pattern": "^[1-9][0-9]{0,77}$",
                    "example": "5000000"
                  }
                }
              },
              "examples": {
                "ethereum_para_base": {
                  "summary": "Move 5 USDC from Ethereum to Base",
                  "value": {
                    "source_chain": "ethereum",
                    "destination_chain": "base",
                    "asset": "USDC",
                    "amount": "5000000"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted. Read the state with `GET /network-transfers/{transfer_id}`.\n`duplicate` is true when a transfer was already in flight for that\nsource network; the transfer returned is that one.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/NetworkTransfer"
                    },
                    "duplicate": {
                      "type": "boolean"
                    }
                  }
                },
                "examples": {
                  "accepted": {
                    "summary": "Accepted",
                    "value": {
                      "data": {
                        "id": "3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607",
                        "status": "pending",
                        "source_chain": "ethereum",
                        "destination_chain": "base",
                        "asset": "USDC",
                        "amount": "5000000",
                        "spread_bps": null,
                        "detail": null,
                        "created_at": "2026-08-24T22:00:00.000Z"
                      },
                      "duplicate": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not hold `network_transfers:write`. This is the primary\ngate and it is not self-service: the scope is outside the bootstrap,\nso `POST /api-keys` cannot grant it. OZAV grants it per account.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "409": {
            "description": "`NETWORK_TRANSFER_PRICE_NOT_CONFIGURED` : no active pricing row applies\nto this account, so the transfer is refused rather than priced at zero.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "`NETWORK_TRANSFER_PRICE_UNREADABLE`: pricing could not be read, so the\ntransfer cannot be accepted. Retry shortly.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/network-transfers \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"source_chain\": \"ethereum\",\n    \"destination_chain\": \"base\",\n    \"asset\": \"USDC\",\n    \"amount\": \"5000000\"\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/network-transfers\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"source_chain\": \"ethereum\",\n    \"destination_chain\": \"base\",\n    \"asset\": \"USDC\",\n    \"amount\": \"5000000\"\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/network-transfers\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"source_chain\": \"ethereum\",\n        \"destination_chain\": \"base\",\n        \"asset\": \"USDC\",\n        \"amount\": \"5000000\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/network-transfers/{transfer_id}": {
      "get": {
        "operationId": "getNetworkTransfer",
        "tags": [
          "Network Transfers"
        ],
        "summary": "Read the state of a network transfer",
        "description": "Returns the state of one transfer.\n\nA transfer that belongs to another holder answers 404, not 403: a 403\nwould confirm the id exists, and that is information about someone else's\naccount.\n\n`in_progress` covers everything between leaving the origin network and\narriving at the destination. While it is in progress, do not retry: a\nsecond request would move the amount a second time.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "network_transfers:read"
        ],
        "x-ozav-availability": "coming_soon",
        "parameters": [
          {
            "name": "transfer_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The transfer state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/NetworkTransfer"
                    }
                  }
                },
                "examples": {
                  "in_progress": {
                    "summary": "Left the origin network, not yet arrived",
                    "value": {
                      "data": {
                        "id": "3f2a1b4c-5d6e-4f70-8a91-b2c3d4e5f607",
                        "status": "in_progress",
                        "source_chain": "ethereum",
                        "destination_chain": "base",
                        "asset": "USDC",
                        "amount": "5000000",
                        "spread_bps": 15,
                        "detail": null,
                        "created_at": "2026-08-24T22:00:00.000Z"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/network-transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\""
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/network-transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1\", {\n  method: \"GET\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n  },\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.get(\n    \"https://developers.ozav.io/v2/network-transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    },\n)\ndata = response.json()"
          }
        ]
      }
    },
    "/swap/quote": {
      "post": {
        "operationId": "quoteSwap",
        "tags": [
          "Swap"
        ],
        "summary": "Price a same-network asset swap",
        "description": "Prices a swap between two assets on the same network, comparing the\nsources available and returning the best net rate.\n\nREAD-ONLY. It is a POST because it carries a body (the pair, the amount,\nthe tolerance), not because it writes. Nothing leaves any wallet.\n\nTHE QUOTE DOES NOT EXECUTE, and `executable` says so rather than leaving\nit to be assumed. There is no route that accepts a quote: moving value\nneeds an execution gate that no source currently passes. The absence of\nthat route is the gate, not an omission.\n\nThe wallet that receives the outgoing asset is derived from the\nauthenticated client. The body carries the pair and the amount, never a\ndestination: accepting one would let a caller price, and later execute, a\nswap whose proceeds land somewhere else.\n\n`slippage_bps` is required and has no silent default. A hidden default\nwould decide, on the client's behalf, how much they accept losing.\n\n`min_amount_out` travels beside `amount_out`, never instead of it. Showing\nonly the expected output would make the tolerance disappear from the\nanswer, and the client would compare an optimistic figure against another\nprovider's already-discounted one.\n\nNOT EVERY NETWORK CARRIES A PAIR. A swap needs both assets settled on\nthe same network, and some networks carry only one of them. A pair no\nsource serves answers a named error, never a zero or an empty quote, so a\npair that cannot be served stays distinguishable from a bad price.\n\n`quotes_compared` and `sources_unavailable` are counts, never lists. That a\ncomparison happened is worth knowing (the best of three is a different\nanswer from the only one that replied), and the number says it without\nnaming who.\n",
        "x-ozav-entitlement": "custody",
        "x-ozav-scopes": [
          "swap:read"
        ],
        "x-ozav-availability": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "chain",
                  "asset_in",
                  "asset_out",
                  "amount_in",
                  "slippage_bps"
                ],
                "properties": {
                  "chain": {
                    "type": "string",
                    "example": "ethereum"
                  },
                  "asset_in": {
                    "type": "string",
                    "example": "USDT"
                  },
                  "asset_out": {
                    "type": "string",
                    "example": "USDC"
                  },
                  "amount_in": {
                    "type": "string",
                    "description": "Decimal integer string, smallest unit of `asset_in`. Never a float.",
                    "pattern": "^[0-9]{1,32}$",
                    "example": "100000000"
                  },
                  "slippage_bps": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "Tolerance in basis points. 50 is 0.50%. Required.",
                    "example": 50
                  }
                }
              },
              "examples": {
                "usdt_para_usdc": {
                  "summary": "Quote 100 USDT into USDC with 0.50% slippage tolerance",
                  "value": {
                    "chain": "ethereum",
                    "asset_in": "USDT",
                    "asset_out": "USDC",
                    "amount_in": "100000000",
                    "slippage_bps": 50
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The swap priced.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "quote": {
                      "type": "object",
                      "properties": {
                        "chain": {
                          "type": "string"
                        },
                        "asset_in": {
                          "type": "string"
                        },
                        "asset_out": {
                          "type": "string"
                        },
                        "amount_in": {
                          "type": "string"
                        },
                        "amount_out": {
                          "type": "string"
                        },
                        "min_amount_out": {
                          "type": "string",
                          "description": "The floor an execution would have to honour, after tolerance."
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "executable": {
                          "type": "boolean",
                          "enum": [
                            false
                          ],
                          "description": "Always false. This surface prices; it does not execute."
                        }
                      }
                    },
                    "quotes_compared": {
                      "type": "integer"
                    },
                    "sources_unavailable": {
                      "type": "integer",
                      "description": "How many sources did not answer. Never silenced: a quote\nchosen from one of three is a different answer from one\nchosen from three of three.\n"
                    }
                  }
                },
                "examples": {
                  "best_of_two": {
                    "summary": "Best of two sources",
                    "value": {
                      "quote": {
                        "chain": "ethereum",
                        "asset_in": "USDT",
                        "asset_out": "USDC",
                        "amount_in": "100000000",
                        "amount_out": "99910000",
                        "min_amount_out": "99410450",
                        "expires_at": "2026-08-08T23:00:00.000Z",
                        "executable": false
                      },
                      "quotes_compared": 1,
                      "sources_unavailable": 0
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "label": "cURL",
            "source": "curl https://developers.ozav.io/v2/swap/quote \\\n  -X POST \\\n  -H \"X-API-Key: ozav_sk_test_0123456789abcdef\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"chain\": \"ethereum\",\n    \"asset_in\": \"USDT\",\n    \"asset_out\": \"USDC\",\n    \"amount_in\": \"100000000\",\n    \"slippage_bps\": 50\n  }'"
          },
          {
            "lang": "JavaScript",
            "label": "Node.js",
            "source": "const response = await fetch(\"https://developers.ozav.io/v2/swap/quote\", {\n  method: \"POST\",\n  headers: {\n    \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    \"chain\": \"ethereum\",\n    \"asset_in\": \"USDT\",\n    \"asset_out\": \"USDC\",\n    \"amount_in\": \"100000000\",\n    \"slippage_bps\": 50\n  }),\n});\nconst data = await response.json();"
          },
          {
            "lang": "Python",
            "label": "Python",
            "source": "import requests\n\nresponse = requests.post(\n    \"https://developers.ozav.io/v2/swap/quote\",\n    headers={\n        \"X-API-Key\": \"ozav_sk_test_0123456789abcdef\",\n        \"Content-Type\": \"application/json\",\n    },\n    json={\n        \"chain\": \"ethereum\",\n        \"asset_in\": \"USDT\",\n        \"asset_out\": \"USDC\",\n        \"amount_in\": \"100000000\",\n        \"slippage_bps\": 50,\n    },\n)\ndata = response.json()"
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Secret key (ozav_sk_test_* or ozav_sk_live_*)."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The same secret key presented as a bearer token."
      }
    },
    "parameters": {
      "CustomerId": {
        "name": "customer_id",
        "in": "path",
        "required": true,
        "description": "The opaque customer id (`ozav_cus_...`).",
        "schema": {
          "type": "string"
        }
      },
      "DocumentId": {
        "name": "document_id",
        "in": "path",
        "required": true,
        "description": "The opaque supporting-document id (`ozav_doc_...`).",
        "schema": {
          "type": "string"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Page size.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 20
        }
      },
      "StartingAfter": {
        "name": "starting_after",
        "in": "query",
        "description": "Opaque cursor returned as `next_cursor` on the previous page.",
        "schema": {
          "type": "string"
        }
      },
      "IdempotencyKeyRequired": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Client-generated unique key: 16-256 characters of `[A-Za-z0-9_-]`.\nSame key + same body replays the original\nresponse (`Idempotent-Replayed: true`); same key + different body\nreturns 409 IDEMPOTENCY_KEY_REUSED; a concurrent duplicate returns 202.\n",
        "schema": {
          "type": "string",
          "minLength": 16,
          "maxLength": 256,
          "pattern": "^[A-Za-z0-9_-]{16,256}$"
        }
      },
      "IdempotencyKeyOptional": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Optional idempotency key (16-256 characters of `[A-Za-z0-9_-]`); same semantics as on money-moving operations.\n",
        "schema": {
          "type": "string",
          "minLength": 16,
          "maxLength": 256,
          "pattern": "^[A-Za-z0-9_-]{16,256}$"
        }
      }
    },
    "headers": {
      "IdempotentReplayed": {
        "description": "Present and `true` when this response is a REPLAY of an earlier request that carried the same `Idempotency-Key` and the same body. The status code and body are byte-for-byte what the first call returned, and nothing happened a second time. Absent on a first execution. Use it to tell \"I created it\" from \"it already existed\", which the status code alone does not tell you.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      },
      "XRequestId": {
        "description": "Correlation id for this exact request. Set on EVERY response, success or error. On an ERROR it is also in the body (`error.request_id`); on a SUCCESS the body carries no id, so this header is the only place it exists. Log it. It is the first thing OZAV support asks for, and a generated client that ignores response headers throws it away.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request is invalid.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "The request is well-formed but cannot be fulfilled - e.g. a `rail` that no settlement-ready provider serves yet (see Rail.x-ozav-rail-availability).\n",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The organization lacks the entitlement (code ENTITLEMENT_REQUIRED) or\nthe key lacks the scope (code INSUFFICIENT_SCOPE). On money-moving\nrequests (POST /transfers, POST /wallets/{wallet_id}/transfers) a\ncompliance hold on the customer answers with code COMPLIANCE_HOLD: the\nhold is reviewed and lifted on OZAV's side; retrying does not clear it.\n",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "NotFound": {
        "description": "The resource does not exist, is not visible to the caller, or the id was\nmalformed for this position (see \"Resource ids\" under Errors). These share\none status on purpose; `code` and `message` tell them apart.",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "Conflict": {
        "description": "Conflict (e.g. IDEMPOTENCY_KEY_REUSED).",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "IdempotencyProcessing": {
        "description": "Accepted - a concurrent request with the same `Idempotency-Key` is still executing; the body is `{\"status\": \"processing\"}`. Retry with the same key once the original completes: a completed request replays its stored response verbatim with the original status code and the `Idempotent-Replayed: true` header. This is NOT the resource - check the status code before deserializing.\n\nIf this `202` repeats instead of resolving, the original request's outcome is UNKNOWN and no stored result is coming - the key stays claimed when a request fails after the resource may already have been written. Stop retrying, reconcile with a `GET` to see whether the resource exists, and use a NEW key if it does not.\n",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "status"
              ],
              "properties": {
                "status": {
                  "type": "string",
                  "const": "processing"
                }
              }
            },
            "examples": {
              "in_flight": {
                "summary": "Same Idempotency-Key still executing - retry with the same key",
                "value": {
                  "status": "processing"
                }
              }
            }
          }
        }
      },
      "NotImplementedYet": {
        "description": "The route exists and validated your request, but the capability behind it is not wired yet - a build-ahead seam, not a client error. Nothing was created. Do not retry on a loop; check `GET /capabilities` for go-live.\n",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "DependencyUnavailable": {
        "description": "A dependency OZAV needs (storage, a resource lookup) could not be reached, so OZAV fails closed rather than guess. Safe to retry with the SAME `Idempotency-Key` where the operation accepts one.\n",
        "headers": {
          "X-Request-ID": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Wait the number of seconds in the `Retry-After` response header\nbefore retrying; retrying sooner just consumes budget without being served.\n",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          "X-Request-ID": {
            "$ref": "#/components/headers/XRequestId"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      }
    },
    "schemas": {
      "NetworkTransfer": {
        "type": "object",
        "description": "One network transfer of your own balance.\n\n`status` is deliberately smaller than the internal state machine. There is\nno \"expired\": internally that means the attestation window passed and will\nbe requested again, and showing the word to a client would say the money\nwas lost about a transfer that is going fine.\n",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "running",
              "submitted",
              "refused",
              "failed",
              "indeterminate"
            ],
            "description": "Six states, and the difference between them is money, so it is\nstated one by one. This mirrors `YieldApplication.status`.\n\n`pending` - accepted and queued. Nothing has been attempted yet.\n\n`running` - being attempted right now. Wait; do not order again.\n\n`submitted` - it left the source network and is on its way. This is\nNOT a failure and NOT a completion: the arrival is confirmed on the\ndestination network, and this field does not yet report it.\n\n`refused` - a gate declined it and NO money left your balance.\n`detail` says which gate. Fixing what it names and ordering again is\nsafe.\n\n`failed` - it was attempted and NOTHING left your balance, and that\nis known. Safe to order again.\n\n`indeterminate` - it stopped WITHOUT knowing whether the amount left\nyour balance. DO NOT order it again: a second attempt may move the\namount twice, and moving between networks cannot be undone. Support\nhas been notified and a person confirms it on the network.\n"
          },
          "source_chain": {
            "type": "string"
          },
          "destination_chain": {
            "type": "string"
          },
          "asset": {
            "type": "string"
          },
          "amount": {
            "type": "string",
            "description": "Decimal integer string, smallest unit of `asset`."
          },
          "spread_bps": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The spread RATE that applies to this transfer, in integer basis\npoints, frozen when it was accepted. It is the rate, not an amount\ndeducted: the transfer moves the full `amount` you requested. Null until then. The network cost is a separate line and is\nnever folded into this.\n"
          },
          "delivery": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "in_progress",
              "completed",
              "failed",
              null
            ],
            "description": "Whether the amount ARRIVED on the destination network. This is a\ndifferent question from `status`, and both matter.\n\n`status` is about the order: accepted, running, submitted, refused,\nfailed, indeterminate. It stops at `submitted` once the amount\nleaves the source network, and stays there.\n\n`delivery` is about the money: `in_progress` while it is crossing,\n`completed` once it is credited on the destination network, and\n`failed` if the crossing ended without crediting.\n\n`null` means there is no crossing yet: the order has not left.\nIt does NOT mean the amount failed to arrive.\n"
          },
          "detail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why, for `refused`, `failed` and `indeterminate`. Null otherwise.\nFor `indeterminate` it carries the do-not-retry text.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "YieldApplication": {
        "type": "object",
        "description": "An instruction to apply custodied balance to a yield strategy. It is\nwhat the client ASKED FOR, not what the client HOLDS. The position\nactually held is read from `GET /yield/positions`, which is built from\nwhat happened on-chain. Confusing the two would make a refused\ninstruction look like a position.\n",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "running",
              "applied",
              "refused",
              "failed",
              "indeterminate"
            ],
            "description": "Three of these are terminal and two of them are safe to retry. The\ndifference is money, so it is stated field by field.\n\n`applied` - confirmed on-chain. `tx_hash` carries the transaction.\n\n`refused` - a gate declined it and NO money moved. `detail` says\nwhich gate. Fixing what it names and ordering again is safe.\n\n`failed` - it was attempted and NOTHING moved, and that is known:\neither the network rejected the transaction, or it never left. Safe\nto order again.\n\n`indeterminate` - it stopped WITHOUT knowing whether the money\nmoved. Either an internal approval is still pending, or a\ntransaction went out and was not yet recorded. DO NOT order it\nagain: a second order can move the balance twice. While an\napplication sits here, further orders for the same strategy answer\n202 with `duplicate: true` and this same application. It clears\nonly after a person checks the chain. `tx_hash` is populated\nwhenever a transaction is known, and it is the pointer to use.\n\n`pending` / `running` - queued, and being worked on.\n"
          },
          "strategy_id": {
            "type": "string"
          },
          "amount": {
            "type": "string",
            "description": "Smallest unit, as a decimal integer string."
          },
          "detail": {
            "type": [
              "string",
              "null"
            ],
            "description": "The cause, for `refused` and `failed`."
          },
          "tx_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present only when a transaction existed; null for `refused`."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "NetworkCost": {
        "type": "object",
        "description": "What the blockchain network charges to carry out an operation. A pass\nthrough cost, not a charge by OZAV, and it is quoted on its own line.\n\nReported at six decimal places when two would round a real cost to\n\"0.00\": a fraction of a cent on a cheap network is a real number, and\nrounding it to zero makes the same false claim that stating no cost\nwould.\n",
        "required": [
          "amount",
          "currency"
        ],
        "properties": {
          "amount": {
            "type": "string",
            "description": "Decimal string. Never a float.",
            "examples": [
              "0.60",
              "0.003215"
            ]
          },
          "currency": {
            "type": "string",
            "enum": [
              "USD"
            ],
            "description": "The currency `amount` is quoted in. Always USD, including when the network charges in its own token - the token cost is converted so the line is comparable."
          }
        }
      },
      "YieldRedemption": {
        "type": "object",
        "description": "What can leave a yield position right now, discriminated by CAUSE.\n\n`balance` says what the position is worth; this says what the venue will\nrelease. The two diverge when the underlying market is fully utilised,\nand the holder needs to know before requesting a withdrawal rather than\nat the point of refusal.\n\nThe three causes call for opposite actions, so they are never collapsed:\n`held` means the funds are there and still earning (wait), `unreadable`\nmeans the limit could not be read (a defect to fix), and only\n`available` states a redeemable amount.\n",
        "required": [
          "kind",
          "available_to_withdraw"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "available",
              "held",
              "unreadable"
            ],
            "description": "WHY the position is in this state, and the three call for opposite actions. Branch on\nthis before reading `available_to_withdraw`.\n\n`available` - a redeemable amount is stated. `held` - the funds are there and still\nearning, but the venue will not release them now; wait and re-read. `unreadable` - the\nlimit could not be read, which is a defect on our side, not a statement about your\nfunds; do not present it as zero.\n"
          },
          "available_to_withdraw": {
            "type": [
              "string",
              "null"
            ],
            "description": "Smallest-unit amount, as a string. `\"0\"` when `kind` is `held`, which\nis a measured zero. `null` when `kind` is `unreadable`, which is not\na zero at all: it means the limit was never read.\n"
          }
        },
        "examples": [
          {
            "kind": "available",
            "available_to_withdraw": "42180550"
          },
          {
            "kind": "held",
            "available_to_withdraw": "0"
          },
          {
            "kind": "unreadable",
            "available_to_withdraw": null
          }
        ]
      },
      "YieldRisk": {
        "type": "object",
        "description": "What can go wrong with a strategy, stated as SHAPE rather than as names.\n\nOZAV never names the protocol, the curator, the issuer, or the contract\naddress on a client surface: you contract with OZAV, not with the venue.\nThat rule left a real gap, because without any of those you also could\nnot tell two strategies apart on risk. This object closes it: it answers\nthe questions that change a decision, and none that identify a party.\n\n`measured_at` is when these facts were read on-chain. A risk profile with\nno date ages without anyone noticing.\n",
        "required": [
          "management",
          "curator_ref",
          "issuer_freeze",
          "redemption_form",
          "principal_guaranteed",
          "smart_contract_risk",
          "measured_at"
        ],
        "properties": {
          "management": {
            "type": "string",
            "enum": [
              "direct",
              "curated"
            ],
            "description": "Whether a team selects the underlying markets (`curated`) or nothing\nsits between the holder and the market (`direct`).\n"
          },
          "curator_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "An OPAQUE, stable label for the curator; `null` when `management` is\n`direct`. It is not a name and never becomes one. It answers exactly\none question: do two strategies share the team that decides? That is\nthe concentration question, and you can answer it without knowing who\nthe team is. Two strategies with the same value concentrate the same\ndecision, however different their networks look.\n"
          },
          "issuer_freeze": {
            "type": "string",
            "enum": [
              "none",
              "contract_rule",
              "unilateral_key"
            ],
            "description": "Whether the instrument's issuer can freeze the position, and how.\n`none`: no such authority exists. `contract_rule`: it exists and runs\nthrough public, auditable logic on-chain. `unilateral_key`: it exists\nas a KEY, and the issuer can freeze any account on its own.\n\nThe SAME instrument can carry a different form on different networks.\nWhen it does, the two are listed as separate strategies and this field\nis what tells them apart.\n"
          },
          "redemption_form": {
            "type": "string",
            "enum": [
              "immediate",
              "by_request"
            ],
            "description": "The form of redemption the instrument supports by design.\n`immediate` redeems against the contract, up to what it releases.\n`by_request` is a request to the issuer, with its own turnaround, and\nnever settles on the spot.\n\nThis is NOT today's liquidity. How much can leave right now is on the\nposition, read live from the chain; a calm day never turns\n`by_request` into `immediate`.\n"
          },
          "principal_guaranteed": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "Always `false`. OZAV does not promise, guarantee, or indicate a\nreturn, and does not repurchase or restore principal. The result is\nwhatever the protocol produces, positive or negative.\n"
          },
          "smart_contract_risk": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "Always `true`. The balance sits in a contract, and a contract can\nfail or be exploited. Admission checks reduce this risk and do not\nremove it.\n"
          },
          "measured_at": {
            "type": "string",
            "format": "date",
            "description": "When these facts were read on-chain, `YYYY-MM-DD`."
          }
        }
      },
      "RealizedApy": {
        "type": "object",
        "description": "A MEASURED rate, derived from the on-chain share price recorded hourly.\nDistinct from the indicative rate, which is refreshed off-chain and\nstates what the venue pays now. Both are published; neither replaces the\nother.\n\nWhen the rate cannot be stated, `known` is `false` and `reason` names the\ncause instead of the field being null or zero.\n",
        "required": [
          "known"
        ],
        "properties": {
          "known": {
            "type": "boolean",
            "description": "Whether a rate could be measured at all. Branch on this FIRST - when it is `false` there is no `bps` and `reason` names the cause. An unknown rate is not zero."
          },
          "bps": {
            "type": "integer",
            "description": "Present only when `known` is true."
          },
          "observed_hours": {
            "type": "integer",
            "description": "How much time the readings actually span. Present only when `known`\nis true. The `7d`/`30d` in the field name is what was REQUESTED; this\nis what was measured.\n"
          },
          "points": {
            "type": "integer",
            "description": "Readings used. Present only when `known` is true."
          },
          "reason": {
            "type": "string",
            "enum": [
              "no_data",
              "window_not_covered",
              "window_too_short",
              "unusable_price",
              "price_went_backwards"
            ],
            "description": "Present only when `known` is false."
          }
        },
        "examples": [
          {
            "known": true,
            "bps": 512,
            "observed_hours": 167,
            "points": 168
          },
          {
            "known": false,
            "reason": "window_not_covered"
          }
        ]
      },
      "DecimalAmount": {
        "type": "string",
        "description": "Decimal string at accounting precision - up to 6 fractional digits.\nNever a float. The value must be greater than zero, with no leading\nzeros in the integer part (a bare `0` integer part is allowed for\nsub-unit values such as \"0.50\") and at most 15 integer digits.\n",
        "pattern": "^(0|[1-9]\\d{0,14})(\\.\\d{1,6})?$",
        "examples": [
          "1250.00",
          "0.50"
        ]
      },
      "TransferDecimalAmount": {
        "type": "string",
        "description": "Decimal string accepted on transfer creation - up to 8 fractional\ndigits. Never a float.\n",
        "pattern": "^\\d+(\\.\\d{1,8})?$",
        "examples": [
          "1250.00",
          "0.12345678"
        ]
      },
      "Currency": {
        "type": "string",
        "description": "Uppercase ISO 4217 code or stablecoin symbol (e.g. BRL, USD, EUR, MXN, USDC, USDT). Uppercase is the canonical form: values are accepted case-insensitively on input but are always uppercase in responses.\n",
        "pattern": "^[A-Z]{3,6}$"
      },
      "Transaction": {
        "type": "object",
        "description": "Amounts use the SAME scale for both `type`s: a decimal string at the currency's human precision. A deposit is stored on-chain as an integer in the asset's smallest unit and is converted here, so `15.750742` never appears as `15750742` next to a transfer's `97.117228`. One movement in the unified feed. `type` tells you which resource it came from, and `id` is that resource's id - fetch the full record with it.",
        "required": [
          "object",
          "id",
          "type",
          "status",
          "amount",
          "currency",
          "created_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "transaction",
            "description": "Resource type discriminator for the envelope; always the literal `transaction`. What KIND of movement it is lives in `type`."
          },
          "id": {
            "type": "string",
            "description": "The underlying resource's id. For `transfer` it is the `ozav_tr_` id; for `deposit` it is the custody deposit id. Opaque either way - store it and pass it back, never parse it."
          },
          "type": {
            "type": "string",
            "enum": [
              "transfer",
              "deposit"
            ],
            "description": "`transfer` - a movement you created through this API. `deposit` - an on-chain credit observed into custody."
          },
          "status": {
            "type": "string",
            "description": "The underlying resource's status, passed through unchanged. Read it together with `type`: the vocabularies are per-resource, so a `deposit` never carries a transfer status and the reverse."
          },
          "amount": {
            "$ref": "#/components/schemas/DecimalAmount"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the movement was recorded (RFC 3339, UTC). This is the field the feed is ordered by."
          },
          "customer_id": {
            "type": "string",
            "description": "Present when the movement belongs to a customer of yours."
          },
          "network": {
            "type": "string",
            "description": "Present on `deposit`; the network the credit arrived on."
          }
        }
      },
      "Rail": {
        "type": "string",
        "description": "Canonical payment-rail vocabulary. Only rails backed by a\nsettlement-ready provider are routable today (see the machine-readable\n`x-ozav-rail-availability`); a preview rail is defined for\nforward-compatibility, but registering a payout destination on one\nreturns `422` until a settlement-ready provider serves that corridor.\n\n| Rail | Corridor | Availability |\n|---|---|---|\n| `pix` | BRL - Brazil instant payments | available |\n| `ach` | USD - US ACH | not offered |\n| `fedwire` | USD - US wire | sandbox only |\n| `sepa` | EUR - SEPA credit transfer | live (behind enablement) |\n| `spei` | MXN - Mexico SPEI | sandbox only |\n| `swift` | Cross-border SWIFT | live (behind enablement) |\n| `faster_payments` | GBP - UK Faster Payments | live (behind enablement) |\n| `co_transfer` | COP - Colombia local transfer | sandbox only |\n\n**`not offered`** means OZAV does not serve that corridor: the value stays\nin the enum so records that already carry it keep reading, and any request\nfor it is refused with `ACCOUNT_RAIL_NOT_AVAILABLE` in every environment,\nsandbox included.\n\n**`live (behind enablement)`** means the corridor is served in production,\nbut not to everyone: opening an account still requires that OZAV has\nenabled the specific customer for foreign-currency named accounts. Until\nthen a `live` request is refused with a named error that says which step\nis missing - not with `ACCOUNT_RAIL_NOT_AVAILABLE`, which would send you\nlooking at the corridor instead of the customer.\n\n**`sandbox only` is a real, usable state, and it is not \"coming soon\".**\nThe corridor is fully exercisable with a `test` key: you can open the\naccount, read its receiving instructions and build against the exact\nresponse shape production will return. What it does not have is a\nsettlement provider behind it, so a `live` key is refused with\n`ACCOUNT_RAIL_NOT_AVAILABLE` rather than opening something that cannot\nreceive. Build against it in sandbox; the request you write does not\nchange when the corridor goes live.\n",
        "enum": [
          "pix",
          "ach",
          "fedwire",
          "sepa",
          "spei",
          "swift",
          "faster_payments",
          "co_transfer"
        ],
        "x-ozav-rail-availability": {
          "pix": "sandbox_active",
          "ach": "coming_soon",
          "fedwire": "sandbox_only",
          "sepa": "live_gated",
          "spei": "sandbox_only",
          "swift": "live_gated",
          "faster_payments": "live_gated",
          "co_transfer": "sandbox_only"
        }
      },
      "Network": {
        "type": "string",
        "description": "Public v2 blockchain network vocabulary for wallet balances and crypto\ntransfer legs. This enum is the wire contract today and intentionally\nlists only networks accepted by public v2 fields now. The custody stack\ncan have additional active networks before those networks are accepted\nby every public API flow; see `x-ozav-network-matrix` for that explicit\nsplit.\n",
        "enum": [
          "ethereum",
          "base",
          "optimism",
          "polygon",
          "avalanche_c_chain",
          "worldchain",
          "unichain",
          "linea",
          "sonic",
          "sei",
          "celo",
          "zksync_era",
          "solana",
          "tron",
          "stellar"
        ],
        "x-ozav-network-availability": {
          "ethereum": "available",
          "base": "deposit_paused",
          "optimism": "deposit_paused",
          "polygon": "opt_in",
          "avalanche_c_chain": "deposit_paused",
          "worldchain": "deposit_paused",
          "unichain": "deposit_paused",
          "linea": "deposit_paused",
          "sonic": "deposit_paused",
          "sei": "deposit_paused",
          "celo": "deposit_paused",
          "zksync_era": "deposit_paused",
          "solana": "available",
          "tron": "available",
          "stellar": "opt_in"
        },
        "x-ozav-network-matrix": {
          "ethereum": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC",
              "USDT"
            ],
            "verified_assets": [
              "USDC",
              "USDT"
            ],
            "native_cross_network": true
          },
          "base": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC"
            ],
            "verified_assets": [
              "USDC"
            ],
            "native_cross_network": true
          },
          "optimism": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC"
            ],
            "verified_assets": [
              "USDC"
            ],
            "native_cross_network": true
          },
          "polygon": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC",
              "USDT"
            ],
            "verified_assets": [
              "USDC",
              "USDT"
            ],
            "native_cross_network": true
          },
          "avalanche_c_chain": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC",
              "USDT"
            ],
            "verified_assets": [
              "USDC",
              "USDT"
            ],
            "native_cross_network": true
          },
          "worldchain": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC"
            ],
            "verified_assets": [
              "USDC"
            ],
            "native_cross_network": true
          },
          "unichain": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC"
            ],
            "verified_assets": [
              "USDC"
            ],
            "native_cross_network": false
          },
          "linea": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC"
            ],
            "verified_assets": [
              "USDC"
            ],
            "native_cross_network": false
          },
          "sonic": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC"
            ],
            "verified_assets": [
              "USDC"
            ],
            "native_cross_network": false
          },
          "sei": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC"
            ],
            "verified_assets": [
              "USDC"
            ],
            "native_cross_network": false
          },
          "celo": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC"
            ],
            "verified_assets": [
              "USDC"
            ],
            "native_cross_network": false
          },
          "zksync_era": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC"
            ],
            "verified_assets": [
              "USDC"
            ],
            "native_cross_network": false
          },
          "solana": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC",
              "USDT"
            ],
            "verified_assets": [
              "USDC",
              "USDT"
            ],
            "native_cross_network": true
          },
          "tron": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDT"
            ],
            "verified_assets": [
              "USDT"
            ],
            "native_cross_network": false
          },
          "stellar": {
            "public_v2": "accepted",
            "custody_stack": "active",
            "assets": [
              "USDC"
            ],
            "verified_assets": [
              "USDC"
            ],
            "native_cross_network": false
          },
          "tempo": {
            "public_v2": "not_accepted",
            "custody_stack": "active",
            "assets": [
              "USDT"
            ],
            "verified_assets": [
              "USDT"
            ],
            "native_cross_network": false
          },
          "arbitrum": {
            "public_v2": "not_accepted",
            "custody_stack": "active",
            "assets": [
              "USDC",
              "USDT"
            ],
            "verified_assets": [
              "USDC",
              "USDT"
            ],
            "native_cross_network": true
          },
          "aptos": {
            "public_v2": "not_accepted",
            "custody_stack": "deferred",
            "assets": [],
            "verified_assets": [],
            "native_cross_network": false
          },
          "sui": {
            "public_v2": "not_accepted",
            "custody_stack": "deferred",
            "assets": [],
            "verified_assets": [],
            "native_cross_network": false
          }
        }
      },
      "Metadata": {
        "type": "object",
        "description": "Free-form string map (max 50 keys).",
        "additionalProperties": {
          "type": "string"
        }
      },
      "MonetaryAmount": {
        "type": "object",
        "description": "An amount paired with its currency, at 6-fractional-digit accounting precision.",
        "required": [
          "amount",
          "currency"
        ],
        "properties": {
          "amount": {
            "$ref": "#/components/schemas/DecimalAmount"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          }
        }
      },
      "TransferMonetaryAmount": {
        "type": "object",
        "required": [
          "amount",
          "currency"
        ],
        "description": "Amount block accepted on transfer creation (8-fractional-digit precision).",
        "properties": {
          "amount": {
            "$ref": "#/components/schemas/TransferDecimalAmount"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          }
        }
      },
      "Address": {
        "type": "object",
        "description": "A postal address. `line1`, `city`, and `country` are required; the rest are optional. Jurisdictions that structure the address (Brazil among them) need it broken out - provide the discrete `number`, `neighborhood`, and (where relevant) `complement` fields, with `line1` then holding the street name alone. Which of these are required to onboard depends on jurisdiction and is reported in `Customer.requirements.missing_fields` (a Brazilian customer needs `line1`, `number`, `neighborhood`, `city`, `state`, and `postal_code`).",
        "required": [
          "line1",
          "city",
          "country"
        ],
        "properties": {
          "line1": {
            "type": "string",
            "description": "Street. Where an address is not structured into discrete components, the building number may be included here; when the `number` field is provided, `line1` is the street name alone."
          },
          "line2": {
            "type": "string",
            "description": "Second address line - unit or suite. Optional. See also `complement`."
          },
          "number": {
            "type": "string",
            "description": "Building / house number, kept separate from the street. Optional in general; required for Brazilian onboarding."
          },
          "neighborhood": {
            "type": "string",
            "description": "Neighborhood or district (e.g. Brazilian `bairro`). Optional in general; required for Brazilian onboarding."
          },
          "complement": {
            "type": "string",
            "description": "Address complement - apartment, block, or reference (e.g. Brazilian `complemento`). Optional."
          },
          "city": {
            "type": "string",
            "description": "City or municipality."
          },
          "state": {
            "type": "string",
            "description": "State or province - an ISO 3166-2 subdivision code where applicable."
          },
          "postal_code": {
            "type": "string",
            "description": "Postal or ZIP code."
          },
          "country": {
            "type": "string",
            "description": "Country as an ISO 3166-1 alpha-2 code (e.g. BR, US).",
            "pattern": "^[A-Z]{2}$"
          }
        }
      },
      "Phone": {
        "type": "object",
        "description": "A contact phone split into an international dialing code and the national number, so onboarding requirements that need the phone's country (Brazil among them) can read it without parsing an E.164 string. Both parts are required when a phone is supplied.",
        "required": [
          "country_code",
          "number"
        ],
        "properties": {
          "country_code": {
            "type": "string",
            "description": "International dialing code, e.g. `+55` or `55`.",
            "pattern": "^\\+?\\d{1,4}$"
          },
          "number": {
            "type": "string",
            "description": "National phone number."
          }
        }
      },
      "HolderTaxId": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "country",
          "type",
          "value"
        ],
        "description": "Tax / registration identifier of the **account holder**, who may be a person or a company.\n\nThe `type` enum is the union of the personal (`cpf`, `ssn`, `nif`, `passport`) and company\n(`cnpj`, `ein`, `company_number`) identifiers, because a payout destination can be held by\neither. Use `other` only when none applies - it is the one value that carries no meaning and\ngets no validation.\n\n`cpf` and `cnpj` are checked against their own check digits and rejected with `400` when the\nnumber does not add up.\n",
        "properties": {
          "country": {
            "type": "string",
            "description": "Issuing country as an ISO 3166-1 alpha-2 code.",
            "pattern": "^[A-Z]{2}$"
          },
          "type": {
            "type": "string",
            "description": "The kind of identifier carried in `value`:\n- `cpf` - Brazilian individual taxpayer registry. Check digits are verified.\n- `cnpj` - Brazilian company registry. Check digits are verified.\n- `ssn` - US Social Security Number.\n- `ein` - US Employer Identification Number.\n- `nif` - Iberian/European tax identification number.\n- `company_number` - company registry number outside Brazil/US.\n- `passport` - passport number.\n- `other` - any identifier not covered above. Carries no meaning and gets no validation.\n",
            "enum": [
              "cpf",
              "cnpj",
              "ssn",
              "ein",
              "nif",
              "company_number",
              "passport",
              "other"
            ]
          },
          "value": {
            "type": "string",
            "description": "The identifier itself. Punctuation is accepted and ignored."
          }
        }
      },
      "TaxId": {
        "type": "object",
        "description": "A national tax or personal identifier for an individual.",
        "required": [
          "country",
          "type",
          "value"
        ],
        "properties": {
          "country": {
            "type": "string",
            "description": "Issuing country as an ISO 3166-1 alpha-2 code.",
            "pattern": "^[A-Z]{2}$"
          },
          "type": {
            "type": "string",
            "description": "The kind of identifier carried in `value`:\n- `cpf` - Brazilian individual taxpayer registry (Cadastro de Pessoas Físicas).\n- `ssn` - US Social Security Number.\n- `nif` - Iberian/European tax identification number (Número de Identificação Fiscal).\n- `passport` - passport number.\n- `other` - any identifier not covered above.\n",
            "enum": [
              "cpf",
              "ssn",
              "nif",
              "passport",
              "other"
            ]
          },
          "value": {
            "type": "string",
            "description": "The identifier itself, as issued. Formatting characters are not required."
          }
        }
      },
      "Registration": {
        "type": "object",
        "description": "A company registration or incorporation identifier for a business.",
        "required": [
          "country",
          "type",
          "value"
        ],
        "properties": {
          "country": {
            "type": "string",
            "description": "Country of registration as an ISO 3166-1 alpha-2 code.",
            "pattern": "^[A-Z]{2}$"
          },
          "type": {
            "type": "string",
            "description": "The kind of registration identifier carried in `value`:\n- `cnpj` - Brazilian company registry (Cadastro Nacional da Pessoa Jurídica).\n- `ein` - US Employer Identification Number.\n- `nif` - Iberian tax/registry number (Número de Identificação Fiscal).\n- `company_number` - a national company/registry number (e.g. Companies House).\n- `other` - any registration identifier not covered above.\n",
            "enum": [
              "cnpj",
              "ein",
              "nif",
              "company_number",
              "other"
            ]
          },
          "value": {
            "type": "string",
            "description": "The registration number itself, as issued."
          }
        }
      },
      "CapabilityKind": {
        "type": "string",
        "description": "Canonical OZAV verification / screening capability taxonomy - the checks\nyou can request for a customer (`POST /verifications`) and the keys of a\ncustomer's `verification.checks` map.\n\nIdentity / KYC (individuals):\n- `kyc_document` - government ID document capture and authentication.\n- `liveness` - confirms the person is physically present (anti-spoofing).\n- `face_match` - the selfie matches the ID document portrait.\n- `proof_of_address` - a document evidencing residential address.\n- `age_estimation` - estimate that the person is above an age threshold.\n\nBusiness / KYB (companies):\n- `kyb` - business registration and standing verification.\n- `ubo` - ultimate-beneficial-owner identification and checks.\n\nAML (name-based):\n- `aml_name_screening` - point-in-time designated-list name screening.\n- `aml_ongoing_monitoring` - continuous re-screening for new matches.\n\nTransaction / on-chain:\n- `wallet_screening` - blockchain-address risk screening.\n- `travel_rule` - originator/beneficiary information exchange for crypto sends.\n\nContactability:\n- `email_verify` - the email address is reachable and controlled by the customer.\n- `phone_verify` - the phone number is reachable and controlled by the customer.\n\nFinancial:\n- `economic_capacity` - assessment of the customer's economic capacity (declared/evidenced financial standing) used to justify transaction limits.\n",
        "enum": [
          "kyc_document",
          "liveness",
          "face_match",
          "proof_of_address",
          "age_estimation",
          "kyb",
          "ubo",
          "aml_name_screening",
          "aml_ongoing_monitoring",
          "wallet_screening",
          "travel_rule",
          "email_verify",
          "phone_verify",
          "economic_capacity"
        ]
      },
      "CheckOutcome": {
        "type": "string",
        "description": "Outcome of a single verification check.\n- `not_started` - the check has not begun.\n- `pending` - the check is in progress; no outcome yet.\n- `pass` - the check succeeded.\n- `fail` - the check did not succeed.\n- `review` - inconclusive; queued for manual review (never silently passed).\n",
        "enum": [
          "pass",
          "fail",
          "review",
          "pending",
          "not_started"
        ]
      },
      "CheckResult": {
        "type": "object",
        "description": "The result of a single verification check.",
        "properties": {
          "outcome": {
            "$ref": "#/components/schemas/CheckOutcome"
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the check reached its outcome (RFC 3339 / ISO 8601, UTC). Present once the check has completed."
          }
        }
      },
      "CustomerStatus": {
        "type": "string",
        "description": "Onboarding / KYC lifecycle state of a customer.\n\n| Value | Meaning |\n|---|---|\n| `pending` | Created; verification has not completed. The customer cannot transact yet. |\n| `in_review` | Checks were submitted and are under (automated or manual) review. |\n| `approved` | Verification passed. The customer may transact once OZAV has everything a payout will need; see the note below. |\n| `rejected` | Verification failed; the customer cannot be onboarded. |\n| `suspended` | A previously active customer has been blocked by a compliance or operational action. |\n\n**`approved` is about the checks, not about being able to operate.**\nOZAV activates a customer only when it also holds everything a payout\nwill need: the holder's name, a valid tax document for the country\ndeclared on it, and an address. A Brazilian company with a valid CNPJ\nis exempt from the address, because the public registry supplies it.\n\nA customer can therefore sit at `approved` and still be refused, with\n`422` on `POST /accounts` and on the money paths. That is deliberate:\nbefore, such a customer entered and only discovered the gap at payout\ntime, with funds already committed.\n\nThe request body cannot resolve it. Send the missing field through the\ncustomer update path, or ask OZAV: the blocked queue names each\ncustomer and the exact field it is waiting on.\n",
        "enum": [
          "pending",
          "in_review",
          "approved",
          "rejected",
          "suspended"
        ]
      },
      "CustomerIndividual": {
        "type": "object",
        "description": "Identity attributes for an `individual` customer or associated person. Every field except `legal_name` is optional at creation so multi-jurisdiction and incremental onboarding are supported - but onboarding a customer to completion needs more. What is still owed is reported per jurisdiction in `Customer.requirements.missing_fields`; for a Brazilian individual that is `mother_name`, `date_of_birth`, `phone`, `email`, a full structured `residential_address`, and `tax_id`.",
        "required": [
          "legal_name"
        ],
        "properties": {
          "legal_name": {
            "type": "string",
            "description": "Full legal name as it appears on the identity document."
          },
          "mother_name": {
            "type": "string",
            "description": "Mother's full name. Required for Brazilian onboarding; optional at creation."
          },
          "date_of_birth": {
            "type": "string",
            "format": "date",
            "description": "Date of birth (ISO 8601 date, YYYY-MM-DD). Validated as a real calendar date, not in the future, at least 18 years ago and not more than 120 - a structurally valid string that is none of those is rejected at create, not accepted and passed on."
          },
          "nationality": {
            "type": "string",
            "pattern": "^[A-Z]{2}$",
            "description": "Nationality as an ISO 3166-1 alpha-2 country code. It feeds risk assessment, and it does NOT change which fields are required: the required-field set is driven by the country on `tax_id` (for an individual) or `registration` (for a business), not by nationality and not by the currency of any account you later open. It does not change WHICH OTHER fields are required (the tax-id country drives that branch), but `nationality` is itself reported in `requirements.missing_fields` until you supply it."
          },
          "phone": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Phone"
              }
            ],
            "description": "Structured contact phone (dialing code + number), distinct from the loose top-level `Customer.phone` string. Required for Brazilian onboarding, which needs the phone's country explicitly."
          },
          "is_pep": {
            "type": "boolean",
            "description": "Politically-exposed-person self-declaration. Optional at creation; treated as `false` when omitted."
          },
          "tax_id": {
            "$ref": "#/components/schemas/TaxId"
          },
          "residential_address": {
            "$ref": "#/components/schemas/Address"
          },
          "occupation": {
            "type": "string",
            "description": "The person's profession or occupation."
          },
          "source_of_funds": {
            "type": "string",
            "description": "Declared origin of the funds the person will move (e.g. salary, savings, sale of property). Reported in `requirements.missing_fields` until supplied."
          },
          "monthly_income_usd": {
            "type": "number",
            "minimum": 0,
            "description": "Self-declared monthly income in USD (amounts are USD-denominated). This is the CAPACITY the expected monthly volume is measured against: without it the economic-capacity assessment has nothing to compare and returns no verdict. `0` is a valid answer (declared no income) and is not treated as absent. Reported in `requirements.missing_fields` until supplied."
          },
          "expected_monthly_volume_usd": {
            "type": "number",
            "minimum": 0,
            "description": "Expected monthly transaction movement in USD. This is the amount the economic-capacity assessment MEASURES; what it measures against is the declared capacity (`monthly_income_usd` for an individual, `annual_revenue_usd` for a business). Supplying this alone yields no assessment. Reported in `requirements.missing_fields` until supplied."
          },
          "account_purpose": {
            "type": "string",
            "description": "Intended use of the account / purpose of the relationship."
          }
        }
      },
      "CustomerBusiness": {
        "type": "object",
        "description": "Identity attributes for a `business` customer. Only `legal_name` and `registration` are required at creation; onboarding a business to completion needs more, reported per jurisdiction in `Customer.requirements.missing_fields`. For a Brazilian business that is `phone`, a business email (the customer `email` serves as the business email), a full structured `registered_address`, `company_type`, and `owners` - at least one associated person owning ≥25%, added via `POST /customers/{customer_id}/associated_persons`.",
        "required": [
          "legal_name",
          "registration"
        ],
        "properties": {
          "legal_name": {
            "type": "string",
            "description": "Registered legal name of the company."
          },
          "trade_name": {
            "type": "string",
            "description": "Trading / doing-business-as name, if different from the legal name."
          },
          "registration": {
            "$ref": "#/components/schemas/Registration"
          },
          "incorporation_date": {
            "type": "string",
            "format": "date",
            "description": "Date the company was incorporated (ISO 8601 date)."
          },
          "legal_structure": {
            "type": "string",
            "description": "Legal form of the entity (e.g. Ltda, LLC, S.A.). Accepted and stored on the customer record, and it is NOT requested in `requirements.missing_fields`: no automated assessment reads it today, so it is offered for completeness rather than asked for. For Brazil the field an onboarding actually reads is `company_type`."
          },
          "company_type": {
            "type": "string",
            "description": "Company classification. For Brazil the onboarding values are `MEI` (microempreendedor individual) and `PJ` (pessoa jurídica). Required for Brazilian onboarding; optional at creation."
          },
          "industry": {
            "type": "string",
            "description": "Primary industry or line of business."
          },
          "website": {
            "type": "string",
            "format": "uri",
            "description": "Company website URL."
          },
          "phone": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Phone"
              }
            ],
            "description": "Structured contact phone for the company (dialing code + number). Required for Brazilian onboarding."
          },
          "registered_address": {
            "$ref": "#/components/schemas/Address"
          },
          "source_of_funds": {
            "type": "string",
            "description": "Declared source of the funds the business will move. Reported in `requirements.missing_fields` until supplied."
          },
          "expected_monthly_volume_usd": {
            "type": "number",
            "minimum": 0,
            "description": "Expected monthly transaction movement in USD. This is the amount the economic-capacity assessment MEASURES; what it measures against is the declared capacity (`monthly_income_usd` for an individual, `annual_revenue_usd` for a business). Supplying this alone yields no assessment. Reported in `requirements.missing_fields` until supplied."
          },
          "annual_revenue_usd": {
            "type": "number",
            "minimum": 0,
            "description": "Declared annual revenue in USD (amounts are USD-denominated). This is the CAPACITY the expected monthly volume is measured against (the assessment reads it as `annual_revenue_usd / 12`): without it the economic-capacity assessment has nothing to compare and returns no verdict. `0` is a valid answer and is not treated as absent. Reported in `requirements.missing_fields` until supplied."
          },
          "is_regulated_activity": {
            "type": "boolean",
            "description": "Whether the business performs a regulated activity. A declared value (including `false`) counts as answered; absent is reported in `requirements.missing_fields`. When `true`, supply `regulator` and `regulated_activity_details`."
          },
          "regulator": {
            "type": "string",
            "description": "Name of the supervising regulator when the activity is regulated."
          },
          "regulated_activity_details": {
            "type": "string",
            "description": "Detail of the regulated activity (licence, scope) when regulated."
          },
          "nature_of_business": {
            "type": "string",
            "description": "Description of what the business does / the services it provides."
          },
          "aml_program": {
            "type": "string",
            "description": "Description of the business's compliance / AML program. Accepted and stored on the customer record, and it is NOT requested in `requirements.missing_fields`: no automated assessment reads it today, so it is offered for completeness rather than asked for. "
          },
          "operating_jurisdictions": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-Z]{2}$"
            },
            "description": "Countries the business operates in (ISO 3166-1 alpha-2 codes). Accepted and stored on the customer record, and it is NOT requested in `requirements.missing_fields`: no automated assessment reads it today, so it is offered for completeness rather than asked for. Sanctions screening does not currently read this list."
          },
          "account_purpose": {
            "type": "string",
            "description": "Intended use of the account / purpose of the relationship."
          }
        }
      },
      "CustomerVerification": {
        "type": "object",
        "description": "The customer's verification posture - the mode applied, the overall status, and per-check results.",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "full",
              "reliance"
            ],
            "description": "How this customer is verified:\n- `full` - OZAV performs the verification checks directly.\n- `reliance` - you attest, under a reliance arrangement, that verification\n  was performed on your side; available only to organizations holding the\n  `kyc_reliance` entitlement (contractual attestation). Reliance mode does\n  not auto-approve a customer in the public API response; the customer's\n  `status` reflects the current onboarding state.\n\nDesignated-list screening is always performed by OZAV regardless of mode.\n\n**When `mode` is absent.** This field is optional, and its absence is meaningful:\nOZAV cannot state a verification posture for this record. Treat it as\n\"posture unknown\". It is NOT equivalent to `full`, and must never be read as one.\n\nTwo different internal situations produce it, and the API does not distinguish them:\neither no verification posture was ever recorded (typical for records created outside\nthe API v2 onboarding path, which carry no canonical verification block), or a posture\nexists internally but is not expressible in this API version. Both mean the same thing\nto you: do not treat this record as verified on the strength of this object.\n\n**Precedence against `status`.** `status` is required and is always present. When\n`mode` is absent, `status` describes the internal onboarding state ONLY and is not a\nverification attestation - a record can read `status: approved` with no `mode`, and\nthat combination must not be used as evidence that verification was performed.\n\n**`requirements.missing_fields` is a lower bound here.** It is still emitted and still\nuseful, but when the canonical block is missing OZAV cannot resolve the record's\njurisdiction, so jurisdiction-specific requirements are not listed. Satisfying every\nfield it names may still leave the record incomplete.\n\nThe record is still returned in full, and other records are unaffected: one\nincomplete record never fails a call.\n"
          },
          "status": {
            "$ref": "#/components/schemas/CustomerStatus"
          },
          "checks": {
            "type": "object",
            "description": "Per-capability check results, keyed by `CapabilityKind`. Empty until checks have run.",
            "additionalProperties": {
              "$ref": "#/components/schemas/CheckResult"
            }
          }
        }
      },
      "CustomerDecision": {
        "type": "object",
        "description": "OZAV's compliance decision on this customer, as it stands - the answer to\n\"was it approved, and if not, why, and whose move is it\".\n\n`status` is finer than the customer's `status`: a customer reads `pending`\nwhile OZAV is reviewing AND while OZAV is waiting on documents from you,\nand only `decision.status` tells those apart (`pending_review` vs\n`information_requested`). Read `requirements.owner` for whose move it is.\n\n`reason_code` is a closed vocabulary (below) and `reason` is its English\nlabel. Free-text analyst notes never travel: a rejection recorded before\nthis vocabulary existed reads `policy`.\n\nPresent on `GET /customers/{customer_id}`, on every record of\n`GET /customers`, and in the `customer.updated` webhook body - which is\nemitted on approval, on rejection, and when information is requested.\n",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pending_review",
              "information_requested",
              "approved",
              "rejected"
            ],
            "description": "- `pending_review` - OZAV is reviewing; nothing is owed by you right now.\n- `information_requested` - OZAV asked for documents or corrections;\n  `requirements.documents_due` lists them with the deadline.\n- `approved` / `rejected` - final.\n"
          },
          "reason_code": {
            "$ref": "#/components/schemas/DecisionReasonCode"
          },
          "reason": {
            "type": "string",
            "description": "English label of `reason_code`. Present only with `reason_code`."
          },
          "decided_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the current decision was recorded. Absent while `pending_review`."
          }
        }
      },
      "CustomerSla": {
        "type": "object",
        "description": "OZAV's response-time commitment on this customer, as a clock: whose move\nit is, until when, and whether the deadline has passed.\n\n- OZAV decides within **48 business hours** (Mon-Fri, Brazilian national\n  holidays excluded) once the record is complete - profile fields filled\n  and the required documents accepted. While that clock runs `owner` is\n  `ozav`.\n- When OZAV asks you for a document or a correction the clock is yours:\n  **30 days** for a document, **15 days** for a correction (`owner`\n  `integrator`; `due_at` mirrors `requirements.documents_due[].due_at`).\n- `owner` is `none` when the decision is final, when no onboarding case is\n  open, or while OZAV is waiting on a third party (that wait counts\n  against no one).\n\nPresent on `GET /customers/{customer_id}`, on every record of\n`GET /customers`, and in the `customer.updated` webhook body.\n",
        "required": [
          "owner",
          "due_at",
          "breached"
        ],
        "properties": {
          "owner": {
            "type": "string",
            "enum": [
              "ozav",
              "integrator",
              "none"
            ],
            "description": "Whose clock is running."
          },
          "due_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the running clock expires. `null` when `owner` is `none`."
          },
          "breached": {
            "type": "boolean",
            "description": "`true` when `due_at` is in the past and the move is still owed. Always `false` when `owner` is `none`."
          }
        }
      },
      "DecisionReasonCode": {
        "type": "string",
        "description": "Why a customer was rejected, or why information was requested. Closed\nvocabulary; each code has a fixed English label in `decision.reason`.\n\nRejection codes: `sanctions_match`, `pep_unmitigated`, `identity_failed`,\n`document_invalid`, `capacity_unproven`, `inconsistent_data`,\n`restricted_jurisdiction`, `no_response`, `fraud_suspected`, `policy`.\n\nRequest codes (also on `requirements.documents_due[].reason_code` and on\nthe `requirement.due` webhook `reason`): `document_missing`,\n`document_invalid`, `data_inconsistent`, `capacity_unproven`,\n`identity_session_needed`, `half_of_limit_reached`.\n",
        "enum": [
          "sanctions_match",
          "pep_unmitigated",
          "identity_failed",
          "document_invalid",
          "capacity_unproven",
          "inconsistent_data",
          "restricted_jurisdiction",
          "no_response",
          "fraud_suspected",
          "policy",
          "document_missing",
          "data_inconsistent",
          "identity_session_needed",
          "half_of_limit_reached"
        ]
      },
      "DocumentDue": {
        "type": "object",
        "description": "A document OZAV's compliance asked for on this customer, with the deadline.",
        "required": [
          "key",
          "label",
          "status"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "The requirement key (e.g. `proof_of_address`, `bank_statement`, `government_id_front`)."
          },
          "doc_type": {
            "$ref": "#/components/schemas/CustomerDocumentType",
            "description": "The `doc_type` to send in `POST /customers/{customer_id}/documents` to satisfy this item - the same vocabulary as `still_required` on the document list. Absent when the requirement is not an API document."
          },
          "label": {
            "type": "string",
            "description": "Human label of the requirement."
          },
          "status": {
            "type": "string",
            "enum": [
              "due",
              "submitted",
              "accepted",
              "rejected"
            ],
            "description": "`due` - owed by you, the clock runs. `submitted` - a file landed and stopped\nthe clock; OZAV still has to accept it. `accepted` - done. `rejected` - the\nfile was refused; a new `due` line follows with the reason.\n"
          },
          "due_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Deadline for `due` items; `null` when none applies."
          },
          "reason_code": {
            "$ref": "#/components/schemas/DecisionReasonCode"
          }
        }
      },
      "Requirements": {
        "type": "object",
        "description": "Outstanding items blocking the customer from advancing through verification / onboarding - the honest \"here's what's still needed\". Present on the create response, on `GET /customers/{customer_id}`, and on every record in `GET /customers`. The list case matters: `CustomerVerification.mode` points here as the signal to read when a verification posture is absent, and that guidance would be unbacked if the field were not guaranteed on the list.",
        "properties": {
          "owner": {
            "type": "string",
            "enum": [
              "integrator",
              "ozav",
              "none"
            ],
            "description": "Whose move it is. `integrator` - OZAV is waiting on you: documents due,\ninformation requested, OR an approved customer with a non-empty `due`\n(see the next field). `ozav` - OZAV is reviewing; nothing to do.\n`none` - the decision is final AND nothing is owed: a rejection, or an\napproval whose `due` is empty.\n\nRead `documents_due`, `due` and `decision.status` together for what is\nactually owed. `missing_fields` alone does NOT flip this to\n`integrator`: `owners` is unreachable for a business (see below).\n\nAn approved customer with a non-empty `due` returns `integrator`, not\n`none` - that combination used to read as \"decision final, nothing to\ndo\" while the account was in fact blocked. A REJECTED customer keeps\n`none` even with a non-empty `due`: the list survives the verdict, and\nno field reverses a rejection.\n"
          },
          "documents_due": {
            "type": "array",
            "description": "What OZAV's compliance asked for on this customer - document, status and\ndeadline - and why (`reason_code`). It shrinks as files land (`submitted`)\nand are accepted. The `requirement.due` webhook announces a new ask; this\nfield is the pull side of the same fact.\n",
            "items": {
              "$ref": "#/components/schemas/DocumentDue"
            }
          },
          "due": {
            "type": "array",
            "description": "What the customer still owes before the ACCOUNT can operate - which is a different question from whether the verification passed. Empty is the normal state; a non-empty `due` on an `approved` customer means the verification is done and the account is still blocked, and `owner` reads `integrator`.\nThe set below is closed and comes from the check that guards withdrawals. The strings are human-readable Portuguese today, not machine tokens, so match them exactly rather than parsing them: `nome do titular` (holder name), `CPF valido` (a valid Brazilian individual tax id), `CNPJ valido` (a valid Brazilian company registration), `documento` (a tax id of any kind, for a non-Brazilian holder), `endereco (logradouro e cidade)` (street and city). A Brazilian company with a valid CNPJ is exempt from the address - the public registry already carries it.\nCapability kinds and document requests can also appear here from the onboarding path; `documents_due` is the structured view of those, with deadline and reason.\nTo clear an entry, send the corresponding field with `PATCH /customers/{customer_id}`. The check re-runs on that call: when it passes, the account activates and this array comes back empty in the same response. It is never removed, only emptied - an absent `due` would be indistinguishable from one nobody computed.",
            "items": {
              "type": "string"
            }
          },
          "due_codes": {
            "type": "array",
            "description": "The same thing as `due`, in stable machine tokens - added because `due` carries human-readable Portuguese and some integrators compare it by equality. `due` is unchanged and stays; read whichever fits your code.\nThe vocabulary is deliberately the SAME as `missing_fields`: `legal_name`, `tax_id`, `registration`, `residential_address`, `registered_address`. Two names for the same thing inside one object would be worse than prose. The token depends on the customer type - for a business, a missing document is `registration` and a missing address is `registered_address`.\nOnly entries that come from the account check get a code. A document request also lands in `due`, and its structured home is `documents_due` (with deadline and reason) - inventing a token for it here would promise vocabulary nothing emits. So `due_codes` can be SHORTER than `due`; it is not positional.",
            "items": {
              "type": "string"
            }
          },
          "missing_fields": {
            "type": "array",
            "description": "Identity/onboarding fields still required for this customer, computed from its own data and JURISDICTION-AWARE from the tax-id / registration country. Each token is the PATH OF THE REQUEST FIELD you must supply, minus the `individual.` / `business.` prefix - so `date_of_birth` means `individual.date_of_birth`, and `residential_address` means `individual.residential_address`. Match them exactly; the set below is closed and generated from the code that emits it.\nFor a Brazilian individual: `mother_name`, `date_of_birth`, `phone`, `email`, `residential_address` (a full structured address), and `tax_id`. For a Brazilian business: `phone`, `email`, `registered_address`, `company_type`, and `owners` (at least one associated person owning ≥25%). Outside Brazil a base set applies (`legal_name`, `tax_id` / `registration`, `email`, and the address of the matching block).\nIndependently of jurisdiction, the risk profile is also reported here until answered: `source_of_funds` and `expected_monthly_volume_usd` for both customer types, plus `is_regulated_activity` and `expected_daily_volume_usd` for a business. A declared `false` counts as answered and is NOT reported.\nThe economic-capacity DECLARATION is reported alongside the expected volume, and the pair is what makes the assessment possible: `monthly_income_usd` for an individual and `annual_revenue_usd` for a business. The expected volume is the amount being measured; the income or revenue is what it is measured AGAINST. Supplying only the volume produces no verdict. `0` is a valid answer for either and is NOT reported as still owed.\nAn individual also owes `occupation`, `nationality`, `is_pep` and `account_purpose`. `is_pep` is a politically-exposed-person self-declaration: an explicit `false` is an answer, an absent field is an unanswered question.\n`regulator` and `regulated_activity_details` are CONDITIONAL: they are reported only while `is_regulated_activity` is `true`. A `false` answer does not ask for them, and an ABSENT `is_regulated_activity` asks for that question itself rather than for its follow-up.\n`expected_daily_volume_usd` is a separate declaration, not the monthly figure divided by anything: account provisioning asks for both, and a derived number would be a value the customer never declared.\nA business also owes `nature_of_business`, `website`, `account_purpose` and `incorporation_date`. They stay OPTIONAL on `POST /customers` - the customer is created and completed over time - but account opening asks for them later, so they are reported here from the start rather than surfacing as a refusal once your own customer is already waiting.\nAn empty array means nothing is outstanding for the tokens that can be cleared. ⚠️ `owners` is currently NOT one of them: it is reported for every business regardless of the associated persons on file, because neither read path supplies the qualifying-owner count. An empty array is therefore unreachable for a business, and gating your flow on it will block every business customer permanently. Read the tokens you care about instead of testing the array for emptiness.",
            "items": {
              "type": "string",
              "enum": [
                "mother_name",
                "expected_daily_volume_usd",
                "nature_of_business",
                "website",
                "account_purpose",
                "incorporation_date",
                "date_of_birth",
                "phone",
                "email",
                "residential_address",
                "registered_address",
                "tax_id",
                "registration",
                "legal_name",
                "company_type",
                "owners",
                "representative",
                "source_of_funds",
                "expected_monthly_volume_usd",
                "is_regulated_activity",
                "monthly_income_usd",
                "annual_revenue_usd",
                "occupation",
                "nationality",
                "is_pep",
                "regulator",
                "regulated_activity_details"
              ]
            }
          }
        }
      },
      "CustomerCreateRequest": {
        "type": "object",
        "required": [
          "type",
          "email"
        ],
        "description": "`email` is required. An `individual` customer additionally requires `individual.tax_id`;\na `business` customer requires `business.registration` (enforced server-side).\n",
        "properties": {
          "type": {
            "type": "string",
            "description": "Whether the customer is a natural person or a company:\n- `individual` - a person; supply the `individual` block (including `individual.tax_id`).\n- `business` - a company; supply the `business` block (including `business.registration`).\n",
            "enum": [
              "individual",
              "business"
            ]
          },
          "external_ref": {
            "type": "string",
            "description": "Your own identifier for this customer, echoed back on the resource. Optional; need not be unique."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Contact email for the customer. Required."
          },
          "phone": {
            "type": "string",
            "description": "Contact phone number in E.164 form (e.g. +5511999998888). Optional."
          },
          "individual": {
            "$ref": "#/components/schemas/CustomerIndividual"
          },
          "business": {
            "$ref": "#/components/schemas/CustomerBusiness"
          },
          "verification_mode": {
            "type": "string",
            "description": "The verification posture for THIS customer (see `CustomerVerification.mode`).\nOptional: omitted, the organization's posture applies - `reliance` when\nyour organization holds the `kyc_reliance` product (the default for API\nintegrators), otherwise `full`.\n- `reliance` - you verified the person and attest it; the end-customer does\n  nothing. OZAV still screens against designated lists and assesses\n  economic capacity, and approves automatically once the profile is\n  complete, the partner document is `received` and screening is clear.\n  OZAV terms (`terms_session_id`) are NOT required. Requires the\n  `kyc_reliance` entitlement (`403 ENTITLEMENT_REQUIRED` otherwise).\n- `full` - OZAV verifies the end-customer in a hosted session\n  (`POST /verifications`: document, liveness, face match).\n",
            "enum": [
              "full",
              "reliance"
            ]
          },
          "monitoring": {
            "type": "boolean",
            "default": false,
            "description": "Enable ongoing designated-list monitoring for this customer."
          },
          "terms_session_id": {
            "type": "string",
            "description": "The id (`ts_…`) of an ACCEPTED terms-of-service session. NOT required\nfor a `reliance` customer (the default posture for API integrators):\nthe contract with the end-customer is yours. For `full` customers, mint\nit with `POST /customers/terms-sessions`, present `terms_url` to your\nend user, record the acceptance with `POST /customers/terms-sessions/accept`,\nthen pass the same id here. Consumed at provisioning for personas that\nsettle or hold funds: provisioning a settlement customer fails closed\nwith `422 TERMS_ACCEPTANCE_REQUIRED` when there is no accepted session,\nand the customer stays unprovisioned (`status: pending`). Not required\nfor compliance-only (verification-only) personas, which are never\nprovisioned at a settlement layer.\n\nSANDBOX: a test key never resolves this field - the attestation is\nsimulated and any value (or none) is accepted. The gate is real only on\n`live`. Do not read a sandbox `201` as proof that your terms flow works.\n"
          },
          "self_custody": {
            "$ref": "#/components/schemas/CustomerSelfCustody"
          },
          "custody_model": {
            "type": "string",
            "enum": [
              "managed",
              "self_custody"
            ],
            "description": "Which custody to provision for THIS customer, when your organization\nhas both custody products enabled.\n\nOmit it and the default applies: `self_custody` when your\norganization has it, otherwise `managed`. That default is why this\nfield exists - before it, holding both products removed the choice\ninstead of offering it, and every customer came out `self_custody`\n(no deposit address, and no custody balance for an off-ramp to\ndebit).\n\nAsking for a model your organization has not contracted returns\n`403 ENTITLEMENT_REQUIRED` naming the missing product - never a\nsilent fallback to the other model. When it resolves to\n`self_custody`, `self_custody.address` is required.\n"
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "CustomerSelfCustody": {
        "type": "object",
        "required": [
          "chain",
          "address"
        ],
        "description": "Bring-your-own-wallet block for self-custody provisioning. Supply the\nend-user-controlled wallet address on the given chain; OZAV records it and\nnever opens a managed wallet for it (the end-user holds the key). Consumed\nonly when your organization holds the `self_custody` product; when the\nprovisioning plan resolves to self-custody and this block is missing, the\ncall fails closed with `422 SELF_CUSTODY_ADDRESS_REQUIRED` and the customer\nstays unprovisioned. The resulting wallet reports\n`custody_model: self_custody`.\n\n**How the plan resolves, and what to do when you did not ask for\nself-custody.** The create request does not choose a custody model. The\nmodel comes from the products your organization holds, and **self-custody\noutranks managed custody when both are enabled** - so an organization with\nboth gets self-custody on every customer, and a request without this block\nis refused even though it never asked for self-custody.\n\nIf you meant managed custody, that refusal is not something you can fix in\nthe request body: ask OZAV to disable `self_custody` for your organization.\nThe `422` message says this too, so the response is self-explanatory.\n\n⚠️ **This block accepts more chains than `POST /wallets` does.** The two\nchain lists are deliberately different: the enum here is the wider one,\nand `POST /wallets` accepts a narrower set. Registering a second address\nlater on one of the chains only this block accepts is refused. If a chain\nmatters to you and it is not in the `POST /wallets` enum, ask before you\ndesign around it.\n",
        "properties": {
          "chain": {
            "type": "string",
            "enum": [
              "ethereum",
              "base",
              "optimism",
              "polygon",
              "avalanche_c_chain",
              "worldchain",
              "unichain",
              "linea",
              "sonic",
              "sei",
              "celo",
              "zksync_era",
              "solana",
              "tron",
              "stellar"
            ],
            "description": "The chain the end-user-controlled address lives on. Public API v2 accepts\nthe active EVM networks listed in `Network`, plus Solana, Tron, and\nStellar. The address must be valid on the chosen chain; OZAV records it\nas-is and never derives or holds a key for it.\n"
          },
          "address": {
            "type": "string",
            "minLength": 1,
            "description": "The end-user-controlled wallet address on `chain`."
          }
        }
      },
      "CustomerUpdateRequest": {
        "type": "object",
        "description": "Mutable customer attributes. Only the fields you include are changed; omitted fields are left untouched.",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "Replacement contact email."
          },
          "phone": {
            "type": "string",
            "description": "Replacement contact phone number (E.164)."
          },
          "individual": {
            "$ref": "#/components/schemas/CustomerIndividual"
          },
          "business": {
            "$ref": "#/components/schemas/CustomerBusiness"
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "TermsSession": {
        "type": "object",
        "description": "A terms-of-service session. Customer provisioning at a settlement partner is fail-closed on one of these being accepted.",
        "required": [
          "object",
          "status",
          "terms_url",
          "terms_version",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The session token, prefixed `ts_`. Returned ONLY on creation - OZAV stores just its hash. Persist it; it is never returned again, not even on an idempotent replay."
          },
          "object": {
            "type": "string",
            "enum": [
              "terms_session"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "accepted"
            ],
            "description": "`pending` until you record the acceptance; only `accepted` satisfies `POST /customers`. There is no hosted page in between, so there is no intermediate state."
          },
          "terms_url": {
            "type": "string",
            "format": "uri",
            "description": "The terms to present to your end user."
          },
          "terms_version": {
            "type": "string",
            "description": "The version in force when the session was created. Acceptance is recorded against THIS version, not the current one."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "accepted_at": {
            "type": "string",
            "format": "date-time",
            "description": "Present once the acceptance has been recorded."
          }
        }
      },
      "TermsSessionAcceptRequest": {
        "type": "object",
        "description": "Your attestation that the end user accepted the terms, with the evidence you collected.",
        "required": [
          "terms_session_id",
          "accepted_by"
        ],
        "properties": {
          "terms_session_id": {
            "type": "string",
            "description": "The `id` returned by `POST /customers/terms-sessions`."
          },
          "accepted_by": {
            "type": "object",
            "description": "Who accepted.",
            "required": [
              "legal_name"
            ],
            "properties": {
              "legal_name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "format": "email"
              }
            }
          },
          "accepted_at": {
            "type": "string",
            "format": "date-time",
            "description": "When they accepted. ISO-8601 with an explicit UTC offset, not in the future. Defaults to the time of this call."
          },
          "ip_address": {
            "type": "string"
          },
          "user_agent": {
            "type": "string"
          }
        }
      },
      "Customer": {
        "type": "object",
        "description": "An end-user you serve - an individual or a business. The anchor resource that every other object references by its `customer_id`.",
        "required": [
          "id",
          "object",
          "type",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque customer identifier, prefixed `ozav_cus_`."
          },
          "object": {
            "type": "string",
            "const": "customer",
            "description": "Resource type discriminator; always the literal `customer`."
          },
          "type": {
            "type": "string",
            "description": "Whether the customer is a natural person (`individual`) or a company (`business`).",
            "enum": [
              "individual",
              "business"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/CustomerStatus"
          },
          "account_status": {
            "type": "string",
            "enum": [
              "active",
              "pending"
            ],
            "description": "Whether the ACCOUNT can operate - a different question from whether the VERIFICATION passed, which is `status`. `active` means named accounts, cards and payouts are available. `pending` means they are not, and the calls that need them refuse with `422 ACCOUNT_NOT_ACTIVATED`.\nAn approved customer can sit at `pending` for two different reasons. When data is missing, `requirements.due` names it and `requirements.owner` reads `integrator` - send the field with `PATCH /customers/{customer_id}` and the account activates in the same response. When `due` is empty, the account is simply not provisioned yet and nothing is owed by you.\nRead this field instead of inferring operability from `status`: an approved customer is not necessarily one that can move money."
          },
          "decision": {
            "$ref": "#/components/schemas/CustomerDecision"
          },
          "sla": {
            "$ref": "#/components/schemas/CustomerSla"
          },
          "external_ref": {
            "type": "string",
            "description": "Your own identifier for this customer, as supplied at creation."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Contact email for the customer."
          },
          "phone": {
            "type": "string",
            "description": "Contact phone number (E.164)."
          },
          "individual": {
            "$ref": "#/components/schemas/CustomerIndividual"
          },
          "business": {
            "$ref": "#/components/schemas/CustomerBusiness"
          },
          "verification": {
            "$ref": "#/components/schemas/CustomerVerification"
          },
          "requirements": {
            "$ref": "#/components/schemas/Requirements"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the customer was created (RFC 3339 / ISO 8601, UTC)."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the customer was last modified (RFC 3339 / ISO 8601, UTC)."
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "CustomerDocumentType": {
        "type": "string",
        "description": "The kind of KYC/KYB document. Jurisdiction-agnostic by design - generic\ncategories, never provider fields or country-specific artifact names -\nbecause this API onboards your customers worldwide.\n\n**What this list is, and what it is not.** It is the vocabulary for\nDECLARING what you are uploading. It is not a per-country list of which\nartifact satisfies which category: whether a given national ID, driving\nlicence or utility bill is accepted for a specific customer is decided\nin review, not by this enum, and OZAV does not publish a per-jurisdiction\nacceptance table. Declare the category that best describes the artifact\nand upload it; if something is missing or unusable you get a named\noutcome on the verification, not a silent rejection.\n\nThe FIELDS a customer must supply are a separate question, answered by\n`requirements.missing_fields` on the customer itself. That set is driven\nby the country on `tax_id` / `registration`, and it does not vary by the\ncurrency of any account you later open.\n\n**Required versus optional, and why nothing here blocks.** For an\ninternational account the receiving institution asks for a defined set of\ncompany information. Some categories below satisfy part of that set and\nsome do not apply to every company - a Brazilian company, for example, has\nno separate memorandum of association, because its Contrato Social is both\nthe constitutive act and the articles.\n\nSo this API accepts every category at any time and refuses none of them for\nbeing absent. Upload what the company has, under the category that\ndescribes it.\n\n**What this API does not do yet, said plainly.** `requirements` on\n`GET /customers/{id}` reports missing onboarding *fields*, not missing\ndocuments - it will not tell you which of the categories above are still\noutstanding. Document review happens at the receiving institution, and\ntoday its outcome reaches you through your account contact, not through\nthis API. Publishing the list here without saying that would suggest a\ncompleteness signal that does not exist.\n\nA company that cannot produce one of these is not blocked from being\ncreated. It is blocked, later and visibly, from having an account opened -\nand the difference matters: the first is a wall at the door, the second is\na list you can work through.\n",
        "enum": [
          "identity_document",
          "selfie",
          "liveness",
          "proof_of_address",
          "company_registration",
          "ownership_structure",
          "financial_statement",
          "source_of_funds",
          "bank_statement",
          "authorized_signature_form",
          "banking_reference",
          "corporate_resolution",
          "good_standing_certificate",
          "source_of_funds_certification",
          "other"
        ]
      },
      "CustomerDocumentUploadRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "doc_type"
        ],
        "properties": {
          "subject_type": {
            "type": "string",
            "enum": [
              "customer",
              "associated_person"
            ],
            "default": "customer",
            "description": "Who this document belongs to. Omit for customer-scoped documents."
          },
          "associated_person_id": {
            "type": "string",
            "description": "Required when subject_type is associated_person. Opaque `ozav_ap_...` id."
          },
          "doc_type": {
            "$ref": "#/components/schemas/CustomerDocumentType"
          },
          "side": {
            "type": "string",
            "enum": [
              "front",
              "back",
              "single"
            ],
            "description": "Document side when applicable."
          },
          "issuing_country": {
            "type": "string",
            "pattern": "^[A-Z]{2}$",
            "description": "ISO 3166-1 alpha-2 issuing country when applicable."
          },
          "mime": {
            "type": "string",
            "description": "MIME type of the file to upload (e.g. application/pdf, image/png). Optional integrity hint.",
            "example": "application/pdf"
          },
          "size_bytes": {
            "type": "integer",
            "minimum": 1,
            "description": "Declared file size in bytes. Optional integrity hint."
          },
          "sha256": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "Lowercase hex SHA-256 of the file. Optional integrity hint; rejected if malformed."
          }
        }
      },
      "CustomerDocument": {
        "type": "object",
        "description": "A customer's uploaded supporting document. Never exposes the raw storage location.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque document id.",
            "example": "ozav_doc_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
          },
          "object": {
            "type": "string",
            "enum": [
              "document"
            ],
            "description": "Resource type discriminator; always the literal `document`."
          },
          "customer_id": {
            "type": "string",
            "description": "The customer this document belongs to."
          },
          "subject_type": {
            "type": "string",
            "enum": [
              "customer",
              "associated_person"
            ],
            "description": "Whose document this is. `customer` - the customer itself. `associated_person` - one of a business customer's associated persons, named in `associated_person_id`."
          },
          "associated_person_id": {
            "type": "string",
            "description": "Present when subject_type is associated_person."
          },
          "doc_type": {
            "$ref": "#/components/schemas/CustomerDocumentType"
          },
          "side": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "front",
              "back",
              "single",
              null
            ],
            "description": "Which face of a two-sided document these bytes are. `single` for a document captured in one image. `null` where the document type has no sides (a PDF statement)."
          },
          "issuing_country": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 3166-1 alpha-2 country that issued the document, when known. `null` when the document type has no issuer (a proof of address) or it was not supplied."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_upload",
              "received",
              "rejected"
            ],
            "description": "`pending_upload` = grant issued, bytes not yet confirmed; `received` = bytes present; `rejected` = failed review."
          },
          "mime": {
            "type": [
              "string",
              "null"
            ],
            "description": "Content type of the stored bytes, as observed on upload. `null` while `status` is `pending_upload`."
          },
          "size_bytes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Size of the stored bytes. `null` while `status` is `pending_upload`."
          },
          "sha256": {
            "type": [
              "string",
              "null"
            ],
            "description": "SHA-256 of the stored bytes, computed by OZAV on receipt - not a value you send. Compare it against your own digest to prove the upload was not truncated. `null` while `status` is `pending_upload`."
          },
          "review": {
            "$ref": "#/components/schemas/DocumentReview"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the upload grant was created (RFC 3339, UTC), not when bytes arrived."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the record last changed (RFC 3339, UTC) - the receipt of bytes and a review outcome both move it."
          }
        }
      },
      "DocumentReview": {
        "type": "object",
        "description": "OZAV's own review of a supporting document (proof of address, bank\nstatement, proof of income, company document). Identity documents and\nselfies are verified in the hosted verification session instead and\nread `not_reviewed` here.\n\n`rejected` comes with a closed `reason_code` and its English label; a\nfresh `requirements.documents_due` line follows with a new deadline. The\n`document.reviewed` webhook carries the same outcome. Omitted (not\n`not_reviewed`) when the review could not be read at response time.\n",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "accepted",
              "rejected",
              "in_review",
              "not_reviewed"
            ],
            "description": "- `not_reviewed` - no review yet (or not a document OZAV reviews).\n- `in_review` - waiting on a compliance officer.\n- `accepted` - the document satisfies the requirement.\n- `rejected` - refused; see `reason_code`. Send a new file.\n"
          },
          "reason_code": {
            "$ref": "#/components/schemas/DocumentReviewReasonCode"
          },
          "reason": {
            "type": "string",
            "description": "English label of `reason_code`. Present only with `reason_code`."
          },
          "reviewed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the current review was recorded. Absent while `not_reviewed`."
          }
        }
      },
      "DocumentReviewReasonCode": {
        "type": "string",
        "description": "Why a document was rejected. Closed vocabulary; each code has a fixed English label in `review.reason`.",
        "enum": [
          "unreadable",
          "not_the_requested_document",
          "holder_mismatch",
          "holder_is_not_the_company",
          "expired",
          "address_mismatch",
          "low_confidence",
          "tampering_signals",
          "missing_period",
          "officer_decision"
        ]
      },
      "CustomerCredential": {
        "type": "object",
        "description": "A portable verification credential (IETF SD-JWT, `vc+sd-jwt`) signed by OZAV. Give `credential` to the end user; keep `id` to look the credential up at `status_url`.",
        "required": [
          "id",
          "object",
          "customer_id",
          "format",
          "credential",
          "kyc_level",
          "method",
          "issued_at",
          "expires_at",
          "jwks_url",
          "status_url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque credential id (the JWT `jti`).",
            "example": "ozav_vc_Q2xhdWRlRmFibGU1MQ"
          },
          "object": {
            "type": "string",
            "enum": [
              "credential"
            ],
            "description": "Resource type discriminator; always the literal `credential`."
          },
          "customer_id": {
            "type": "string",
            "description": "The customer this credential attests."
          },
          "format": {
            "type": "string",
            "enum": [
              "vc+sd-jwt"
            ],
            "description": "Always `vc+sd-jwt` - an IETF SD-JWT with selectively disclosable claims."
          },
          "credential": {
            "type": "string",
            "description": "The compact SD-JWT, `<jwt>~<disclosure>~...~`. Hand it to the end user; do not store it as personal data - the disclosures carry the CPF, name and birth date in clear."
          },
          "kyc_level": {
            "type": "integer",
            "enum": [
              2,
              3
            ],
            "description": "`3` = hosted session (document, liveness, face match); `2` = document-based identification (reliance or manual review)."
          },
          "method": {
            "type": "string",
            "enum": [
              "hosted_session",
              "reliance",
              "manual_review"
            ],
            "description": "How OZAV verified the customer."
          },
          "issued_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this credential was issued (RFC 3339, UTC)."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this credential stops being valid regardless of the customer's status."
          },
          "jwks_url": {
            "type": "string",
            "format": "uri",
            "description": "Where OZAV publishes the signing keys (JWKS, `kid`-addressed). Public; no API key."
          },
          "status_url": {
            "type": "string",
            "format": "uri",
            "description": "Public status of this credential, `valid` | `revoked` | `expired` | `unknown`. No API key; no personal data."
          }
        }
      },
      "CustomerDocumentUpload": {
        "description": "A newly created document plus the short-lived signed URL to PUT the file bytes to.",
        "allOf": [
          {
            "$ref": "#/components/schemas/CustomerDocument"
          },
          {
            "type": "object",
            "properties": {
              "upload_url": {
                "type": "string",
                "description": "Short-lived upload URL - `PUT` the raw file bytes here. Not returned by the list\nendpoint.\n\n**Send your API key on this PUT as well.** The embedded `upload_token` scopes the\ngrant to this one customer/document pair and expires on its own, but it does NOT\nreplace authentication - a `PUT` without `X-API-Key` (or `Authorization: Bearer`)\nis rejected with `MISSING_API_KEY`.\n\nBecause the key is required, **this URL is for your server, not your browser**: do\nnot hand it to an end user's device, which would mean shipping your secret key with\nit. Upload the bytes from your backend.\n"
              },
              "upload_expires_at": {
                "type": "string",
                "format": "date-time",
                "description": "When `upload_url` expires."
              }
            }
          }
        ]
      },
      "AssociatedPersonCreateRequest": {
        "type": "object",
        "description": "A person associated with a business customer (UBO, director, officer, or signer), added for KYB.",
        "required": [
          "roles",
          "individual"
        ],
        "properties": {
          "roles": {
            "type": "array",
            "minItems": 1,
            "description": "One or more roles this person holds in the business:\n- `ubo` - ultimate beneficial owner (an owner above the ownership threshold).\n- `director` - a member of the board or a statutory director.\n- `officer` - an executive officer (e.g. CEO, CFO).\n- `authorized_signer` - authorized to act or sign on the account.\n",
            "items": {
              "type": "string",
              "enum": [
                "ubo",
                "director",
                "officer",
                "authorized_signer"
              ]
            }
          },
          "ownership_percent": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Percentage of the business this person owns, 0-100. Relevant for `ubo` roles."
          },
          "individual": {
            "$ref": "#/components/schemas/CustomerIndividual"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Contact email for this person. Optional."
          }
        }
      },
      "AssociatedPersonUpdateRequest": {
        "type": "object",
        "description": "Mutable attributes of an associated person. Only the fields you include are changed.",
        "properties": {
          "roles": {
            "type": "array",
            "description": "Replacement set of roles - one or more of `ubo`, `director`, `officer`, `authorized_signer`.",
            "items": {
              "type": "string",
              "enum": [
                "ubo",
                "director",
                "officer",
                "authorized_signer"
              ]
            }
          },
          "ownership_percent": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Revised ownership percentage, 0-100."
          },
          "individual": {
            "$ref": "#/components/schemas/CustomerIndividual"
          }
        }
      },
      "AssociatedPerson": {
        "type": "object",
        "description": "A person linked to a business customer for KYB purposes.\n\n`verification.mode` is **inherited from the customer**, never set per\nperson: an associated person has no verification posture of its own, so\na customer onboarded under `reliance` reports `reliance` here too.\n\nWhen the customer has no recorded posture, `mode` is **omitted**. Absent\nis not `full`: it means OZAV has no posture on record for that customer,\nand `status` must not be read as a verification attestation while `mode`\nis absent.\n",
        "required": [
          "id",
          "object",
          "roles"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque associated-person identifier, prefixed `ozav_ap_`."
          },
          "object": {
            "type": "string",
            "const": "associated_person",
            "description": "Resource type discriminator; always the literal `associated_person`."
          },
          "customer_id": {
            "type": "string",
            "description": "The `ozav_cus_` id of the business customer this person belongs to."
          },
          "roles": {
            "type": "array",
            "description": "Roles this person holds - one or more of `ubo`, `director`, `officer`,\n`authorized_signer` (see `AssociatedPersonCreateRequest.roles`).\n",
            "items": {
              "type": "string",
              "enum": [
                "ubo",
                "director",
                "officer",
                "authorized_signer"
              ]
            }
          },
          "ownership_percent": {
            "type": "number",
            "description": "Percentage of the business this person owns, 0-100."
          },
          "individual": {
            "$ref": "#/components/schemas/CustomerIndividual"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Contact email for this person."
          },
          "verification": {
            "$ref": "#/components/schemas/CustomerVerification"
          },
          "requirements": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Requirements"
              }
            ],
            "description": "What is still missing **on this person** before an account can be opened\nfor the customer they belong to.\n\nA business account is refused while a qualifying owner is incomplete, and\nthat refusal names the person. This field is the same information delivered\nwhen you create the person, instead of when you request the account.\n\nThe set depends on the customer's **jurisdiction**, never on a provider. A\nperson attached to a Brazilian company owes the full KYB set:\n`individual.legal_name`, `individual.date_of_birth`, `individual.tax_id`,\n`email`, `individual.phone` and `individual.residential_address`, with the\naddress complete: `line1`, `number`, `neighborhood`, `city`, `state` and\n`postal_code`. Outside Brazil only `individual.legal_name` and\n`individual.date_of_birth` are owed.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the associated person was added (RFC 3339 / ISO 8601, UTC)."
          }
        }
      },
      "ExchangeConditions": {
        "type": "object",
        "description": "Public exchange conditions. `spread_bps` is the additional\nclient-selected spread for the quote, in integer basis points. OZAV cost,\nfloor, margin, split, and provider analytics are internal and are not\nserialized in public API responses.\n",
        "required": [
          "spread_bps"
        ],
        "properties": {
          "spread_bps": {
            "type": "integer",
            "minimum": 0,
            "description": "Additional client-selected spread, in integer basis points (100 bps = 1%)."
          }
        }
      },
      "Quote": {
        "type": "object",
        "description": "An FX quote - opaque, short-lived, and carrying the applied exchange conditions. Execute it by creating a transfer that references its `id`.",
        "required": [
          "id",
          "object",
          "rate_type",
          "rate",
          "source",
          "target",
          "exchange_conditions",
          "expires_at",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque quote identifier, prefixed `ozav_q_`."
          },
          "object": {
            "type": "string",
            "const": "quote",
            "description": "Resource type discriminator; always the literal `quote`."
          },
          "customer_id": {
            "type": "string",
            "description": "Opaque customer id (prefixed `ozav_cus_`) the quote was created for."
          },
          "rate_type": {
            "type": "string",
            "description": "How the `rate` behaves until execution:\n- `floating` - indicative; the rate re-derives at execution time.\n- `locked` - held fixed until `expires_at`.\n",
            "enum": [
              "floating",
              "locked"
            ]
          },
          "rate": {
            "type": "string",
            "description": "The FX rate as a decimal string - target-currency units per one source unit."
          },
          "source": {
            "$ref": "#/components/schemas/MonetaryAmount"
          },
          "target": {
            "$ref": "#/components/schemas/MonetaryAmount"
          },
          "exchange_conditions": {
            "$ref": "#/components/schemas/ExchangeConditions"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the quote expires (RFC 3339 / ISO 8601, UTC). Null for floating quotes with no venue-side expiry."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the quote was created (RFC 3339 / ISO 8601, UTC)."
          },
          "facility": {
            "$ref": "#/components/schemas/FacilityPreview"
          },
          "facility_unavailable": {
            "$ref": "#/components/schemas/FacilityUnavailable"
          }
        }
      },
      "FacilityUnavailable": {
        "type": "object",
        "description": "**Why the rate-lock preview is not available for this account.**\n\nReturned instead of `facility` when the request set `facility_preview: true` and the facility cannot be offered. It never fails the quote - the quote is the product - it only explains the absence.\n\n🔴 **Read this instead of guessing at a missing `facility` block.** Branch on `reason`, which is the stable slug and the same vocabulary as `details.reason` on `422 FACILITY_LOCK_UNAVAILABLE`, so one mapping serves the preview and the refusal.\n\n⚠️ **This preview runs the offer gates only** - the ones that decide whether a lock can be SHOWN. It deliberately does not evaluate collateral or your contractual limits, because those are resolved at acceptance, inside the lock. So `collateral_insufficient` is **not** a value you will see here: it appears on `POST /transfers` instead. The table below is the complete set this endpoint can return.\n\n| `reason` | What it means | What to do | |---|---|---| | `client_not_exempt` | Your organization has not contracted the facility. | Talk to your account manager - no request will succeed until it is contracted. | | `no_client` | The key does not resolve to an organization the facility knows. | Contact support with the request id. | | `no_terms` / `terms_inactive` / `no_terms_version` / `invalid_terms` | The signed schedule (`Termo`) is missing, not active, or incomplete. | A contract step, not a parameter. Talk to your account manager. | | `past_client_cutoff` | Locking now is past today's cutoff. | Try earlier on the next business day. | | `client_window_exceeds_settlement` | Today's window does not fit settlement. | Ask for a shorter window, or try earlier. | | `unwind_not_ready` | The facility is not being offered right now. Not about your account. | Retry later; if it persists, contact support. | | `pool_config_missing` / `execution_margin_not_configured` / `settlement_mode_not_configured` | A platform setting the facility needs is missing. Not about your account. | Retry later; if it persists, contact support with the request id. | | `invalid_amounts` | The amounts on the quote cannot be used to price a lock. | Check `source.amount` and the target; if they look right, contact support. | | `previa_indisponivel` | The preview could not be computed. Not about your account, and not about your request. | The quote itself is valid and usable. If you need the preview, contact support with the `request_id` of this response. |\n",
        "readOnly": true,
        "required": [
          "reason",
          "message"
        ],
        "properties": {
          "reason": {
            "type": "string",
            "description": "Stable, machine-readable slug. **Branch on this, never on `message`.**"
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation, safe to show an operator. May change without notice, and is currently served in Portuguese on some paths."
          }
        }
      },
      "FacilityPreview": {
        "type": "object",
        "description": "**The FX rate lock conditions (`trava de câmbio`), previewed alongside the quote.**\n\nReturned ONLY when the request set `facility_preview: true` AND your organization has the facility contracted and operable. Its `terms_version` is the value `POST /transfers` accepts as `facility_terms_version`, and this is the only place it comes from.\n\n⚠️ **Absence is the answer, not an error.** A client without the facility gets a normal `201` quote with this block missing and no explanation - the preview never fails a quote, because the quote is the product. To learn WHY, attempt the transfer: it refuses loudly with `422 FACILITY_LOCK_UNAVAILABLE` and a machine-readable `details.reason`.\n\n⚠️ **These numbers are contractual, not settings.** The penalty, the minimum charge, the reserve percentage, the client window and the cutoff come from the schedule (`Termo`) your organization signed. They are shown here because an acceptance over conditions the payer never saw does not support a charge later.\n",
        "readOnly": true,
        "properties": {
          "terms_version": {
            "type": "string",
            "description": "The version of the signed schedule these conditions come from. Echo it back as `facility_terms_version` on `POST /transfers`. It must match byte for byte."
          },
          "locked_rate": {
            "type": "number",
            "description": "The rate that would be locked, at this instant."
          },
          "source_amount": {
            "type": "string",
            "description": "How much is being locked, in the source unit."
          },
          "source_asset": {
            "type": "string",
            "description": "The source unit of `source_amount`."
          },
          "expected_amount_minor": {
            "type": "string",
            "description": "What the customer receives at `locked_rate`, in the MINOR unit of `expected_amount_currency` (centavos for BRL). A string, because the value can exceed what a JSON number holds exactly."
          },
          "expected_amount_currency": {
            "type": "string",
            "description": "The currency of `expected_amount_minor`. Always `BRL` today; the field is present so the name never has to change when other payout currencies open."
          },
          "client_deadline_at": {
            "type": "string",
            "format": "date-time",
            "description": "By when the customer must deliver (pay the Pix, or hand over the asset). Missing it is what triggers the penalty below."
          },
          "settlement_deadline_at": {
            "type": "string",
            "format": "date-time",
            "description": "OZAV's own settlement deadline."
          },
          "settlement_mode": {
            "type": "string",
            "enum": [
              "T0",
              "T1"
            ],
            "description": "Settlement day for this lock."
          },
          "cutoff_time_brt": {
            "type": "string",
            "description": "The daily cutoff, in BRT, from the schedule. ⚠️ **It and the client window cut each other short: the deadline is always whichever comes FIRST.** With a 120-minute window and a 17:00 cutoff, locking at 16:00 gives 60 minutes, not 120, and locking at 16:30 gives 30. That is the schedule, not a defect - but it is the first thing that surprises an integrator, so branch on `client_deadline_at`, never on the window length."
          },
          "charge_rule": {
            "type": "string",
            "description": "How the charge is composed if the customer does not deliver in time. Cumulative, and the penalty is the large part."
          },
          "penalty_pct": {
            "type": "number",
            "description": "Penalty, as a fraction. `0.05` is 5%."
          },
          "minimum_charge_usd": {
            "type": "number",
            "description": "The minimum charge per unsettled operation, in USD. **Not basis points** - an absolute amount."
          },
          "reserve_pct": {
            "type": "number",
            "description": "The reserve (collateral) required, as a fraction of the OPEN POSITION - aggregate, including the new one, not per transaction. `0.02` is 2%. It must be constituted BEFORE acceptance and maintained while positions are open; if the ratio falls below the schedule's minimum, a top-up call is triggered."
          },
          "holiday_risk": {
            "type": "boolean",
            "description": "`true` when the deadline crosses a business-day boundary the calculation cannot resolve against a holiday calendar. Treat the deadline as approximate and confirm."
          },
          "capacity_guaranteed": {
            "type": "boolean",
            "const": false,
            "description": "**Always `false`.** This preview prices the lock; it does not reserve room under the shared ceiling, which is resolved at acceptance. Reading headroom here and deciding later is exactly the non-atomic pre-check the pool exists to prevent - two clients would both see \"there is room\" and both proceed."
          }
        }
      },
      "TransferStatus": {
        "type": "string",
        "description": "Canonical transfer state machine. A successful FX transfer walks\n`awaiting_payment → payment_confirmed → converting → payout_initiated →\ncompleted`; the remaining states are terminal failures or attention states.\n\n| Value | Meaning |\n|---|---|\n| `awaiting_payment` | Transfer created; awaiting the customer's inbound payment. |\n| `payment_confirmed` | The inbound payment has been confirmed. |\n| `converting` | Funds are in flight and the FX conversion is underway (not yet confirmed). |\n| `payout_initiated` | The payout to the destination has been initiated. |\n| `completed` | Terminal success - funds delivered to the destination. |\n| `failed` | Terminal failure - no funds delivered. |\n| `cancelled` | Terminal - the transfer was cancelled. |\n| `expired` | Terminal - the transfer expired before completing. |\n| `needs_review` | Stuck and needs attention (e.g. underfunded, returned, in review); not terminal. |\n| `awaiting_approval` | A custody send held by YOUR approval quorum (`GET /wallets/{id}/policy` → `approvals`). Nothing has been signed; approver keys decide it at `POST /approvals/{id}/approve`. `approval_id` names the approval. Not terminal. |\n",
        "enum": [
          "awaiting_payment",
          "payment_confirmed",
          "converting",
          "payout_initiated",
          "completed",
          "failed",
          "cancelled",
          "expired",
          "needs_review",
          "awaiting_approval"
        ]
      },
      "SettlementStatus": {
        "type": "string",
        "description": "Canonical per-leg settlement status. A transfer carries an independent\n`source_leg` and `destination_leg`, each with one of these values.\n\n| Value | Meaning |\n|---|---|\n| `pending` | Accepted; awaiting the payer's funds. Non-terminal. |\n| `processing` | Funds in flight (received, or payout initiated). Non-terminal. |\n| `needs_review` | Stuck / needs attention (underfunded, in review, returned). Non-terminal. |\n| `completed` | Terminal success - the leg settled. |\n| `failed` | Terminal failure - the leg did not settle. |\n| `cancelled` | Terminal - the leg was cancelled. |\n",
        "enum": [
          "pending",
          "processing",
          "needs_review",
          "completed",
          "failed",
          "cancelled"
        ]
      },
      "SettlementLeg": {
        "type": "object",
        "description": "The settlement state of one side of a transfer (source or destination). Read-only.",
        "readOnly": true,
        "properties": {
          "status": {
            "$ref": "#/components/schemas/SettlementStatus"
          },
          "settled_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this leg reached terminal completion (RFC 3339 / ISO 8601, UTC). Present only once the leg is `completed`."
          }
        }
      },
      "TransferSource": {
        "description": "Where a transfer's funds come from. Discriminated by `type`; the value selects the flow together with the destination.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/TransferSourceFiatPayment"
          },
          {
            "$ref": "#/components/schemas/TransferSourceWallet"
          },
          {
            "$ref": "#/components/schemas/TransferSourceCryptoPayment"
          },
          {
            "$ref": "#/components/schemas/TransferSourceAccount"
          }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "fiat_payment": "#/components/schemas/TransferSourceFiatPayment",
            "wallet": "#/components/schemas/TransferSourceWallet",
            "crypto_payment": "#/components/schemas/TransferSourceCryptoPayment",
            "account": "#/components/schemas/TransferSourceAccount"
          }
        }
      },
      "TransferSourceAccount": {
        "type": "object",
        "description": "The customer's OWN named fiat account (`ozav_acc_`) as the SOURCE of an on-ramp: the\ncustomer already deposited into that account, and the order is funded from its balance.\nNo payment instructions are returned, because there is nothing left to pay.\n\nThe account must be active and known to its institution, and the amount must fit the\nbalance that is not already committed to the customer's other open orders. Choose\n`fiat_payment` instead when the customer has NOT deposited yet and needs payment\ninstructions to send the money.\n",
        "required": [
          "type",
          "account_id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "account",
            "description": "Discriminator for an own-account source; always the literal `account`."
          },
          "account_id": {
            "type": "string",
            "description": "The `ozav_acc_` id of the customer's own named fiat account."
          }
        }
      },
      "PixPaymentInstructions": {
        "type": "object",
        "description": "Provider-neutral Pix payment instructions for a specific pay-in transaction.",
        "required": [
          "rail",
          "intent_id"
        ],
        "properties": {
          "rail": {
            "type": "string",
            "const": "pix",
            "description": "The payment rail; always `pix`."
          },
          "intent_id": {
            "type": "string",
            "description": "OZAV's provider-neutral id for the Pix pay-in intent. **Opaque - store it and echo it back, never parse it.** Unlike the `ozav_*` resource ids, this one carries no prefix and is a UUID today; the format is not part of the contract and may change. Use it to correlate the Pix payment with this transfer, not to infer anything about the intent."
          },
          "br_code": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pix copy-and-paste BR Code for this specific transaction."
          },
          "qr_code_image": {
            "type": [
              "string",
              "null"
            ],
            "description": "QR code image payload, when available."
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the Pix instructions expire."
          }
        }
      },
      "TransferSourceFiatPayment": {
        "type": "object",
        "description": "A fiat payment the customer sends in (the FX / on-ramp source leg).",
        "required": [
          "type",
          "currency"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "fiat_payment",
            "description": "Discriminator for a fiat-payment source; always the literal `fiat_payment`."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "rail": {
            "$ref": "#/components/schemas/Rail"
          },
          "virtual_account_id": {
            "type": "string",
            "description": "Optional named virtual-account id to receive the payment into (roadmap; omit for the standard flow)."
          },
          "payment_instructions": {
            "readOnly": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PixPaymentInstructions"
              }
            ],
            "description": "Returned for pay-in transfers after OZAV creates transaction-specific payment instructions. Clients display the Pix BR Code / QR to the payer; callers do not submit this field.\n"
          }
        }
      },
      "TransferSourceWallet": {
        "type": "object",
        "description": "A managed wallet as the source of funds (off-ramp / wallet transfer).",
        "required": [
          "type",
          "wallet_id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "wallet",
            "description": "Discriminator for a wallet source; always the literal `wallet`."
          },
          "wallet_id": {
            "type": "string",
            "description": "The `ozav_wal_` id of the source wallet."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "chain": {
            "type": "string",
            "description": "The network the funds leave from, when the wallet serves more than one. Same rule as on a wallet destination: with the FX rate lock it is what the hedge follows, and an absent network refuses the lock instead of guessing."
          }
        }
      },
      "TransferSourceCryptoPayment": {
        "type": "object",
        "description": "An inbound crypto deposit as the source (crypto receive).",
        "required": [
          "type",
          "deposit_address_id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "crypto_payment",
            "description": "Discriminator for a crypto-payment source; always the literal `crypto_payment`."
          },
          "deposit_address_id": {
            "type": "string",
            "description": "The id of the crypto deposit address the funds arrive at."
          }
        }
      },
      "TransferDestination": {
        "description": "Where a transfer's funds go. Discriminated by `type`; the value selects the flow together with the source.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/TransferDestinationPayout"
          },
          {
            "$ref": "#/components/schemas/TransferDestinationWallet"
          },
          {
            "$ref": "#/components/schemas/TransferDestinationCryptoAddress"
          },
          {
            "$ref": "#/components/schemas/TransferDestinationAccount"
          }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "payout_destination": "#/components/schemas/TransferDestinationPayout",
            "wallet": "#/components/schemas/TransferDestinationWallet",
            "crypto_address": "#/components/schemas/TransferDestinationCryptoAddress",
            "account": "#/components/schemas/TransferDestinationAccount"
          }
        }
      },
      "TransferDestinationPayout": {
        "type": "object",
        "description": "A registered payout destination as the target (FX / off-ramp destination leg).",
        "required": [
          "type",
          "payout_destination_id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "payout_destination",
            "description": "Discriminator for a payout-destination target; always the literal `payout_destination`."
          },
          "payout_destination_id": {
            "type": "string",
            "description": "The `ozav_pd_` id of the registered payout destination."
          }
        }
      },
      "TransferDestinationAccount": {
        "type": "object",
        "description": "The customer's OWN named fiat account (`ozav_acc_`) as the target of an off-ramp: the\nstablecoin is sold and the fiat is credited to that account, where it stays until the\ncustomer moves it. Available for USD, EUR and GBP accounts that are active and known to\nthe account's institution; the quote's target currency must be the account's currency.\n\nChoose this when the customer wants to hold a fiat balance rather than pay a third\nparty; choose `payout_destination` to pay out to a registered external account.\n",
        "required": [
          "type",
          "account_id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "account",
            "description": "Discriminator for an own-account target; always the literal `account`."
          },
          "account_id": {
            "type": "string",
            "description": "The `ozav_acc_` id of the customer's own named fiat account."
          }
        }
      },
      "TransferDestinationWallet": {
        "type": "object",
        "description": "A managed wallet as the target (on-ramp / wallet transfer).",
        "required": [
          "type",
          "wallet_id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "const": "wallet",
            "description": "Discriminator for a wallet target; always the literal `wallet`."
          },
          "wallet_id": {
            "type": "string",
            "description": "The `ozav_wal_` id of the destination wallet."
          },
          "chain": {
            "type": "string",
            "description": "The network the credit lands on, when the wallet serves more than one.\n\n🔴 **Optional in general, but effectively REQUIRED when you use the FX rate lock.** The hedge OZAV buys is withdrawn to treasury on the SAME network as the delivery - buying on one and delivering on another would leave a crossing for someone to resolve with the exposure already open. With no network supplied there is nothing to follow, so the lock is **refused** (`facility.reason = sem_destino_de_tesouraria`) rather than guessed. The transfer itself still proceeds; only the lock is lost.\n\n⚠️ The refusal can also mean the pair itself has no registered treasury destination (`USDC:tron`, for instance, is not a pair the counterparty sustains). The message names the `asset:network` pair, so read it before assuming the field is at fault."
          }
        }
      },
      "TransferDestinationCryptoAddress": {
        "type": "object",
        "required": [
          "type",
          "address",
          "network",
          "currency"
        ],
        "description": "External crypto destination; triggers wallet screening and travel-rule requirements by policy.",
        "properties": {
          "type": {
            "type": "string",
            "const": "crypto_address",
            "description": "Discriminator for an external crypto-address target; always the literal `crypto_address`."
          },
          "address": {
            "type": "string",
            "description": "The external blockchain address to send to; must be valid on `network`."
          },
          "network": {
            "$ref": "#/components/schemas/Network"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          }
        }
      },
      "PayoutDocumentUpload": {
        "type": "object",
        "description": "The supporting document for a payout, sent inline as base64.\n\nThe bytes travel in the request rather than through a signed upload URL\nbecause this is one small file per payout, and a single call leaves no\nwindow in which the document is registered and its bytes are absent,\na state that reads as \"sent\" and is not.\n",
        "additionalProperties": false,
        "required": [
          "file_name",
          "content_type",
          "content_base64"
        ],
        "properties": {
          "file_name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "Name shown to compliance reviewers.",
            "example": "invoice-2026-08.pdf"
          },
          "content_type": {
            "type": "string",
            "description": "PDF only. The receiving institution named PDF as the format for the\ninvoice, and a file its compliance review cannot accept comes back as\na request for information days later, by email. Refusing here is an\nimmediate message to whoever still has the file in hand.\n",
            "enum": [
              "application/pdf"
            ]
          },
          "content_base64": {
            "type": "string",
            "minLength": 1,
            "description": "The file, base64-encoded, without a `data:` prefix. Maximum 10 MB\ndecoded. Content that does not decode, or decodes to zero bytes, is\nrefused with `400 DOCUMENT_CONTENT_INVALID` before anything is stored.\n",
            "example": "JVBERi0xLjQKJeLjz9MK"
          }
        }
      },
      "PayoutDocument": {
        "type": "object",
        "description": "The stored supporting document for a payout.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object": {
            "type": "string",
            "enum": [
              "payout_document"
            ]
          },
          "transfer_id": {
            "type": "string",
            "description": "The transfer this document supports (`ozav_tr_...`)."
          },
          "file_name": {
            "type": "string"
          },
          "content_type": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer",
            "description": "Decoded size."
          },
          "provider_status": {
            "type": "string",
            "enum": [
              "enviado",
              "recusado",
              "sem_transacao",
              "sem_codigo"
            ],
            "description": "What happened when the document was forwarded to the receiving\ninstitution.\n\n**Absent means it has not been attempted yet**, which is always the\ncase on upload: forwarding happens only after the payment order\nexists, because the document attaches to a transaction that does not\nexist while you are uploading. Absence is not a failure, and it is\nnot a promise of success either. It is the third state, and\ncollapsing it into either of the other two is what makes a payout\nthat is quietly waiting on a document look like one that settled.\n"
          }
        }
      },
      "TravelRule": {
        "type": "object",
        "description": "Originator and beneficiary information for a crypto send, required by BCB\nResolution 520/2025 when value leaves custody to an external address.\n\n**Read the \"Required for an external crypto send\" list below before building\nthis object.** The fields marked required *there* are not the same as the ones\nmarked required *here*: this schema is shared with paths where Travel Rule is\nsupplementary, so it stays permissive. The mandatory set is enforced on\n`destination.type = crypto_address`, and a request that misses any of it is\nrefused with `422 TRAVEL_RULE_REQUIRED` and a `details.missing_fields` array\nnaming each one.\n\n### Required for an external crypto send\n\n| field | rule |\n|---|---|\n| `originator.name` | non-blank |\n| `originator.identity_documents[].type` | at least one entry must be accepted for the customer's country - see the table below |\n| `originator.identity_documents[].number` | non-blank |\n| `originator.identity_documents[].issuing_country` | required on **every** entry |\n| `originator.address.street_line_1` | non-blank (`street` and `line1` are accepted aliases) |\n| `originator.address.city` | non-blank |\n| `originator.address.country` | non-blank |\n| `beneficiary.name` | non-blank |\n| `beneficiary.wallet_type` | `external` or `self_custodied` (`hosted` is rejected on this path) |\n| `beneficiary.wallet_attested_ownership_at` | ISO-8601 timestamp of when the holder attested ownership; must not be in the future |\n| `beneficiary.is_self` | boolean; `true` when the destination belongs to your own customer |\n\n#### Accepted originator identity documents, by country\n\nThe requirement follows the customer's country of residence as held on the\ncustomer record - **not** the country you send inside `travel_rule`, which is\nstored but never selects the regime. A country not listed below uses the\ndefault set.\n\nTo be precise about where that country comes from: you declare it when you\ncreate the customer (`individual.tax_id.country`, or the address), and you can\ncorrect it later with `PATCH /customers/{id}`. Both paths refuse a jurisdiction\nOZAV does not serve. So the regime is not selectable *per request*, but it does\nfollow what you declared for that customer - keep it accurate.\n\n| country | accepted `identity_documents[].type` |\n|---|---|\n| `BR` | `cpf`, `cnpj` |\n| `NG` | `bvn`, `nin`, `passport`, `national_id` |\n| *(default)* | `passport`, `national_id` |\n\nUntil 30 Aug 2026 this gate required `cpf` or `cnpj` from every originator,\nin both environments, which made an external send impossible for a customer\noutside Brazil. If your customer's country is not listed and the default set\ndoes not fit its national identifiers, tell us and it gets modelled - the\nlist is data, not code.\n\nA destination that is an OZAV wallet id (`ozav_wal_...`) is exempt: the value\nstays in custody and both sides are already verified.\n\nValues inside `originator` and `beneficiary` must be plain strings - do not\nembed internal or third-party references.\n",
        "properties": {
          "originator": {
            "$ref": "#/components/schemas/TravelRuleParty"
          },
          "beneficiary": {
            "$ref": "#/components/schemas/TravelRuleParty"
          }
        }
      },
      "TravelRuleParty": {
        "type": "object",
        "description": "One side of a Travel Rule record. `required` here lists only what every path\nneeds; see `TravelRule` for the stricter set an external crypto send enforces.\n",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Full legal name of the party, as it appears on their verified identity."
          },
          "account": {
            "type": "string",
            "description": "The wallet address or account reference for this party."
          },
          "identity_documents": {
            "type": "array",
            "description": "Identity documents for this party. On an external crypto send the originator\nmust carry at least one entry whose type is accepted for the customer's\ncountry (see \"Accepted originator identity documents, by country\").\nAccepted under the legacy name\n`identifying_information` as well; both normalize to the same record.\n",
            "items": {
              "type": "object",
              "required": [
                "type",
                "number",
                "issuing_country"
              ],
              "properties": {
                "type": {
                  "type": "string",
                  "description": "Document kind, lowercase. Which kinds satisfy the originator\nrequirement depends on the customer's country - see \"Accepted\noriginator identity documents, by country\" above. Other kinds are\naccepted and stored but do not satisfy it on their own.\n",
                  "example": "cpf"
                },
                "number": {
                  "type": "string",
                  "description": "The document number. Non-blank.",
                  "example": "11144477735"
                },
                "issuing_country": {
                  "type": "string",
                  "description": "ISO 3166-1 alpha-2 country that issued the document.",
                  "example": "BR"
                }
              }
            }
          },
          "address": {
            "type": "object",
            "description": "Postal address. Required for the originator on an external crypto send.\n`street_line_1` is the canonical key; `street` and `line1` are accepted aliases.\n",
            "properties": {
              "street_line_1": {
                "type": "string"
              },
              "street_line_2": {
                "type": "string"
              },
              "city": {
                "type": "string"
              },
              "state": {
                "type": "string"
              },
              "postal_code": {
                "type": "string"
              },
              "country": {
                "type": "string",
                "description": "ISO 3166-1 alpha-2.",
                "example": "BR"
              }
            }
          },
          "wallet_type": {
            "type": "string",
            "enum": [
              "hosted",
              "external",
              "self_custodied"
            ],
            "description": "How the party's wallet is held. On an external crypto send the beneficiary\nmust be `external` or `self_custodied`; `hosted` is rejected there.\n"
          },
          "wallet_attested_ownership_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the holder attested ownership of the wallet."
          },
          "is_self": {
            "type": "boolean",
            "description": "True when this party is your own customer (a send to their own external\nwallet) rather than a third party.\n"
          }
        }
      },
      "Transfer": {
        "type": "object",
        "description": "A money-movement resource. One resource serves every flow; the `(source.type, destination.type)` pair selects it.",
        "required": [
          "id",
          "object",
          "status",
          "source",
          "destination",
          "amounts"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque transfer identifier, prefixed `ozav_tr_`."
          },
          "object": {
            "type": "string",
            "const": "transfer",
            "description": "Resource type discriminator; always the literal `transfer`."
          },
          "customer_id": {
            "type": "string",
            "description": "The `ozav_cus_` id of the customer this transfer is for."
          },
          "quote_id": {
            "type": "string",
            "description": "The `ozav_q_` id of the executed quote, when the transfer references one."
          },
          "status": {
            "$ref": "#/components/schemas/TransferStatus"
          },
          "approval_id": {
            "type": "string",
            "description": "Present only while `status` is `awaiting_approval`: the `ozav_apr_` id your approver keys decide at `POST /approvals/{approval_id}/approve`. Absent once decided - the trail lives on the approval, not here.\n"
          },
          "status_detail": {
            "type": "object",
            "description": "Optional provider-neutral detail explaining the current status. Present for a transfer that needs OZAV reconciliation, and for one that ended badly (`failed`, `cancelled`, `expired`) once the reason was recorded; `code` is a catalog error code you can branch on. Absent when no reason was recorded, which is the case for transfers created before 2026-09-06; treat absence as \"not stated\", never as \"no reason\". It never contains provider names, native rail error payloads, or a compliance verdict.",
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable OZAV status detail code."
              },
              "message": {
                "type": "string",
                "description": "Human-readable provider-neutral status detail."
              },
              "retryable": {
                "type": "boolean",
                "description": "Whether the client should retry the same action without OZAV intervention."
              }
            }
          },
          "source": {
            "$ref": "#/components/schemas/TransferSource"
          },
          "destination": {
            "$ref": "#/components/schemas/TransferDestination"
          },
          "source_leg": {
            "$ref": "#/components/schemas/SettlementLeg"
          },
          "destination_leg": {
            "$ref": "#/components/schemas/SettlementLeg"
          },
          "amounts": {
            "type": "object",
            "description": "The amounts moved and the FX rate applied to this transfer.",
            "properties": {
              "source": {
                "$ref": "#/components/schemas/MonetaryAmount"
              },
              "target": {
                "$ref": "#/components/schemas/MonetaryAmount"
              },
              "rate": {
                "type": "string",
                "description": "The applied FX rate as a decimal string (target units per source unit). Absent for a same-currency move."
              }
            }
          },
          "exchange_conditions": {
            "$ref": "#/components/schemas/ExchangeConditions"
          },
          "travel_rule": {
            "$ref": "#/components/schemas/TravelRule"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the transfer was created (RFC 3339 / ISO 8601, UTC)."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the transfer was last updated (RFC 3339 / ISO 8601, UTC)."
          },
          "completed_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the transfer reached a terminal state (RFC 3339 / ISO 8601, UTC). Present once `status` is terminal."
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          },
          "facility": {
            "$ref": "#/components/schemas/FacilityOutcome"
          }
        }
      },
      "FacilityOutcome": {
        "type": "object",
        "description": "**What happened to the FX rate lock on this transfer.**\n\nPresent on the `201` of a create that sent `facility_terms_accepted: true`, **on BRL/Pix flows**. A transfer created without the lock has no `facility` key at all, and its body is byte-for-byte what it has always been.\n\n⚠️ **Two limits worth knowing before you build on it.**\n\n**It is a create-time outcome, not transfer state.** `GET /transfers/{id}` does not return it, and neither do the `transfer.*` webhook events after the first one. If you need it later, store it when you receive the `201`.\n\n**The lock applies to BRL/Pix on-ramp and off-ramp only.** The fields are accepted on every combination, but on a `crypto_address` destination, a wallet-to-wallet move, or the BRS redemption corridor they have no effect and no `facility` block comes back. Read the absence of the block on those flows as \"no lock\", not as \"locked\".\n\n🔴 **Read `locked` before you present a price to the payer.** A lock can be REQUESTED and still not happen, and those cases return `201` on purpose: the Pix charge is valid and the customer will pay it, so refusing would lose the operation for a condition that only costs the lock. The consequence is that this block is the only thing standing between \"the price is fixed\" and \"the price is whatever the market does before you pay\".\n\n| `state` | `locked` | What it means | What to do | |---|---|---|---| | `locked` | `true` | The rate is fixed until `deadline_at`. | Deliver before the deadline. Missing it triggers the schedule's penalty. | | `not_locked` | `false` | The lock did not happen and there is nothing to correct - typically the shared ceiling had no room, or a withdrawal was covered by the balance already delivered. The transfer is valid. | Nothing, but the conversion uses the rate at execution time. Tell whoever is paying. | | `refused` | `false` | The lock was refused by account state - conditions changed between the quote and the acceptance. | Read `reason`, fix it, and accept again. | | `undetermined` | `false` | 🔴 The hedge MAY have executed; nothing was recorded as locked. | Do NOT retry and do NOT treat as failed. Reconcile with `GET /transfers/{id}`. |\n",
        "readOnly": true,
        "required": [
          "locked",
          "state"
        ],
        "properties": {
          "locked": {
            "type": "boolean",
            "description": "Whether the rate is actually fixed. The one field a payer's decision hangs on."
          },
          "state": {
            "type": "string",
            "enum": [
              "locked",
              "not_locked",
              "refused",
              "undetermined"
            ],
            "description": "The outcome. ⚠️ `not_locked` and `refused` are NOT synonyms and must not be collapsed: `refused` means the caller has something to correct, `not_locked` means there is nothing to do and the operation proceeds unlocked."
          },
          "reason": {
            "type": "string",
            "description": "Machine-readable slug for why the lock did not happen. Absent when `locked` is `true`. **This is the stable discriminator - branch on it, never on `message`.** Same vocabulary as `details.reason` on `422 FACILITY_LOCK_UNAVAILABLE`, so one mapping serves both the refusal and the degraded path."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation. May change without notice, and is currently served in Portuguese on some paths. Never parse it."
          },
          "deadline_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the locked rate expires. Present only when `locked` is `true`. This is the deadline as RECORDED, not one recalculated client-side - a second, locally computed deadline is not the one a charge would be based on."
          },
          "amount": {
            "type": "string",
            "description": "The amount locked. Present only when `locked` is `true`."
          },
          "currency": {
            "type": "string",
            "description": "The currency of `amount`. Present only when `locked` is `true`."
          }
        }
      },
      "AccountPayoutCreateRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "A payment out of the customer's own named account, in the account's\ncurrency, to one of the customer's registered payout destinations.\n",
        "required": [
          "amount",
          "destination",
          "reference"
        ],
        "properties": {
          "amount": {
            "type": "string",
            "description": "Decimal string with exactly two fractional digits (`\"50.00\"`). Never a float.",
            "pattern": "^(0|[1-9]\\d{0,14})\\.\\d{2}$",
            "examples": [
              "50.00"
            ]
          },
          "destination": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "type",
              "payout_destination_id"
            ],
            "properties": {
              "type": {
                "type": "string",
                "const": "payout_destination"
              },
              "payout_destination_id": {
                "type": "string",
                "description": "The `ozav_pd_` id of a destination registered for the SAME customer, in the same currency as the account."
              }
            }
          },
          "reference": {
            "type": "string",
            "description": "1-35 characters (letters, digits, space, `.`, `_`, `/`, `-`). Your\nidempotency lever: the same `reference` with the same `amount` on the\nsame account is the same payout. Also sent to the receiving bank as\nremittance information.\n",
            "pattern": "^[A-Za-z0-9 ._/-]{1,35}$"
          },
          "purpose": {
            "type": "string",
            "maxLength": 140,
            "description": "Optional purpose code or short description, passed to the payment institution when the rail carries one."
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "AccountPayout": {
        "type": "object",
        "additionalProperties": false,
        "description": "A payment sent out of a customer's named account. `status` follows the\npayment institution: `accepted` means the order was taken, not that the\nmoney arrived; `settled` means it did; `rejected` is final; `pending`\nmeans the institution has not classified the outcome yet - read the\naccount's `/movements` rather than assuming either way.\n",
        "required": [
          "id",
          "object",
          "account_id",
          "destination",
          "amount",
          "currency",
          "reference",
          "status",
          "metadata"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque payout id, prefixed `ozav_apo_`. Stable for the same `reference` and `amount`."
          },
          "object": {
            "type": "string",
            "const": "account_payout"
          },
          "account_id": {
            "type": "string",
            "description": "The `ozav_acc_` id the money left."
          },
          "destination": {
            "type": "object",
            "required": [
              "type",
              "payout_destination_id"
            ],
            "properties": {
              "type": {
                "type": "string",
                "const": "payout_destination"
              },
              "payout_destination_id": {
                "type": "string"
              }
            }
          },
          "amount": {
            "type": "string",
            "description": "Decimal string with two fractional digits."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "reference": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "accepted",
              "settled",
              "rejected"
            ]
          },
          "purpose": {
            "type": "string"
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "AccountCreateRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Request a named fiat receiving account for a customer whose supporting\ndocuments are already on file. `approved` is the verification result and\nis NOT sufficient on its own: this endpoint reads the documents, and on\n`live` it refuses with `422 ACCOUNT_PROFILE_INCOMPLETE` naming each one\nstill missing. Send them with `POST /customers/{customer_id}/documents`\nfirst. The sandbox does not enforce this, so an account opening there\nproves the request is well-formed, never that the customer is ready.\n\nThe URL stays the same across currencies, countries, rails, and\nproviders; availability is selected by the account rail matrix below.\n",
        "x-ozav-account-rail-availability": [
          {
            "currency": "BRL",
            "country": "BR",
            "rail": "pix"
          },
          {
            "currency": "USD",
            "country": "US",
            "rail": "ach"
          },
          {
            "currency": "USD",
            "country": "US",
            "rail": "fedwire"
          },
          {
            "currency": "EUR",
            "country": "EU",
            "rail": "sepa"
          },
          {
            "currency": "GBP",
            "country": "GB",
            "rail": "faster_payments"
          },
          {
            "currency": "MXN",
            "country": "MX",
            "rail": "spei"
          },
          {
            "currency": "COP",
            "country": "CO",
            "rail": "co_transfer"
          },
          {
            "currency": "USD",
            "country": "US",
            "rail": "swift"
          },
          {
            "currency": "USD",
            "country": "GB",
            "rail": "swift"
          }
        ],
        "required": [
          "customer_id",
          "account_type",
          "currency",
          "country",
          "rail"
        ],
        "properties": {
          "customer_id": {
            "type": "string",
            "description": "The `ozav_cus_` id of the approved customer receiving the account."
          },
          "account_type": {
            "type": "string",
            "const": "named_fiat_account",
            "description": "Account kind. API v2 exposes named fiat accounts here."
          },
          "currency": {
            "type": "string",
            "enum": [
              "BRL",
              "USD",
              "EUR",
              "GBP",
              "MXN",
              "COP"
            ],
            "description": "Account currency. BRL is the first sandbox-active fiat account currency."
          },
          "country": {
            "type": "string",
            "pattern": "^[A-Z]{2}$",
            "description": "Account country or region code. BR is active for BRL/Pix sandbox accounts."
          },
          "rail": {
            "$ref": "#/components/schemas/Rail",
            "description": "Receiving rail for this account."
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "AccountStatus": {
        "type": "string",
        "description": "Public lifecycle state for a named account:\n- `pending` - opening is in progress or waiting for review.\n- `active` - receiving instructions can be shown to the customer.\n- `suspended` - account is blocked or deactivated.\n- `failed` - opening did not complete.\n",
        "enum": [
          "pending",
          "active",
          "suspended",
          "failed"
        ]
      },
      "AccountPixInstructions": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "key",
          "key_type"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Pix key customers can use to receive BRL into this account."
          },
          "key_type": {
            "type": "string",
            "enum": [
              "cpf",
              "cnpj",
              "email",
              "phone",
              "random",
              "evp"
            ],
            "description": "What kind of Pix key `key` is. Payer apps validate the key against its type, so publish the pair together - a correct key presented as the wrong type is refused."
          }
        }
      },
      "AccountBankInstructions": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "bank_name": {
            "type": "string",
            "description": "Bank or payment institution name, when assigned."
          },
          "branch": {
            "type": "string",
            "description": "Branch number, when assigned."
          },
          "account_number": {
            "type": "string",
            "description": "Account number, including check digit when assigned."
          },
          "routing_number": {
            "type": "string",
            "description": "Domestic routing number for supported rails such as ACH."
          },
          "iban": {
            "type": "string",
            "description": "IBAN for supported rails such as SEPA."
          },
          "bic": {
            "type": "string",
            "description": "BIC or SWIFT code, when assigned."
          },
          "sort_code": {
            "type": "string",
            "description": "UK sort code (six digits, usually shown as `00-00-00`). Present on `faster_payments`\naccounts.\n"
          },
          "clabe": {
            "type": "string",
            "description": "Mexican CLABE - 18 digits, including its own check digit. Present on `spei` accounts.\n"
          },
          "bre_b_key": {
            "type": "string",
            "description": "Colombian Bre-B key (the payer-facing alias). Present on `co_transfer` accounts.\n"
          },
          "account_type": {
            "type": "string",
            "enum": [
              "payment"
            ],
            "description": "The account category to state on a transfer form. `payment` is a payment account (conta de pagamento), which some payer flows require to be selected explicitly instead of checking or savings."
          }
        }
      },
      "AccountReceivingInstructions": {
        "type": "object",
        "additionalProperties": false,
        "description": "Rail-specific instructions to receive funds into this customer account.",
        "properties": {
          "pix": {
            "$ref": "#/components/schemas/AccountPixInstructions"
          },
          "bank_account": {
            "$ref": "#/components/schemas/AccountBankInstructions"
          }
        }
      },
      "PayoutDestinationCapabilities": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "receive"
        ],
        "properties": {
          "receive": {
            "type": "boolean",
            "description": "Whether this destination is registered with the settlement venue.\n\nRead this, not `status`. A destination is `active` from the moment you create it, and\n`status` never says anything about readiness - `status: active` with\n`capabilities.receive: false` is a normal, expected state.\n\n**What it gates, precisely:** a payout sourced from a wallet\n(`wallet` -> `payout_destination`) requires the venue registration, and is refused with\n`422 PROVISIONING_REQUIRED` while this is `false`. A **local BRL/Pix** destination\nsettles on the domestic rail and never gets a venue reference, so it stays `false` and\nis still usable from a fiat pay-in (`fiat_payment` -> `payout_destination`).\n\nSo: `false` means \"not venue-registered\", not \"unusable\". Poll\n`GET /payout-destinations/{id}` if you need the wallet-sourced route.\n"
          }
        }
      },
      "AccountCapabilities": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "receive",
          "send"
        ],
        "properties": {
          "receive": {
            "type": "boolean",
            "description": "Whether this account can currently receive on its configured rail."
          },
          "send": {
            "type": "boolean",
            "description": "Whether this account can currently initiate sends on its configured rail through API v2."
          }
        }
      },
      "AccountMovement": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "object",
          "direction",
          "amount",
          "currency",
          "occurred_at"
        ],
        "description": "One credit into a named fiat account, as the account's institution reports it. Amounts are decimal strings, never JSON numbers.",
        "properties": {
          "id": {
            "type": "string",
            "description": "The institution's own identifier for the entry. Stable across reads; use it to de-duplicate."
          },
          "object": {
            "type": "string",
            "const": "account_movement"
          },
          "direction": {
            "type": "string",
            "enum": [
              "credit"
            ],
            "description": "Only credits are listed today - money that arrived in the account."
          },
          "amount": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "description": "The amount the payer sent, as a decimal string in `currency`."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the institution booked the credit (RFC 3339, UTC)."
          },
          "reference": {
            "type": "string",
            "description": "The free text the payer sent with the transfer, when the rail carries one. Absent means the rail carried none."
          },
          "payer": {
            "type": "object",
            "additionalProperties": false,
            "description": "Who paid, when the institution reports it. Absent when it does not.",
            "properties": {
              "name": {
                "type": "string"
              },
              "document": {
                "type": "string",
                "description": "The payer's tax or registration document, as reported."
              }
            }
          },
          "received": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "amount",
              "currency"
            ],
            "description": "The amount that actually landed, when the institution reports it separately from `amount` (international wires lose correspondent charges). Absent means the institution reports one figure only.",
            "properties": {
              "amount": {
                "type": "string",
                "pattern": "^-?\\d+(\\.\\d+)?$"
              },
              "currency": {
                "$ref": "#/components/schemas/Currency"
              }
            }
          }
        }
      },
      "AccountBalance": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "available",
          "currency",
          "as_of"
        ],
        "description": "A balance reading mirrored from the account provider. Amounts are decimal strings in the account currency's own precision (two decimals for USD, EUR, GBP and BRL). Never a JSON number.",
        "properties": {
          "available": {
            "type": "string",
            "pattern": "^-?\\d+(\\.\\d+)?$",
            "description": "The balance available at `as_of`, as a decimal string (`\"965.00\"`)."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "as_of": {
            "type": "string",
            "format": "date-time",
            "description": "When OZAV took this reading (RFC 3339, UTC). Readings refresh every few minutes; a payment received after this instant is not yet reflected."
          }
        }
      },
      "Account": {
        "type": "object",
        "additionalProperties": false,
        "description": "A customer-owned named fiat account. The account id is OZAV-owned and\nremains stable across provider routing.\n\n`currency`, `country` and `rail` describe the account that was actually\nopened, which may differ from the values in your request. OZAV selects\nthe banking route, and the receiving coordinates decide the rail: an\naccount reachable by IBAN is not reachable by the same instructions as\none reachable by a domestic account number. Read these three fields from\nthe response rather than assuming your request was echoed back, and\npublish `receiving_instructions` to your payers exactly as returned.\n",
        "required": [
          "id",
          "object",
          "customer_id",
          "account_type",
          "currency",
          "country",
          "rail",
          "status",
          "holder",
          "receiving_instructions",
          "capabilities",
          "balance",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque account identifier, prefixed `ozav_acc_`."
          },
          "object": {
            "type": "string",
            "const": "account",
            "description": "Resource type discriminator; always the literal `account`."
          },
          "customer_id": {
            "type": "string",
            "description": "The `ozav_cus_` id of the owning customer."
          },
          "account_type": {
            "type": "string",
            "const": "named_fiat_account",
            "description": "The only account kind v2 exposes today - a fiat account in the customer's own name. Present so a future kind is additive rather than a breaking change."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "country": {
            "type": "string",
            "pattern": "^[A-Z]{2}$",
            "description": "ISO 3166-1 alpha-2 country of the account that was actually opened. OZAV picks the banking route, so read this back rather than assuming your request was echoed - see the note on this schema."
          },
          "rail": {
            "$ref": "#/components/schemas/Rail"
          },
          "status": {
            "$ref": "#/components/schemas/AccountStatus"
          },
          "holder": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "name",
              "type"
            ],
            "description": "Who the account is in the name of. Taken from the customer record, not from this request - the account cannot be opened in a name the customer has not been verified under.",
            "properties": {
              "name": {
                "type": "string",
                "description": "The legal holder name."
              },
              "type": {
                "type": "string",
                "enum": [
                  "individual",
                  "business"
                ],
                "description": "Mirrors the owning customer's type; a business customer's account is held in the company name, an individual's in the person's."
              }
            }
          },
          "receiving_instructions": {
            "$ref": "#/components/schemas/AccountReceivingInstructions"
          },
          "capabilities": {
            "$ref": "#/components/schemas/AccountCapabilities"
          },
          "balance": {
            "description": "The balance OZAV last read from the account provider, or `null` when OZAV has never\nread one for this account. Always present. `null` never means zero: it means no\nreading exists yet (a freshly opened account, or one whose provider does not expose a\nbalance). When present, `as_of` says when the reading was taken - the number is a\nmirror, not a live query, and a payment received after `as_of` is not reflected.\n",
            "oneOf": [
              {
                "$ref": "#/components/schemas/AccountBalance"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the account record was created (RFC 3339, UTC). Not when it became usable - `status` and `capabilities` decide that."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the account record last changed (RFC 3339, UTC)."
          },
          "pending_action": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "type"
            ],
            "description": "Present only while `status` is `pending`, and only when something is actually being\nwaited on from the holder. `url` is omitted when the verification link cannot be\nverified as belonging to the account provider, in which case the action is still\nreported so the holder knows verification is outstanding.\n",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "complete_verification"
                ],
                "description": "What is being waited on. `complete_verification` - the holder must finish a verification step, at `url` when one could be published."
              },
              "url": {
                "type": "string",
                "format": "uri",
                "description": "Verification webview for the account holder. HTTPS, provider-owned host."
              }
            }
          },
          "failure_reason": {
            "type": "string",
            "description": "Present only while `status` is `failed`, and only when a reason was recorded. Absent\nmeans we did not record one. It never means \"a reason exists and we withheld it\".\n\nA comma-separated list of codes. Never free text, and never a message written by the\nunderlying institution. Every value is one of:\n\n- `billing_address`, `registered_address`, `company_documents`, `representatives`,\n  `representative_address`, `representative_documents`, `time_in_business`,\n  `tax_document`, `legal_name`: the holder still owes us this. Collect it and retry.\n- `counterparty_block`: the block is NOT the holder's paperwork. Sending more\n  documents will not clear it. Contact us.\n- `other_requirement`: a reason was recorded that we could not map to a code above.\n  When it appears alongside other codes, the rest of the list is NOT exhaustive.\n\nBranch on exact values, and treat an unrecognised value the way you treat\n`other_requirement`. The list grows. Every value is a member of\n`AccountFailureCode`, which carries the full enum.\n"
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "AccountFailureCode": {
        "type": "string",
        "description": "One value of `Account.failure_reason`. That field carries a comma-separated list of these,\nso a value here is never the whole field.\n",
        "enum": [
          "billing_address",
          "registered_address",
          "company_documents",
          "representatives",
          "representative_address",
          "representative_documents",
          "time_in_business",
          "tax_document",
          "legal_name",
          "counterparty_block",
          "other_requirement"
        ]
      },
      "AccountPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Page"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "description": "The accounts on this page, newest first. Page through with `starting_after`; an empty array alongside `has_more` false means the end.",
                "items": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          }
        ]
      },
      "AccountHolder": {
        "type": "object",
        "description": "The legal owner of a payout-destination account.",
        "required": [
          "name",
          "type"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The account holder's legal name."
          },
          "type": {
            "type": "string",
            "description": "Whether the account holder is a natural person (`individual`) or a company (`business`).",
            "enum": [
              "individual",
              "business"
            ]
          },
          "tax_id": {
            "$ref": "#/components/schemas/HolderTaxId"
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          }
        }
      },
      "PayoutDestinationDetailsPix": {
        "type": "object",
        "description": "BRL / PIX payout destination. Provide a reusable PIX key; do not provide a one-time Pix BR Code.",
        "required": [
          "pix_key"
        ],
        "properties": {
          "pix_key": {
            "type": "string",
            "description": "A PIX key - CPF/CNPJ, email, phone number, or random (EVP) key."
          }
        }
      },
      "PayoutDestinationDetailsUsBank": {
        "type": "object",
        "description": "USD / ach or fedwire.",
        "required": [
          "account_number",
          "routing_number"
        ],
        "properties": {
          "account_number": {
            "type": "string",
            "description": "The beneficiary bank account number."
          },
          "routing_number": {
            "type": "string",
            "description": "The 9-digit ABA routing number of the beneficiary bank."
          },
          "account_type": {
            "type": "string",
            "description": "The bank account type - `checking` or `savings`.",
            "enum": [
              "checking",
              "savings"
            ]
          }
        }
      },
      "PayoutDestinationDetailsSepa": {
        "type": "object",
        "description": "EUR / sepa.",
        "required": [
          "iban"
        ],
        "properties": {
          "iban": {
            "type": "string",
            "description": "The beneficiary IBAN."
          }
        }
      },
      "PayoutDestinationDetailsSwift": {
        "type": "object",
        "description": "swift - beneficiary reached by SWIFT wire. This is a PAYOUT destination shape; it is not the account-opening corridor of the same name, and `Rail.x-ozav-rail-availability` does not describe it.",
        "required": [
          "account_number",
          "bic"
        ],
        "properties": {
          "account_number": {
            "type": "string",
            "description": "The beneficiary account number or IBAN."
          },
          "bic": {
            "type": "string",
            "description": "The beneficiary bank's SWIFT/BIC code."
          },
          "intermediary": {
            "type": "object",
            "description": "Optional intermediary / correspondent bank details, when the route requires one.",
            "additionalProperties": true
          },
          "purpose_of_funds": {
            "type": "string",
            "description": "Purpose-of-payment statement required for some cross-border corridors."
          }
        }
      },
      "PayoutDestinationDetailsSpei": {
        "type": "object",
        "description": "MXN / spei.",
        "required": [
          "clabe"
        ],
        "properties": {
          "clabe": {
            "type": "string",
            "description": "The 18-digit CLABE identifying the beneficiary account."
          }
        }
      },
      "PayoutDestinationDetailsFasterPayments": {
        "type": "object",
        "description": "GBP / faster_payments - beneficiary reached by UK Faster Payments. This is a PAYOUT destination shape; it is not the account-opening corridor of the same name, and `Rail.x-ozav-rail-availability` does not describe it.",
        "required": [
          "account_number",
          "sort_code"
        ],
        "properties": {
          "account_number": {
            "type": "string",
            "description": "The 8-digit UK account number."
          },
          "sort_code": {
            "type": "string",
            "description": "The 6-digit UK sort code."
          }
        }
      },
      "PayoutDestinationDetailsCoTransfer": {
        "type": "object",
        "description": "COP / co_transfer (sandbox_only - exercisable with a test key, not live yet; see Rail.x-ozav-rail-availability).",
        "required": [
          "account_number",
          "bank_code"
        ],
        "properties": {
          "account_number": {
            "type": "string",
            "description": "The beneficiary bank account number."
          },
          "bank_code": {
            "type": "string",
            "description": "The Colombian bank code identifying the beneficiary bank."
          },
          "account_type": {
            "type": "string",
            "description": "The account type (e.g. ahorros, corriente)."
          }
        }
      },
      "PayoutDestinationDetails": {
        "description": "Rail-specific beneficiary details; the applicable schema is selected by `rail`.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/PayoutDestinationDetailsPix"
          },
          {
            "$ref": "#/components/schemas/PayoutDestinationDetailsUsBank"
          },
          {
            "$ref": "#/components/schemas/PayoutDestinationDetailsSepa"
          },
          {
            "$ref": "#/components/schemas/PayoutDestinationDetailsSwift"
          },
          {
            "$ref": "#/components/schemas/PayoutDestinationDetailsSpei"
          },
          {
            "$ref": "#/components/schemas/PayoutDestinationDetailsFasterPayments"
          },
          {
            "$ref": "#/components/schemas/PayoutDestinationDetailsCoTransfer"
          }
        ]
      },
      "PayoutDestinationCreateRequest": {
        "type": "object",
        "description": "Register a payout destination for a customer on a specific rail. The `details` shape is selected by `rail`.",
        "required": [
          "customer_id",
          "currency",
          "rail",
          "account_holder",
          "details"
        ],
        "properties": {
          "customer_id": {
            "type": "string",
            "description": "The `ozav_cus_` id of the customer this destination belongs to."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "rail": {
            "$ref": "#/components/schemas/Rail"
          },
          "account_holder": {
            "$ref": "#/components/schemas/AccountHolder"
          },
          "details": {
            "$ref": "#/components/schemas/PayoutDestinationDetails"
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "PayoutDestination": {
        "type": "object",
        "description": "Where a customer's funds land off-ramp - a bank account, PIX key, or other rail endpoint.",
        "required": [
          "id",
          "object",
          "currency",
          "rail",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque payout-destination identifier, prefixed `ozav_pd_`."
          },
          "object": {
            "type": "string",
            "const": "payout_destination",
            "description": "Resource type discriminator; always the literal `payout_destination`."
          },
          "customer_id": {
            "type": "string",
            "description": "The `ozav_cus_` id of the owning customer."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "rail": {
            "$ref": "#/components/schemas/Rail"
          },
          "status": {
            "type": "string",
            "description": "Whether the destination has been deactivated by you:\n- `active` - not deactivated.\n- `inactive` - deactivated; reactivate it before using it again.\n\n**`active` does not mean the destination is ready to receive a\ntransfer.** A newly created destination is `active` immediately, but\nstays unusable until OZAV finishes registering it with the settlement\nrail. Using one before then is refused with\n`422 PROVISIONING_REQUIRED` (\"Complete destination registration\nfirst\"), and no field on this resource distinguishes the two states\nyet. Until one exists, treat `422 PROVISIONING_REQUIRED` as retryable\nrather than as a bad request, and do not gate your UI on `status`\nalone.\n",
            "enum": [
              "active",
              "inactive"
            ]
          },
          "capabilities": {
            "$ref": "#/components/schemas/PayoutDestinationCapabilities"
          },
          "account_holder": {
            "$ref": "#/components/schemas/AccountHolder"
          },
          "details": {
            "$ref": "#/components/schemas/PayoutDestinationDetails"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the destination was registered (RFC 3339 / ISO 8601, UTC)."
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          }
        }
      },
      "CardCreateRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "customer_id"
        ],
        "properties": {
          "customer_id": {
            "type": "string",
            "description": "The `ozav_cus_` id of the approved customer receiving the card."
          },
          "type": {
            "type": "string",
            "enum": [
              "virtual",
              "physical"
            ],
            "default": "virtual",
            "description": "Card form factor. `physical` is reserved and currently returns `CARD_TYPE_NOT_AVAILABLE`."
          },
          "funding_type": {
            "type": "string",
            "enum": [
              "prepaid_balance"
            ],
            "default": "prepaid_balance",
            "description": "Cards are funded from OZAV balances in this API version."
          },
          "brand_name": {
            "type": "string",
            "maxLength": 32,
            "description": "Optional brand label shown for white-label programs."
          },
          "replacement_for": {
            "type": "string",
            "description": "Existing `ozav_card_` id being replaced."
          },
          "replacement_reason": {
            "type": "string",
            "enum": [
              "damaged",
              "expired",
              "lost",
              "stolen"
            ],
            "description": "Why the previous card is being replaced. Send it alongside `replacement_for`; on its own it describes nothing."
          },
          "spending_controls": {
            "$ref": "#/components/schemas/CardSpendingControls"
          }
        }
      },
      "CardSpendingControls": {
        "description": "Per-card rules applied at authorisation time. A merchant category or country the acquirer does not send is treated as unknown: an allow list DECLINES it, because \"only these\" cannot be satisfied by a value nobody can name, while a block list permits it.",
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "allowed_categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Merchant category codes (MCC) permitted on this card."
          },
          "blocked_categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Merchant category codes (MCC) refused on this card."
          },
          "allowed_merchant_countries": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "ISO 3166-1 alpha-2 countries permitted. An authorization whose merchant country the acquirer does not send is DECLINED while this list is set - see the note on this schema."
          },
          "blocked_merchant_countries": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "ISO 3166-1 alpha-2 countries refused. An unknown merchant country is PERMITTED here, which is the opposite of the allow list."
          },
          "allowed_card_presences": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "present",
                "not_present"
              ]
            },
            "description": "Restricts to card-present (terminal) or card-not-present (online) purchases. Leaving it unset permits both."
          },
          "blocked_card_presences": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "present",
                "not_present"
              ]
            },
            "description": "Refuses card-present or card-not-present purchases. Setting `[not_present]` is the usual way to make a physical card useless online if stolen."
          },
          "spending_limits": {
            "type": "array",
            "description": "Rolling caps evaluated at authorization time. Each entry pairs an amount with the window it applies to; several entries apply together and the tightest one decides. Read the `amount` note - it is the one field on this API in MINOR UNITS.",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "amount",
                "interval"
              ],
              "properties": {
                "amount": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "Minor units."
                },
                "interval": {
                  "type": "string",
                  "description": "Reset period, aligned to the UTC calendar rather than a rolling window: daily resets at 00:00 UTC, weekly on Monday, monthly on the 1st, yearly on 1 January. all_time never resets, and per_authorization applies to each transaction on its own. Totals are built from authorised amounts, so a later reversal does not return headroom within the same period.",
                  "enum": [
                    "per_authorization",
                    "daily",
                    "weekly",
                    "monthly",
                    "yearly",
                    "all_time"
                  ]
                },
                "categories": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "Merchant category codes (MCC) this limit applies to. Omit to cover all spend on the card. A transaction whose category the acquirer does not send falls outside a scoped limit, so a category budget never silently becomes a card-wide one."
                }
              }
            }
          }
        }
      },
      "Card": {
        "type": "object",
        "description": "Canonical OZAV card object. Sensitive card data is accessed only through secure display sessions.",
        "required": [
          "id",
          "object",
          "customer_id",
          "type",
          "status",
          "funding_type",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque card identifier, prefixed `ozav_card_`."
          },
          "object": {
            "type": "string",
            "const": "card",
            "description": "Resource type discriminator; always the literal `card`."
          },
          "customer_id": {
            "type": "string",
            "description": "The `ozav_cus_` id of the cardholder customer."
          },
          "type": {
            "type": "string",
            "enum": [
              "virtual",
              "physical"
            ],
            "description": "Fixed at creation and never changes. A `physical` card additionally requires a shipping flow; a `virtual` card is usable as soon as it reaches `active`."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "active",
              "frozen",
              "cancelled",
              "expired"
            ],
            "description": "`pending` - created, not yet usable. `active` - authorizations are attempted.\n`frozen` - reversible refusal, set by `/freeze` and undone by `/unfreeze`.\n`cancelled` - TERMINAL, set by `/close`; there is no reopen, issue a replacement.\n`expired` - past `exp_month`/`exp_year`.\n\n`active` is not the same as \"will approve\". A card with no funded balance is `active`\nand declines every purchase with `Insufficient balance` - see `funding_type`.\n"
          },
          "funding_type": {
            "type": "string",
            "enum": [
              "prepaid_balance"
            ],
            "description": "How authorizations are funded. Only `prepaid_balance` exists today: the card spends a\nbalance that must already be there.\n\nNOTE, because it decides whether you can pilot this: the v2 surface has no endpoint that\nLOADS that balance. Until funding is arranged with OZAV, a newly created card is `active`\nand declines every authorization for `Insufficient balance`.\n"
          },
          "last4": {
            "type": [
              "string",
              "null"
            ],
            "description": "Last four digits of the card number, for display. `null` until the issuer has provisioned the card. The full number is never returned - use a display session."
          },
          "exp_month": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Expiry month, 1-12. `null` until the issuer has provisioned the card."
          },
          "exp_year": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Four-digit expiry year. `null` until the issuer has provisioned the card."
          },
          "brand_name": {
            "type": "string",
            "description": "The name printed on the card and shown to the cardholder. Defaults to `OZAV`."
          },
          "spending_controls": {
            "$ref": "#/components/schemas/CardSpendingControls"
          },
          "replacement_for": {
            "type": "string",
            "description": "When this card replaces another, the `ozav_card_` id of the one it replaced. Absent on a card that is not a replacement."
          },
          "replacement_reason": {
            "type": "string",
            "enum": [
              "damaged",
              "expired",
              "lost",
              "stolen"
            ],
            "description": "Why the replaced card was reissued. Absent on a card that is not a replacement. Present only alongside `replacement_for`."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the card record was created (RFC 3339, UTC). Not when it became usable - that is `activated_at`."
          },
          "activated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the card first became `active`. `null` while still `pending`. In the sandbox this is stamped at creation, so a test card skips the pending window."
          },
          "cancelled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When `/close` cancelled the card. `null` on a card that was never closed. Being terminal, this never returns to `null`."
          }
        }
      },
      "CardDisplaySession": {
        "type": "object",
        "required": [
          "id",
          "object",
          "card_id",
          "display_url",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque display-session id, prefixed `ozav_cds_`."
          },
          "object": {
            "type": "string",
            "const": "card_display_session",
            "description": "Resource type discriminator; always the literal `card_display_session`."
          },
          "card_id": {
            "type": "string",
            "description": "The `ozav_card_` id whose sensitive data this session reveals."
          },
          "display_url": {
            "type": "string",
            "format": "uri",
            "description": "OZAV-hosted short-lived display URL."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When `display_url` stops working (RFC 3339, UTC). Sessions are deliberately short-lived; mint a new one rather than storing this URL."
          }
        }
      },
      "CardActivityPage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Page"
          },
          {
            "type": "object",
            "required": [
              "object",
              "card_id",
              "data",
              "has_more",
              "next_cursor"
            ],
            "properties": {
              "object": {
                "type": "string",
                "const": "list",
                "description": "Resource type discriminator for the page envelope; always `list`. What the ITEMS are is in `item_object`."
              },
              "card_id": {
                "type": "string",
                "description": "The `ozav_card_` id this page belongs to; echoed so a page can be attributed without keeping the request around."
              },
              "data": {
                "type": "array",
                "description": "The activity rows. Their shape depends on `item_object` - read that first. Interchange figures and issuer identifiers are never included.",
                "items": {
                  "$ref": "#/components/schemas/CardActivity"
                }
              },
              "item_object": {
                "type": "string",
                "enum": [
                  "card_transaction",
                  "card_authorization"
                ],
                "description": "What the rows in `data` are, and they are NOT the same thing.\n`card_authorization` - the issuer's hold at purchase time; it can expire or be\nreversed and never settle. `card_transaction` - the settled movement.\nSumming both together double-counts the same purchase.\n"
              }
            }
          }
        ]
      },
      "CardActivity": {
        "type": "object",
        "description": "One row of card activity. `item_object` on the page says whether these are settled transactions or authorisation holds - the shape is the same, the meaning is not.\n\nInterchange figures and issuer identifiers are never included.",
        "required": [
          "id",
          "object",
          "card_id",
          "direction",
          "amount",
          "currency",
          "status",
          "merchant",
          "created_at",
          "settled_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque activity id, prefixed `ozav_ctxn_`.",
            "example": "ozav_ctxn_aa11bb22cc33dd44ee55ff6677889900"
          },
          "object": {
            "type": "string",
            "enum": [
              "card_transaction",
              "card_authorization"
            ],
            "description": "Matches the page's `item_object`."
          },
          "card_id": {
            "type": "string",
            "description": "The `ozav_card_` id this activity belongs to."
          },
          "direction": {
            "type": "string",
            "enum": [
              "debit",
              "credit"
            ],
            "description": "`debit` for a purchase, `credit` for a refund. The amount is always POSITIVE and the direction says what it did - a signed amount is easy to mis-render and easy to sum wrongly."
          },
          "amount": {
            "type": "string",
            "description": "The amount charged to the card, in the CARD's currency, always positive.",
            "example": "50.00"
          },
          "currency": {
            "type": "string",
            "description": "The card's own currency. Always `USD` today: the card programme is USD-funded, so this is what the cardholder is actually charged. It is NOT the merchant's currency - see `presentment`.",
            "example": "USD"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "authorized",
              "settled",
              "declined",
              "reversed"
            ]
          },
          "merchant": {
            "type": "object",
            "required": [
              "name",
              "category_code",
              "country"
            ],
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "Cafe Lisboa"
              },
              "category_code": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "MCC.",
                "example": "5812"
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Where the merchant is registered. NOT a reliable test for an international purchase - a merchant can be registered in one country and charge in another currency. Use `presentment`.",
                "example": "PT"
              }
            }
          },
          "presentment": {
            "type": "object",
            "description": "What the purchase cost in the MERCHANT's currency, when the issuer reports it.\n\n**This is how you identify an international transaction:** `presentment.currency !== currency`. Comparing `merchant.country` instead answers a different question and gets it wrong for a merchant registered in one country that bills in another.\n\n**The key is ABSENT when the issuer did not report it** - it is never filled in with the card currency. Absent means \"we were not told\", which has to stay distinguishable from a genuinely domestic purchase. So treat a missing `presentment` as unknown, not as domestic.\n\n`amount` is already scaled for the currency, so a zero-decimal currency comes back without decimals: JPY 5000 is `\"5000\"`, not `\"50.00\"`.",
            "required": [
              "amount",
              "currency"
            ],
            "properties": {
              "amount": {
                "type": "string",
                "example": "5000"
              },
              "currency": {
                "type": "string",
                "description": "ISO 4217 code of the merchant's currency.",
                "example": "JPY"
              }
            }
          },
          "domestic": {
            "type": "boolean",
            "description": "**Deprecated and normally absent - do not build on it.** It has no writer today, so it is omitted from every row the current pipeline produces. Use `presentment` to tell a domestic purchase from an international one."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "settled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Null while an authorisation has not settled."
          }
        }
      },
      "Wallet": {
        "type": "object",
        "description": "Balances and custody model for a customer's holdings. A 1:1 projection - a customer has at most one wallet.",
        "required": [
          "id",
          "object",
          "custody_model",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque wallet identifier, prefixed `ozav_wal_`."
          },
          "object": {
            "type": "string",
            "const": "wallet",
            "description": "Resource type discriminator; always the literal `wallet`."
          },
          "customer_id": {
            "type": "string",
            "description": "The `ozav_cus_` id of the wallet's owning customer."
          },
          "custody_model": {
            "type": "string",
            "enum": [
              "managed",
              "self_custody"
            ],
            "description": "`managed` wallets are held under OZAV-side custody arrangements;\n`self_custody` wallets are controlled by the client or their\nend-user, and OZAV only builds unsigned transactions.\n"
          },
          "status": {
            "type": "string",
            "description": "Whether the wallet is usable. A wallet is `active` only while its\nowning customer account is active; any other account state\n(pending, suspended, closed) projects to `inactive`.\n",
            "enum": [
              "active",
              "inactive"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the wallet was created (RFC 3339 / ISO 8601, UTC)."
          }
        }
      },
      "Balance": {
        "type": "object",
        "description": "A wallet balance for one currency (and network, where applicable).",
        "required": [
          "amount",
          "currency"
        ],
        "properties": {
          "amount": {
            "$ref": "#/components/schemas/DecimalAmount"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "network": {
            "$ref": "#/components/schemas/Network"
          }
        }
      },
      "OnchainBalance": {
        "type": "object",
        "description": "The balance held AT a deposit address on one network, for one currency. NOT the ledger balance (see `Balance`): no in-flight outbound is subtracted, finality is not awaited, and an uncredited inbound deposit is included. Networks with a shared deposit address never appear here - their on-chain balance belongs to every customer at once.\n",
        "required": [
          "object",
          "network",
          "currency",
          "address",
          "amount"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "onchain_balance"
            ],
            "description": "Resource type discriminator; always the literal `onchain_balance`. It marks this as the CHAIN reading, not the ledger `Balance`."
          },
          "network": {
            "$ref": "#/components/schemas/Network"
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "address": {
            "type": "string",
            "description": "The address this balance was read from - the same address `/deposit-addresses` returns for this network.\n"
          },
          "amount": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/DecimalAmount"
              },
              {
                "type": "null"
              }
            ],
            "description": "`null` means the balance COULD NOT BE READ, never that it is zero. A zero here is a measured zero. Treat `null` as \"unknown\" and retry; treating it as `0` would report an empty wallet that may be full.\n"
          },
          "unreadable_reason": {
            "type": "string",
            "description": "Present only when `amount` is `null`. Why the read failed."
          }
        }
      },
      "NftCollection": {
        "type": "object",
        "description": "An ERC-721 / ERC-1155 collection this wallet registered. OZAV does not\nprice it, does not credit it to your ledger balance, and does not include\nit in proof of reserves - so every transfer of a piece requires human\napproval from your quorum.\n",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "nft_collection"
            ]
          },
          "id": {
            "type": "string",
            "description": "The collection id (`ozav_nft_...`).",
            "example": "ozav_nft_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
          },
          "chain": {
            "type": "string",
            "example": "ethereum"
          },
          "contract": {
            "type": "string",
            "description": "The collection contract address, lowercased.",
            "example": "0x1111111111111111111111111111111111111111"
          },
          "standard": {
            "type": "string",
            "enum": [
              "erc721",
              "erc1155"
            ]
          },
          "name": {
            "type": "string",
            "example": "Acervo do Cliente"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_signature",
              "active",
              "disabled"
            ],
            "description": "`pending_signature` - registered, but OZAV's co-signer does not know it yet. `active` - the signed record exists. `disabled` - switched off through this API, which is not the same as revoked."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ClientToken": {
        "type": "object",
        "description": "An ERC-20 this wallet registered beyond the assets OZAV prices and backs.\nOZAV does not price it, does not credit it to your ledger balance, and\ndoes not include it in proof of reserves - so every transfer of it\nrequires human approval from your quorum.\n",
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "client_token"
            ]
          },
          "id": {
            "type": "string",
            "description": "The token id (`ozav_tok_...`).",
            "example": "ozav_tok_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
          },
          "chain": {
            "type": "string",
            "example": "ethereum"
          },
          "contract": {
            "type": "string",
            "description": "The ERC-20 contract address, lowercased.",
            "example": "0x1111111111111111111111111111111111111111"
          },
          "symbol": {
            "type": "string",
            "example": "MEUTOKEN"
          },
          "decimals": {
            "type": "integer",
            "example": 18
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_signature",
              "active",
              "disabled"
            ],
            "description": "`pending_signature` - registered, but OZAV's co-signer does not know\nit yet, so no transfer of it can be signed. `active` - the signed\nrecord exists. `disabled` - switched off through this API, which is\nnot the same as revoked.\n"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WalletAddress": {
        "type": "object",
        "description": "One entry of a managed wallet's address book - the destinations that leave without human review (`allowed`), plus the ones you proposed and OZAV has not decided (`pending_review`) or refused (`rejected`). The book IS the allowlist that the custody plane applies, with your labels.\n",
        "required": [
          "object",
          "network",
          "address",
          "label",
          "status",
          "added_at",
          "requested_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "wallet_address"
            ]
          },
          "network": {
            "$ref": "#/components/schemas/Network"
          },
          "address": {
            "type": "string",
            "description": "As the custody plane matches it (EVM lower-cased)."
          },
          "label": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "allowed",
              "pending_review",
              "rejected"
            ]
          },
          "added_at": {
            "oneOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "description": "When it entered the allowlist; `null` until approved."
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WalletAddressCreateRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "network",
          "address"
        ],
        "properties": {
          "network": {
            "$ref": "#/components/schemas/Network"
          },
          "address": {
            "type": "string",
            "description": "The destination address, validated for its network. EVM addresses are stored lower-cased."
          },
          "label": {
            "type": "string",
            "minLength": 1,
            "maxLength": 60,
            "description": "Optional label, kept with the destination once approved."
          }
        }
      },
      "WalletFreeze": {
        "type": "object",
        "description": "Whether sends from a managed wallet are paused. While `frozen` is true every send answers `409 WALLET_FROZEN` before anything is signed; deposits keep arriving and being credited.\n",
        "required": [
          "object",
          "wallet_id",
          "frozen",
          "frozen_by",
          "reason",
          "since"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "wallet_freeze"
            ]
          },
          "wallet_id": {
            "type": "string"
          },
          "frozen": {
            "type": "boolean"
          },
          "frozen_by": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "customer",
                  "ozav"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "`customer`: your organization froze it and can unfreeze it. `ozav`: OZAV paused withdrawals (a reserve control); you cannot lift it - contact support. `null`: not frozen. When both exist, `customer` is reported: it is the one you can change.\n"
          },
          "reason": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "description": "For a customer freeze, the note you sent prefixed with the freezing key id."
          },
          "since": {
            "oneOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "unfreeze_approval_id": {
            "type": "string",
            "description": "Present when an unfreeze is waiting for your approval quorum - the `ozav_apr_` to decide."
          }
        }
      },
      "WalletFreezeRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "reason": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Optional note, recorded with the freeze (or the lift) and carried in the event."
          }
        }
      },
      "Approval": {
        "type": "object",
        "description": "A decision your approver keys owe: a managed-custody send held by your quorum, or a loosening of the quorum itself. Decisions are per API key; the key that requested the operation cannot decide it.\n",
        "required": [
          "id",
          "object",
          "kind",
          "status",
          "resource",
          "wallet_id",
          "required_approvals",
          "approvals_received",
          "summary",
          "decisions",
          "requested_by_api_key_id",
          "failure_reason",
          "expires_at",
          "created_at",
          "decided_at",
          "executed_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque approval identifier, prefixed `ozav_apr_`."
          },
          "object": {
            "type": "string",
            "enum": [
              "approval"
            ]
          },
          "kind": {
            "type": "string",
            "enum": [
              "wallet_transfer",
              "wallet_policy",
              "wallet_unfreeze"
            ],
            "description": "`wallet_transfer`: a custody send is waiting. `wallet_policy`: a loosening of the wallet's approval quorum is waiting. `wallet_unfreeze`: lifting a freeze you placed on a wallet whose quorum is on.\n"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "expired",
              "executed",
              "failed"
            ],
            "description": "`pending`: waiting for decisions. `approved`: the quorum was reached (transient: the same request executes). `executed`: the held operation ran; see `result` on the deciding response. `failed`: the quorum was reached but the operation was refused on execution; `failure_reason` says why and nothing moved. `rejected`: an approver key said no. `expired`: 72 h passed without a quorum.\n"
          },
          "resource": {
            "type": "object",
            "required": [
              "type",
              "id"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "transfer",
                  "wallet_transfer",
                  "wallet_policy",
                  "wallet_freeze"
                ]
              },
              "id": {
                "type": "string",
                "description": "The `ozav_tr_`, `ozav_wtr_` or `ozav_wal_` id the approval is about."
              }
            }
          },
          "wallet_id": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "required_approvals": {
            "type": "integer"
          },
          "approvals_received": {
            "type": "integer"
          },
          "summary": {
            "type": "object",
            "description": "What is being approved, in brief (amount, currency, destination, or the requested quorum). Never the frozen request.",
            "additionalProperties": true
          },
          "decisions": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "api_key_id",
                "decision",
                "reason",
                "decided_at"
              ],
              "properties": {
                "api_key_id": {
                  "type": "string"
                },
                "decision": {
                  "type": "string",
                  "enum": [
                    "approve",
                    "reject"
                  ]
                },
                "reason": {
                  "oneOf": [
                    {
                      "type": "string"
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "decided_at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "requested_by_api_key_id": {
            "type": "string",
            "description": "The key that created the operation. It cannot approve or reject it."
          },
          "failure_reason": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "decided_at": {
            "oneOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "executed_at": {
            "oneOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ApprovalDecisionRequest": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "reason": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "Optional note recorded with the decision and, on a rejection, carried in `transfer.rejected`."
          }
        }
      },
      "WalletPolicy": {
        "type": "object",
        "description": "What OZAV applies before signing a send from a managed wallet, and the changes to it still being decided. Read `limits.source` first: it says whether the numbers are a per-wallet signed record, your organization's defaults, or nothing at all (everything reviewed).\n",
        "required": [
          "object",
          "wallet_id",
          "destinations",
          "limits",
          "approvals",
          "pending_changes",
          "updated_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "wallet_policy"
            ],
            "description": "Resource type discriminator; always the literal `wallet_policy`."
          },
          "wallet_id": {
            "type": "string",
            "description": "The `ozav_wal_` id this policy belongs to."
          },
          "destinations": {
            "type": "object",
            "required": [
              "mode",
              "allowed"
            ],
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "allowlist",
                  "risk"
                ],
                "description": "`allowlist`: a send to an address outside `allowed` above `limits.auto_approve_below_usd` is parked for human review. `risk`: the risk screening decides on the fly and the list is optional.\n"
              },
              "allowed": {
                "type": "array",
                "description": "Destinations that leave without human review, per network.",
                "items": {
                  "$ref": "#/components/schemas/WalletPolicyAllowedDestination"
                }
              }
            }
          },
          "limits": {
            "type": "object",
            "required": [
              "per_transaction_usd",
              "daily_usd",
              "auto_approve_below_usd",
              "source",
              "valid_until"
            ],
            "properties": {
              "per_transaction_usd": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Ceiling for one send, in USD. `null` when `source` is `unconfigured`."
              },
              "daily_usd": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Ceiling for the rolling 24-hour window, in USD."
              },
              "auto_approve_below_usd": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Below this amount a clean send to a destination NOT in `allowed` still leaves without review. `0` means every unlisted destination is reviewed.\n"
              },
              "source": {
                "type": "string",
                "enum": [
                  "wallet",
                  "organization",
                  "unconfigured"
                ],
                "description": "`wallet`: a signed per-wallet record is in force. `organization`: no per-wallet record; your organization's default limits apply. `unconfigured`: neither exists - every send goes to human review.\n"
              },
              "valid_until": {
                "oneOf": [
                  {
                    "type": "string",
                    "format": "date-time"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "When the per-wallet record expires; `null` unless `source` is `wallet`."
              }
            }
          },
          "approvals": {
            "type": "object",
            "required": [
              "mode"
            ],
            "properties": {
              "mode": {
                "type": "string",
                "enum": [
                  "ozav_review",
                  "customer_quorum"
                ],
                "description": "`ozav_review`: only OZAV's compliance desk decides what falls outside the limits. `customer_quorum`: in addition, `required_approvals` of YOUR approver keys (scope `approvals:write`, other than the key that sent the request) must approve each covered send BEFORE OZAV signs it - the send answers `202 awaiting_approval` and executes on the last approval (`POST /approvals/{id}/approve`). Your quorum is one more gate, never one less: OZAV's screening, limits and review still apply after it.\n"
              },
              "required_approvals": {
                "type": "integer",
                "minimum": 1,
                "maximum": 10,
                "description": "Only in `customer_quorum`. How many distinct approver keys must approve."
              },
              "above_usd": {
                "oneOf": [
                  {
                    "type": "number"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Only in `customer_quorum`. `null`: every send needs the quorum. A number: only sends of that USD value or more (USDC and USDT count 1:1; a currency OZAV cannot value in USD always needs the quorum while one is set).\n"
              }
            }
          },
          "pending_changes": {
            "type": "array",
            "description": "Changes requested and not yet in force, newest first: awaiting review, approved (limits only - in force once the signed record is issued), or rejected.\n",
            "items": {
              "$ref": "#/components/schemas/WalletPolicyPendingChange"
            }
          },
          "updated_at": {
            "oneOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "description": "When the policy in force last changed; `null` when nothing is configured yet."
          }
        }
      },
      "WalletPolicyAllowedDestination": {
        "type": "object",
        "required": [
          "network",
          "address",
          "added_at"
        ],
        "properties": {
          "label": {
            "type": "string",
            "description": "The label you gave the destination (\"cold vault\", \"treasury exchange\"). Display only."
          },
          "network": {
            "$ref": "#/components/schemas/Network"
          },
          "address": {
            "type": "string",
            "description": "The destination address, as the custody plane matches it (EVM lower-cased)."
          },
          "added_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WalletPolicyPendingChange": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "status",
          "requested_at",
          "decided_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The change id, echoed in `wallet_policy.updated` when it is decided."
          },
          "kind": {
            "type": "string",
            "enum": [
              "destination.add",
              "limits.update",
              "approvals.update"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending_review",
              "approved",
              "rejected",
              "awaiting_approval"
            ],
            "description": "`pending_review`: awaiting OZAV. `approved`: accepted, not yet in force (limits only - wait for the signed record; a destination is in `allowed` the moment it is approved, so it never shows as `approved` here). `rejected`: refused; `reason` may say why. `awaiting_approval` (`approvals.update` only): a loosening of your own quorum, waiting for the quorum in force to approve it at `POST /approvals/{approval_id}/approve`.\n"
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "decided_at": {
            "oneOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ]
          },
          "network": {
            "$ref": "#/components/schemas/Network"
          },
          "address": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "per_transaction_usd": {
            "type": "number"
          },
          "daily_usd": {
            "type": "number"
          },
          "auto_approve_below_usd": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ]
          },
          "reason": {
            "type": "string"
          },
          "required_approvals": {
            "oneOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "description": "Only in `approvals.update`; `null` means the quorum is being switched off."
          },
          "above_usd": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "null"
              }
            ]
          },
          "approval_id": {
            "type": "string",
            "description": "Only in `approvals.update` - the `ozav_apr_` your approver keys decide."
          }
        }
      },
      "WalletPolicyChangeRequest": {
        "type": "object",
        "additionalProperties": false,
        "description": "Send at least one of `destinations`, `limits`, `approvals`, `delay` or `cosigner`.",
        "properties": {
          "destinations": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "add"
            ],
            "properties": {
              "add": {
                "type": "array",
                "minItems": 1,
                "maxItems": 20,
                "description": "Destinations to add to the allowlist. Each is validated for its network before it is queued.",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "network",
                    "address"
                  ],
                  "properties": {
                    "network": {
                      "$ref": "#/components/schemas/Network"
                    },
                    "address": {
                      "type": "string",
                      "description": "The destination address. EVM addresses are stored lower-cased."
                    },
                    "label": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 60,
                      "description": "Optional label, kept with the destination once approved."
                    }
                  }
                }
              }
            }
          },
          "limits": {
            "type": "object",
            "additionalProperties": false,
            "description": "Fields you omit inherit the values in force. When nothing is in force yet, `per_transaction_usd` and `daily_usd` are both required.\n",
            "properties": {
              "per_transaction_usd": {
                "type": "number",
                "minimum": 0,
                "description": "Ceiling for one send, in USD."
              },
              "daily_usd": {
                "type": "number",
                "minimum": 0,
                "description": "Ceiling for the rolling 24-hour window, in USD. Must be at least `per_transaction_usd`."
              },
              "auto_approve_below_usd": {
                "oneOf": [
                  {
                    "type": "number",
                    "minimum": 0
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Below this a clean send to an unlisted destination leaves without review. Must not exceed `per_transaction_usd`. `null` means 0: every unlisted destination is reviewed.\n"
              }
            }
          },
          "approvals": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "required_approvals"
                ],
                "properties": {
                  "required_approvals": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10
                  },
                  "above_usd": {
                    "oneOf": [
                      {
                        "type": "number",
                        "minimum": 0
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              }
            ],
            "description": "Your own approval quorum for this wallet. `null` switches it off. Turning it on, or TIGHTENING it (more approvals, a lower or removed threshold), applies at once - no one has to approve more security. LOOSENING or switching it off while a quorum is in force creates an `approvals.update` change that the quorum in force must approve first (`409 APPROVAL_PENDING` while one waits). Works in the sandbox, unlike destinations and limits.\n"
          },
          "cosigner": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "url",
                  "secret",
                  "on_unavailable"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Your HTTPS endpoint. OZAV POSTs the pending transfer to it and reads a decision. `http://` is rejected: it would put the shared secret in the clear."
                  },
                  "secret": {
                    "type": "string",
                    "minLength": 16,
                    "maxLength": 256,
                    "description": "Shared secret. OZAV signs each call `HMAC-SHA256(secret, \"{timestamp}.{body}\")` and sends it as `X-Ozav-Signature`, with the timestamp in `X-Ozav-Timestamp`. **Write-only**: reading the policy never returns it."
                  },
                  "on_unavailable": {
                    "type": "string",
                    "enum": [
                      "hold",
                      "proceed"
                    ],
                    "description": "What OZAV does when your endpoint does not answer. `hold` parks the transfer for a person; `proceed` lets it continue through OZAV's own controls, which all still apply. There is no third option that approves on its own - an endpoint that is down can never be more permissive than one that answers."
                  },
                  "timeout_ms": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10000,
                    "default": 3000
                  }
                }
              }
            ],
            "description": "Your own system in the signing path. `null` removes it.\n\n**An `allow` from your endpoint does not approve anything.** It means \"I do not object\", and the transfer goes on to the exact same OZAV controls it would face without a co-signer - sanctions, KYT, signed vault limit, velocity, allowlist, your approval quorum and your delay. What you gain is the power to REFUSE: a `deny` stops the transfer, always, with no exception and no setting.\n\nReply `200` with `{\"decision\":\"allow\"}` or `{\"decision\":\"deny\",\"reason\":\"...\"}`. Anything else - a non-2xx, an empty body, a proxy page - counts as \"could not ask\", and your `on_unavailable` decides.\n\n**Saving this does not switch it on.** OZAV's independent co-signer obeys a P-256-signed record issued on a separate machine, not this table - otherwise a compromised platform could point it at an endpoint that answers `allow` to everything. Until that record is issued, `GET /policy` reports `cosigner.status: pending_signature` and nothing is consulted.\n"
          },
          "delay": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "hours"
                ],
                "properties": {
                  "hours": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 720
                  },
                  "above_usd": {
                    "oneOf": [
                      {
                        "type": "number",
                        "minimum": 0
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              }
            ],
            "description": "A mandatory delay before a covered send may leave. `null` switches it off. Every other control answers WHO may send, and a stolen credential has all of them; this one answers HOW LONG - it is the window in which a person reads the statement, does not recognise the withdrawal, and cancels.\n\nTurning it on, or TIGHTENING it (more hours, a lower or removed threshold), applies at once. LOOSENING or switching it off is QUEUED and only takes effect once the delay IN FORCE has elapsed - the clock defends itself with its own clock, so a stolen key cannot shorten the wait. `409 DELAY_CHANGE_PENDING` while one is queued; read `delay.loosens_at` for when it lands.\n\nThe delay and the approval quorum COMPOSE: a send covered by both waits for the approvals AND the clock, never the sooner of the two.\n"
          }
        }
      },
      "DepositAddress": {
        "type": "object",
        "description": "An on-chain address that credits this wallet. Read `memo_required` before building a deposit instruction: on networks that attribute by memo, every customer shares one account and the memo is the only thing that routes funds to the right one. A deposit sent without it cannot be attributed.\n",
        "required": [
          "object",
          "network",
          "address",
          "memo_required"
        ],
        "properties": {
          "object": {
            "type": "string",
            "enum": [
              "deposit_address"
            ],
            "description": "Resource type discriminator; always the literal `deposit_address`."
          },
          "network": {
            "$ref": "#/components/schemas/Network"
          },
          "address": {
            "type": "string",
            "description": "The on-chain address to send to."
          },
          "memo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present and non-null exactly when `memo_required` is true. Send it with the deposit, unchanged. Null on address-per-user networks, where it must not be sent.\n"
          },
          "memo_required": {
            "type": "boolean",
            "description": "Whether the deposit is attributed by memo rather than by address. Do not infer this from `memo` being non-null - read this field.\n"
          },
          "memo_type": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "text",
              null
            ],
            "description": "HOW the memo must be sent. `text` means a Stellar MEMO_TEXT. Null where no memo applies.\n\n**Send it as a TEXT memo, never as a numeric one.** Most wallets and exchanges offer a numeric memo/tag field (Stellar MEMO_ID) by default, and our memo is a base64url string, so it will be rejected as invalid there - or, worse, accepted into the numeric field and sent as a different memo entirely. Attribution is an exact string match: a numeric memo never matches, and the deposit arrives unattributed.\n"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the address was assigned to this wallet (RFC 3339, UTC). `null` on an address that predates the record being kept - it does not mean the address is new."
          }
        }
      },
      "UnsignedTransaction": {
        "type": "object",
        "description": "An unsigned EVM transaction for a self-custody wallet to sign and broadcast. OZAV never signs it.",
        "required": [
          "to",
          "data",
          "value",
          "chain_id"
        ],
        "properties": {
          "to": {
            "type": "string",
            "description": "The destination contract or account address (0x-prefixed hex)."
          },
          "data": {
            "type": "string",
            "description": "ABI-encoded calldata for the transaction (0x-prefixed hex; empty for a plain value transfer)."
          },
          "value": {
            "type": "string",
            "description": "Native-token amount to send, in wei, as a decimal string (\"0\" for token transfers)."
          },
          "chain_id": {
            "type": "integer",
            "description": "EIP-155 chain id identifying the target network."
          }
        }
      },
      "TransactionIntent": {
        "type": "object",
        "description": "The result of building an unsigned transaction for a self-custody wallet. Ephemeral - not persisted and not re-fetchable.",
        "required": [
          "id",
          "object",
          "unsigned_transaction",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque transaction-intent identifier, prefixed `ozav_txi_`."
          },
          "object": {
            "type": "string",
            "const": "transaction_intent",
            "description": "Resource type discriminator; always the literal `transaction_intent`."
          },
          "wallet_id": {
            "type": "string",
            "description": "The `ozav_wal_` id of the self-custody wallet the transaction is built for."
          },
          "intent": {
            "type": "string",
            "description": "The kind of transaction that was built:\n- `withdraw` - send funds out of the wallet to an external destination.\n- `transfer` - move funds from the wallet to another address.\n- `deposit` - inbound; not buildable here (a deposit is signed by the external payer).\n",
            "enum": [
              "deposit",
              "withdraw",
              "transfer"
            ]
          },
          "unsigned_transaction": {
            "$ref": "#/components/schemas/UnsignedTransaction"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "Validity horizon of the unsigned transaction, after which nonce/gas may be stale (RFC 3339 / ISO 8601, UTC)."
          }
        }
      },
      "ScreeningSubject": {
        "description": "The screened subject. Provide exactly one of `customer_id`, `person`,\n`business`, or `wallet_address`. Inline subjects (`person` / `business`)\nenable standalone screening with no customer record.\n",
        "oneOf": [
          {
            "type": "object",
            "description": "Screen an existing customer by id.",
            "required": [
              "customer_id"
            ],
            "properties": {
              "customer_id": {
                "type": "string",
                "description": "The `ozav_cus_` id of an existing customer to screen."
              }
            }
          },
          {
            "type": "object",
            "description": "Screen a person inline (no customer record required).",
            "required": [
              "person"
            ],
            "properties": {
              "person": {
                "type": "object",
                "description": "The natural person to screen. Screened as given - no customer record is created and nothing is stored against a customer.",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Full name of the person to screen."
                  },
                  "date_of_birth": {
                    "type": "string",
                    "format": "date",
                    "description": "Date of birth (ISO 8601 date) - helps disambiguate matches."
                  },
                  "country": {
                    "type": "string",
                    "description": "Country of the person as an ISO 3166-1 alpha-2 code.",
                    "pattern": "^[A-Z]{2}$"
                  }
                }
              }
            }
          },
          {
            "type": "object",
            "description": "Screen a business inline (no customer record required).",
            "required": [
              "business"
            ],
            "properties": {
              "business": {
                "type": "object",
                "description": "The legal entity to screen. Screened as given - no customer record is created and nothing is stored against a customer.",
                "required": [
                  "legal_name"
                ],
                "properties": {
                  "legal_name": {
                    "type": "string",
                    "description": "Registered legal name of the business to screen."
                  },
                  "country": {
                    "type": "string",
                    "description": "Country of registration as an ISO 3166-1 alpha-2 code.",
                    "pattern": "^[A-Z]{2}$"
                  }
                }
              }
            }
          },
          {
            "type": "object",
            "required": [
              "wallet_address"
            ],
            "description": "Wallet-address screening is not yet available: this variant currently returns 422 FEATURE_NOT_AVAILABLE. Contact your OZAV account manager for early access.\n",
            "properties": {
              "wallet_address": {
                "type": "object",
                "description": "The blockchain address to screen. NOT AVAILABLE YET - sending this variant returns 422 `FEATURE_NOT_AVAILABLE`, it does not screen and return `clear`.",
                "required": [
                  "address",
                  "network"
                ],
                "properties": {
                  "address": {
                    "type": "string",
                    "description": "The blockchain address to screen."
                  },
                  "network": {
                    "$ref": "#/components/schemas/Network"
                  }
                }
              }
            }
          }
        ]
      },
      "ScreeningMatch": {
        "type": "object",
        "description": "A single match against a designated list.",
        "required": [
          "list_source",
          "matched_name",
          "strength"
        ],
        "properties": {
          "list_source": {
            "type": "string",
            "description": "Which designated-list source produced the match:\n- `un` - United Nations Security Council consolidated list.\n- `ofac` - US OFAC (SDN and related lists).\n- `eu` - European Union consolidated sanctions list.\n- `uk` - UK OFSI consolidated list.\n- `national` - a national designation list.\n",
            "enum": [
              "un",
              "ofac",
              "eu",
              "uk",
              "national"
            ]
          },
          "matched_name": {
            "type": "string",
            "description": "The name on the list that matched the screened subject."
          },
          "strength": {
            "type": "string",
            "description": "How strong the name match is:\n- `exact` - the names match exactly.\n- `alias` - the subject matched a known alias of a listed entity.\n- `fuzzy` - a weaker, token-level match (surfaced for review, never auto-cleared).\n",
            "enum": [
              "exact",
              "alias",
              "fuzzy"
            ]
          },
          "list_entry_ref": {
            "type": "string",
            "description": "The list's own reference id for the matched entry (e.g. an OFAC SDN number), for audit."
          }
        }
      },
      "EvidencePack": {
        "type": "object",
        "description": "What OZAV verified and decided about one customer, assembled at request\ntime. An empty section means absence of record, never approval.\n",
        "properties": {
          "clienteRef": {
            "type": "string",
            "description": "The customer id this record is about (`ozav_cus_...`)."
          },
          "geradoEm": {
            "type": "string",
            "format": "date-time",
            "description": "When this record was assembled."
          },
          "veredito": {
            "type": "string",
            "enum": [
              "aprovado",
              "em_analise",
              "bloqueado",
              "sem_registro"
            ],
            "description": "`sem_registro` means the customer has not been verified - it is not an approval."
          },
          "fundamento": {
            "type": "string",
            "description": "Why this verdict, in one sentence."
          },
          "secoesIndisponiveis": {
            "type": "array",
            "description": "Sections that could not be read. Present means the record does not assert them.",
            "items": {
              "type": "string"
            }
          },
          "resumo": {
            "type": "object",
            "properties": {
              "verificacoes": {
                "type": "integer"
              },
              "triagens": {
                "type": "integer"
              },
              "documentos": {
                "type": "integer"
              },
              "decisoes": {
                "type": "integer"
              },
              "alertas_abertos": {
                "type": "integer"
              },
              "congelamentos_ativos": {
                "type": "integer"
              },
              "decisoes_com_desfecho": {
                "type": "integer",
                "description": "How many decisions already carry what happened afterwards."
              }
            }
          },
          "verificacoes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "capacidade": {
                  "type": "string"
                },
                "resultado": {
                  "type": "string"
                },
                "concluida_em": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          },
          "triagens": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "tipo": {
                  "type": "string"
                },
                "houve_correspondencia": {
                  "type": "boolean"
                },
                "nivel_de_risco": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "triada_em": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "decidida_em": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          },
          "documentos": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "categoria": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "veredito": {
                  "type": "string"
                },
                "avaliador": {
                  "type": "string"
                },
                "avaliado_em": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "parser": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Which assessor version produced the verdict."
                }
              }
            }
          },
          "decisoes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "fluxo": {
                  "type": "string"
                },
                "efeito": {
                  "type": "string"
                },
                "barrou": {
                  "type": "boolean"
                },
                "movimento": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The movement this decision governed."
                },
                "decidida_em": {
                  "type": "string",
                  "format": "date-time"
                },
                "desfecho": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "What happened afterwards. `null` means not yet known."
                }
              }
            }
          },
          "alertas": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "tipo": {
                  "type": "string"
                },
                "severidade": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "estado": {
                  "type": "string"
                },
                "aberto_em": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          },
          "congelamentos": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "estado": {
                  "type": "string"
                },
                "escopo": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "congelado_em": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "liberado_em": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          },
          "atestacaoDeReliance": {
            "type": [
              "object",
              "null"
            ],
            "description": "Present when the integrator performed and attested the identity verification itself.",
            "properties": {
              "metodo": {
                "type": "string"
              },
              "emitida_em": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "expira_em": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        }
      },
      "Screening": {
        "type": "object",
        "description": "A point-in-time designated-list screening of a subject.",
        "required": [
          "id",
          "object",
          "subject",
          "status",
          "result"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque screening identifier, prefixed `ozav_scr_`."
          },
          "object": {
            "type": "string",
            "const": "screening",
            "description": "Resource type discriminator; always the literal `screening`."
          },
          "subject": {
            "$ref": "#/components/schemas/ScreeningSubject"
          },
          "lists": {
            "type": "array",
            "description": "The designated lists that were requested / screened against. Each item is one of:\n- `sanctions` - consolidated sanctions / designated-persons lists.\n- `pep` - politically exposed persons.\n- `adverse_media` - negative-news screening.\n",
            "items": {
              "type": "string",
              "enum": [
                "sanctions",
                "pep",
                "adverse_media"
              ]
            }
          },
          "status": {
            "type": "string",
            "description": "Processing state of the screening:\n- `pending` - the screening is still running.\n- `completed` - the screening finished; read the outcome from `result`.\n",
            "enum": [
              "pending",
              "completed"
            ]
          },
          "result": {
            "type": "string",
            "description": "Conservative, fail-closed outcome:\n- `clear` - every requested list was fully consulted with zero matches.\n- `match_found` - a strong (exact/alias) designated-list match.\n- `review` - a weaker match, or a requested list that could not be fully\n  consulted; never silently cleared.\n\nTELLING THE TWO `review` CAUSES APART, because they call for OPPOSITE actions and the\nanswer is already in this response - read `matches`:\n\n- `review` with a NON-EMPTY `matches` - a weak (token-level) hit. A human decides.\n  Retrying changes nothing; the same subject screens the same way.\n- `review` with an EMPTY `matches` - every list you requested came back without a hit,\n  and at least one of them could not be fully consulted. Nobody matched anything. This\n  is transient on our side: RETRY LATER rather than queueing the subject for review.\n\nThe two are exhaustive for `review`, by construction: a weak hit is counted from the\nsame array that `matches` is serialized from, so an empty `matches` leaves an\nunconsultable source as the only remaining cause.\n\nWHICH source degraded is not published. It is an operational fact about OZAV's\ningestion, not about your subject, and the action it implies (retry) is the same\nwhichever one it was.\n",
            "enum": [
              "clear",
              "match_found",
              "review"
            ]
          },
          "matches": {
            "type": "array",
            "description": "The matches found. Empty when the outcome is `clear` - and ALSO empty on the\n`review` that means a list could not be consulted, which is why `result` and this\nfield have to be read together. See `ScreeningMatch`.\n",
            "items": {
              "$ref": "#/components/schemas/ScreeningMatch"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the screening was created (RFC 3339 / ISO 8601, UTC)."
          }
        }
      },
      "Verification": {
        "type": "object",
        "description": "An identity / business verification session (KYC / KYB) for a customer.\nCompleting or simulating a verification updates the customer's KYC\nstatus. In sandbox, approval can materialize simulated wallet and\nBRL/Pix account projections. Live wallet provisioning and live bank\naccount opening remain separate enabled steps.\n",
        "required": [
          "id",
          "object",
          "customer_id",
          "status"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque verification identifier, prefixed `ozav_vrf_`."
          },
          "object": {
            "type": "string",
            "const": "verification",
            "description": "Resource type discriminator; always the literal `verification`."
          },
          "customer_id": {
            "type": "string",
            "description": "The `ozav_cus_` id of the customer being verified."
          },
          "session_url": {
            "type": "string",
            "format": "uri",
            "description": "Hosted verification session URL - redirect the end-user here to complete identity\ncapture (government ID, selfie, liveness; KYB/UBO for a business customer).\n\n**In sandbox this field never appears, by design.** A `test` key never starts a hosted\nsession, because a real session bills a third party per attempt. Exercise the outcome\nwith `POST /verifications/{verification_id}/simulate` instead - that is the sandbox\naffordance, and it lets you drive `approved` or `rejected` on purpose.\n\n**With a `live` key, two things must both be true**, and the second is not something you\ncontrol:\n\n1. Your `capabilities` include at least one that hosted capture covers:\n   `kyc_document`, `liveness`, `face_match`, `age_estimation`, `kyb`, `ubo`. Asking only\n   for list/data checks (`aml_name_screening`, `email_verify`, `economic_capacity`, ...)\n   never starts a session, because there is nothing for the end-user to do.\n2. Hosted capture is switched on at OZAV. This is a single platform-wide setting,\n   **not** something enabled per organization - there is nothing to request for your\n   account. While it is off, no `live` key starts a session.\n\nWhen either condition is missing - or the key is a `test` key - the verification is still\ncreated and is a valid record: it simply stays `pending` with `checks` at `not_started`\nand **no `session_url`**. That is not an error and retrying will not produce one.\n\nThe field is never fabricated: if the session cannot be created, the request fails with\n`503 VERIFICATION_SESSION_UNAVAILABLE` and nothing is persisted, rather than returning a\nrecord with a broken link.\n"
          },
          "status": {
            "type": "string",
            "description": "Overall state of the verification session:\n- `pending` - created; checks have not started.\n- `in_progress` - one or more checks are running.\n- `completed` - every check reached an outcome.\n- `expired` - the session expired before completing.\n",
            "enum": [
              "pending",
              "in_progress",
              "completed",
              "expired"
            ]
          },
          "checks": {
            "type": "object",
            "description": "Per-capability check results, keyed by `CapabilityKind`.",
            "additionalProperties": {
              "$ref": "#/components/schemas/CheckResult"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the verification session was created (RFC 3339 / ISO 8601, UTC)."
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "description": "An API key's metadata - never its secret, which is shown only at creation and rotation.",
        "required": [
          "id",
          "object",
          "key_prefix",
          "environment",
          "scopes"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque API-key identifier, prefixed `ozav_key_`."
          },
          "object": {
            "type": "string",
            "const": "api_key",
            "description": "Resource type discriminator; always the literal `api_key`."
          },
          "name": {
            "type": "string",
            "description": "Human-readable label you gave the key."
          },
          "key_prefix": {
            "type": "string",
            "description": "Non-secret display prefix of the key (e.g. `ozav_sk_live_a1b2`) - enough to identify it without revealing the secret."
          },
          "environment": {
            "type": "string",
            "description": "Which environment the key operates against:\n- `test` - the sandbox; moves no real money.\n- `live` - production; can move real money.\n",
            "enum": [
              "test",
              "live"
            ]
          },
          "scopes": {
            "type": "array",
            "description": "The scopes granted to this key (intersected with the organization's entitlements at request time).",
            "items": {
              "type": "string"
            }
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "operator",
              "viewer"
            ],
            "description": "The role this key was minted with, when it was minted with one. It is\na label and a mint-time template - **not** part of the permission\ndecision, which stays `entitlements ∩ scopes`. A rotated key keeps it.\n\n**Absent** on a key whose scopes were listed one by one, which is\nevery key created before roles existed. The field is omitted rather\nthan sent as `null`, because `null` here would read as \"this key has\nno powers\" instead of \"this key was not minted from a template\".\n"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the key was last used to authenticate a request (RFC 3339 / ISO 8601, UTC)."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the key expires, if it has an expiry (e.g. an old secret's cutover time after a rotation)."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the key was created (RFC 3339 / ISO 8601, UTC)."
          }
        }
      },
      "ApiKeyWithSecret": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKey"
          },
          {
            "type": "object",
            "properties": {
              "secret": {
                "type": "string",
                "description": "The full secret key; returned only on creation and rotation."
              }
            }
          }
        ]
      },
      "WalletTransferBatch": {
        "type": "object",
        "description": "Up to 50 sends from one managed wallet, on one network, in one currency,\ndecided once and run one at a time in order.\n\n`legs` always has one entry per leg you sent, in the order you sent\nthem - including the ones that were never attempted, so the count always\nmatches your request. A leg with `id: null` does not exist anywhere:\nnothing was sent for it.\n",
        "required": [
          "id",
          "object",
          "wallet_id",
          "status",
          "currency",
          "total_amount",
          "leg_count",
          "legs"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The opaque batch id (`ozav_wtb_...`)."
          },
          "object": {
            "type": "string",
            "enum": [
              "wallet_transfer_batch"
            ]
          },
          "wallet_id": {
            "type": "string",
            "description": "The sending wallet (`ozav_wal_...`)."
          },
          "status": {
            "type": "string",
            "enum": [
              "executing",
              "completed",
              "partially_completed",
              "failed",
              "awaiting_approval",
              "rejected",
              "expired"
            ],
            "description": "`completed` - every leg settled. `partially_completed` - some legs settled, or may have settled, and some did not; what settled moved real money and is not reversible. `failed` - no leg settled and none is in doubt. `executing` - the run has not been closed out yet; read `legs[]` and reconcile rather than re-sending. `awaiting_approval`, `rejected`, `expired` - the batch is (or was) held by your approval quorum and nothing has been signed."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "total_amount": {
            "allOf": [
              {
                "$ref": "#/components/schemas/DecimalAmount"
              }
            ],
            "description": "The sum of the legs. **This is the figure your approval quorum's `above_usd` threshold is applied to** - not any single leg - so ten legs of 9,000 cross a 10,000 threshold."
          },
          "leg_count": {
            "type": "integer",
            "minimum": 1,
            "maximum": 50
          },
          "legs": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "index",
                "id",
                "status",
                "amount",
                "destination",
                "transaction_hash"
              ],
              "properties": {
                "index": {
                  "type": "integer",
                  "description": "Position in the batch, from 0, in the order the legs were attempted."
                },
                "id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The `wallet_transfer` this leg became (`ozav_wtr_...`), or `null` when the leg never became a record at all - not attempted, or refused before the send was even claimed. A leg that was attempted and refused still has an id, and reading the batch back returns the same one."
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "pending",
                    "executing",
                    "completed",
                    "needs_review",
                    "failed",
                    "cancelled"
                  ],
                  "description": "`completed` - settled on chain. `failed` - attempted and refused **before anything was signed**; read `failure`. **`needs_review` - THE SEND MAY HAVE GONE OUT** and the record did not close; it is under reconciliation, and re-sending it can pay twice. `executing` - still in flight. **`cancelled` - NOT ATTEMPTED**, because an earlier leg stopped the run; no money was sent and no transaction exists. `pending` - the batch is waiting on your approval quorum and nothing has run yet."
                },
                "amount": {
                  "$ref": "#/components/schemas/DecimalAmount"
                },
                "destination": {
                  "type": "string"
                },
                "transaction_hash": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "failure": {
                  "type": "object",
                  "description": "Present only on a `failed` leg.",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "approval": {
            "type": "object",
            "description": "Present while the batch is held by your approval quorum, and on a batch that was held. One approval covers the whole batch.",
            "properties": {
              "id": {
                "type": "string",
                "description": "The approval id (`ozav_apr_...`)."
              },
              "required_approvals": {
                "type": "integer"
              },
              "expires_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        }
      },
      "WebhookEventType": {
        "type": "string",
        "description": "Event types currently emitted by API v2 webhooks. The catalog is intentionally\nlimited to events that actually fire today:\n- `webhook.test` - queued by `POST /webhook-endpoints/{id}/test`.\n- `customer.created` / `customer.updated` - emitted by the v2 customers surface.\n- `verification.created` / `verification.updated` - emitted by the v2 verification surface.\n- `screening.completed` - emitted when a screening completes.\n- `transfer.updated` / `transfer.completed` / `transfer.failed` - emitted by the v2 transfer surface.\n- `card.created` / `card.updated` - **card LIFECYCLE only.** `card.created` fires on\n  `POST /cards`; `card.updated` on a spending-controls change (`PATCH /cards/{id}`) and on\n  freeze / unfreeze / close.\n\n\n  **There is deliberately no per-transaction card event.** Nothing fires when a card is\n  used: no purchase, no authorisation, no decline, no settlement. Card spend is\n  **pull-only** - reconcile with `GET /cards/{card_id}/transactions` (settled movement) and\n  `GET /cards/{card_id}/authorizations` (holds that may still reverse). That is a real\n  limit, not an omission from this list: the authorisation pipeline has no client-webhook\n  emitter, and publishing an event with no emitter would advertise something that never\n  fires.\n- `account.updated` - emitted when a named account's public status changes, so an\n  integrator learns their customer can receive money without polling GET /accounts.\n- `deposit.credited` - emitted when crypto sent to one of your wallets has been credited\n  and is spendable. Receiving crypto needs no call from you: the arrival is observed on\n  chain and credited on its own, and it already shows up in `GET /transactions` and in\n  `GET /wallets/{wallet_id}/balances`. This event is the push, so you do not have to poll\n  for it. There is deliberately no `deposit.pending`: before a deposit is credited it can\n  still reorganize on chain, and telling you early would invite you to act on value that\n  may vanish.\n- `requirement.due` - emitted when compliance review determines WHICH documents are still\n  missing for a customer, so you learn what to collect without polling. There is no\n  `requirement.satisfied`: the customer's own `GET /customers/{id}` shrinks\n  `requirements.documents_due` when a file lands, and announcing a second event would need\n  a second emitter to avoid becoming an empty promise.\n- `document.reviewed` - emitted when OZAV's own document review ACCEPTS or REJECTS a\n  supporting document (proof of address, bank statement, proof of income, company\n  document): `{customer_id, document_id, key, label, status: accepted|rejected, reason,\n  reason_label, reviewed_at}`. A rejection is followed by a fresh `requirement.due` for the\n  same document, with a new deadline and the `reason`. Not emitted while a document is\n  waiting on a human reviewer.\n- `wallet_policy.updated` - emitted when a change to a managed wallet's policy is\n  DECIDED: `{wallet_id, change: {id, kind: destination.add|limits.update, status, ...}}`.\n  For a destination, `status: applied` means it is already in `destinations.allowed`.\n  For limits, `status: approved` means accepted and NOT yet in force - the signed record\n  is issued minutes later; confirm with `GET /wallets/{wallet_id}/policy`\n  (`limits.valid_until`). `rejected` carries a `reason` when one was given. Not emitted\n  when you PATCH: proposing is not changing. Also emitted for `kind: approvals.update`\n  when your own approval quorum is applied or a loosening of it is decided. And for `kind: destination.remove` when you\n  take a destination out of the address book (`DELETE /wallets/{id}/addresses/...`).\n- `transfer.approval_required` - emitted when a custody send stops BEFORE execution because\n  the wallet's policy (`approvals.mode: customer_quorum`) requires your approver keys to\n  approve it: `{transfer, approval_id, required_approvals, expires_at}` from\n  `POST /wallets/{id}/transfers`, or `{transfer}` with `status: awaiting_approval` and\n  `approval_id` from `POST /transfers`. Nothing has been signed.\n- `transfer.approved` - emitted when the last required approval arrives and the send is\n  executed in that same request: `{approval_id, transfer}` where `transfer.status` is the\n  OUTCOME (`completed`/`converting` on success, `failed` with `failure` on a clean refusal).\n  A `POST /transfers` send additionally emits its usual `transfer.updated`.\n  **When the approval held a BATCH, the payload carries `batch` instead of `transfer`**, with\n  every leg and its own status - branch on which key is present.\n- `transfer.rejected` - emitted when any approver key rejects: `{approval_id, transfer_id,\n  wallet_id, reason, rejected_by_api_key_id}` - or **`batch_id` in place of `transfer_id`\n  when the approval held a batch.** The held send is released; nothing moves.\n  There is no expiry event: an approval unanswered for 72 h reads `expired` on\n  `GET /approvals/{id}`, and announcing an event that only fires when someone reads would\n  promise a push that does not come on its own.\n- `transfer_batch.completed` / `transfer_batch.failed` - emitted once when a batch\n  (`POST /wallets/{id}/transfer-batches`) finishes running: `{batch}`, carrying every leg\n  with its own status and transaction hash. `transfer_batch.completed` covers\n  `partially_completed` too, because some money did move; `transfer_batch.failed` means no\n  leg settled. There is deliberately no event per leg - the batch exists to be ONE\n  decision, and N events per batch would hand back the loop the route took away. There is\n  no `transfer_batch.created` either: the create is already the 201 response. A batch held\n  by your quorum emits `transfer.approval_required` like a single send, with `batch` in\n  place of `transfer`.\n- `wallet.frozen` / `wallet.unfrozen` - emitted when YOU freeze a managed wallet\n  (`POST /wallets/{id}/freeze`) and when your freeze is lifted (`POST /wallets/{id}/unfreeze`,\n  or the approval of a `wallet_unfreeze` when your quorum is on): `{wallet_id, reason, since,\n  frozen_by_api_key_id}` / `{wallet_id, unfrozen_by_api_key_id, approval_id?}`. While frozen,\n  every send from the wallet is refused with `409 WALLET_FROZEN` before anything is signed.\n  Not emitted for OZAV's own reserve freezes.\n",
        "enum": [
          "webhook.test",
          "customer.created",
          "customer.updated",
          "verification.created",
          "verification.updated",
          "screening.completed",
          "transfer.updated",
          "transfer.completed",
          "transfer.failed",
          "card.created",
          "card.updated",
          "account.updated",
          "deposit.credited",
          "requirement.due",
          "document.reviewed",
          "wallet_policy.updated",
          "transfer.approval_required",
          "transfer.approved",
          "transfer.rejected",
          "wallet.frozen",
          "wallet.unfrozen",
          "transfer_batch.completed",
          "transfer_batch.failed"
        ]
      },
      "WebhookEndpointCreateRequest": {
        "type": "object",
        "required": [
          "url",
          "enabled_events"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://",
            "description": "HTTPS URL that will receive signed POST deliveries."
          },
          "enabled_events": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            },
            "description": "Event types to deliver to this endpoint."
          },
          "description": {
            "type": "string",
            "maxLength": 255,
            "description": "Optional human-readable label for the endpoint."
          }
        }
      },
      "WebhookEndpointUpdateRequest": {
        "type": "object",
        "minProperties": 1,
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://",
            "description": "Replacement HTTPS delivery URL."
          },
          "enabled_events": {
            "type": "array",
            "minItems": 1,
            "description": "REPLACES the subscription list; it is not merged with the current one. Send the full set you want, including the types you already had, or you will silently unsubscribe from them.",
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          "status": {
            "type": "string",
            "description": "Set to `disabled` to stop deliveries without deleting the endpoint (the signing secret and the subscription list survive), or back to `active` to resume.",
            "enum": [
              "active",
              "disabled"
            ]
          },
          "description": {
            "type": "string",
            "maxLength": 255,
            "description": "Replacement human label. Not used for routing."
          }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "description": "Webhook endpoint configuration. Secrets, secret hashes, Vault refs, and internal ids are never returned.",
        "required": [
          "id",
          "object",
          "url",
          "enabled_events",
          "status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque webhook endpoint identifier, prefixed `ozav_wh_`."
          },
          "object": {
            "type": "string",
            "const": "webhook_endpoint",
            "description": "Resource type discriminator; always the literal `webhook_endpoint`."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The HTTPS URL that receives the deliveries. Verify the signature on every request to it (see the Webhooks section); an unverified receiver accepts forged events."
          },
          "enabled_events": {
            "type": "array",
            "description": "The event types this endpoint is subscribed to. An event whose type is not listed here is never delivered to this endpoint, and no record of a skipped delivery is created.",
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          "status": {
            "type": "string",
            "description": "`active` - deliveries are attempted. `disabled` - nothing is delivered and nothing is\nqueued for later; events that occur while disabled are NOT replayed on re-enable.\n\nOZAV does not disable an endpoint on its own. A URL that has failed every delivery for\nweeks still reads `active`, so `status` alone is not a health signal - read `health`.\n",
            "enum": [
              "active",
              "disabled"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-form label you set. Never used for routing or delivery."
          },
          "health": {
            "type": "object",
            "description": "Read-only delivery health. `last_success_at: null` on an endpoint that has existed for\na while means it has NEVER delivered, which is the failure that otherwise stays\ninvisible for weeks. Absent when the aggregate could not be read; its absence never\nfails the request.\n\n`last_error` separates the two OPPOSITE causes, because the fix differs: an `HTTP 401`\nmeans your server answered and REFUSED (a signature problem), while a transport error\nsuch as `dns error: ...` means nothing answered at all (hostname, TLS, firewall) and no\nsignature change will help.\n",
            "properties": {
              "last_success_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When a delivery to this endpoint last got a 2xx. `null` means never."
              },
              "last_failure_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "When a delivery to this endpoint last failed (non-2xx or transport error). `null` means no failure has been recorded."
              },
              "last_error": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The most recent failure, verbatim - either the HTTP status your server returned or the transport-level error. `null` when nothing has failed."
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the endpoint was registered (RFC 3339, UTC)."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the endpoint was last modified (RFC 3339, UTC)."
          }
        }
      },
      "WebhookEndpointWithSecret": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEndpoint"
          },
          {
            "type": "object",
            "properties": {
              "secret": {
                "type": "string",
                "description": "Signing secret (`ozav_whsec_...`); returned only on creation and rotation."
              }
            }
          }
        ]
      },
      "WebhookDeliveryStatus": {
        "type": "string",
        "enum": [
          "pending",
          "delivering",
          "succeeded",
          "failed",
          "exhausted"
        ]
      },
      "WebhookDelivery": {
        "type": "object",
        "description": "One delivery attempt stream for one endpoint/event pair. OZAV retries non-2xx\nand network failures with backoff until the delivery succeeds or exhausts its\nattempt budget. Response bodies are stored only as truncated previews.\n",
        "required": [
          "id",
          "object",
          "endpoint_id",
          "event_id",
          "event_type",
          "status",
          "attempt_count",
          "next_attempt_at",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque delivery identifier, prefixed `ozav_wd_`."
          },
          "object": {
            "type": "string",
            "const": "webhook_delivery",
            "description": "Resource type discriminator; always the literal `webhook_delivery`."
          },
          "endpoint_id": {
            "type": "string",
            "description": "The `ozav_wh_` endpoint this delivery targets."
          },
          "event_id": {
            "type": "string",
            "description": "The `ozav_evt_` event being delivered."
          },
          "event_type": {
            "$ref": "#/components/schemas/WebhookEventType"
          },
          "status": {
            "$ref": "#/components/schemas/WebhookDeliveryStatus"
          },
          "attempt_count": {
            "type": "integer",
            "minimum": 0,
            "description": "Number of delivery attempts made so far."
          },
          "next_attempt_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the next retry is scheduled (RFC 3339, UTC). It keeps moving while the backoff runs; once `status` is `succeeded` or `exhausted` it stops being meaningful."
          },
          "last_attempt_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the most recent attempt was made. `null` before the first attempt."
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When an attempt got a 2xx. `null` on a delivery that has never succeeded - which, on an endpoint whose `response_status` is also `null`, means nothing at the URL ever answered."
          },
          "response_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Last HTTP status code returned by your endpoint, if a response was received."
          },
          "response_preview": {
            "type": [
              "string",
              "null"
            ],
            "description": "Truncated preview of the last response body."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Last network/timeout error, if no HTTP response was received."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the delivery row was created (RFC 3339, UTC) - effectively when the event was queued for this endpoint."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the delivery row last changed (RFC 3339, UTC) - every attempt moves it. }"
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "id",
          "object",
          "type",
          "created_at",
          "data"
        ],
        "description": "Canonical event envelope POSTed to webhook endpoints. OZAV signs the exact\nJSON body bytes with HMAC-SHA256. Deliveries include:\n- `X-OZAV-Signature: t=<unix>,v1=<hex>`, where `v1` is HMAC-SHA256 over\n  `\"<t>.<raw body>\"`, keyed with your endpoint secret **exactly as returned,\n  including the `ozav_whsec_` prefix**. The prefix is part of the key, not a\n  display label: stripping it produces a completely different digest and every\n  delivery fails verification. `v1` is lowercase hex; parse it out of the header\n  rather than comparing the whole `t=...,v1=...` value.\n- `X-OZAV-Event-ID`, stable across retries for idempotency.\n- `X-OZAV-Delivery-ID`, unique to this endpoint/event delivery row.\n\nReject signatures whose timestamp is outside your tolerance window (five\nminutes is recommended), and dedupe by `X-OZAV-Event-ID`.\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque event identifier, prefixed `ozav_evt_`."
          },
          "object": {
            "type": "string",
            "const": "event",
            "description": "Resource type discriminator; always the literal `event`."
          },
          "type": {
            "$ref": "#/components/schemas/WebhookEventType"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the event occurred (RFC 3339, UTC), not when this delivery attempt was made. It stays the same across retries - use it, not receipt time, to order events."
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The affected resource, in the SAME shape its `GET` returns. An event about a transfer\ncarries the `Transfer` object; one about a customer carries the `Customer`.\n\nIt is a SNAPSHOT at event time, and it can be stale by the time you read it. When the\ncurrent state matters (a decision about money), re-fetch by id rather than acting on\nthis body. Deliveries can also arrive out of order.\n"
          }
        }
      },
      "Page": {
        "type": "object",
        "description": "The uniform envelope every list endpoint returns.",
        "required": [
          "data",
          "has_more"
        ],
        "properties": {
          "data": {
            "type": "array",
            "description": "The page of results. Each list endpoint documents its own item type.",
            "items": {}
          },
          "has_more": {
            "type": "boolean",
            "description": "True when more results exist beyond this page - keep paging with `starting_after`."
          },
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque cursor for the next page; pass it as `starting_after`. Null on the last page."
          }
        }
      },
      "ErrorDetail": {
        "type": "object",
        "description": "A single field-level error, when the top-level error decomposes into several.",
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable code for this specific field error."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation of this field error."
          },
          "param": {
            "type": "string",
            "description": "The request field this error refers to."
          }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "description": "The envelope every error response uses, alongside the HTTP status code.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "description": "The error payload; always present on a non-2xx response.",
            "required": [
              "type",
              "code",
              "message",
              "request_id"
            ],
            "properties": {
              "type": {
                "type": "string",
                "description": "Stable high-level error class (see the \"Errors\" section for the full table):\n- `invalid_request_error` - malformed or unfulfillable input.\n- `authentication_error` - missing, invalid, revoked, or expired key.\n- `permission_error` - authenticated but not permitted (entitlement or scope).\n- `not_found_error` - the resource does not exist or is not visible.\n- `conflict_error` - conflicts with existing state (e.g. idempotency-key reuse).\n- `rate_limit_error` - too many requests; honor `Retry-After`.\n- `idempotency_error` - the idempotency layer was unreachable; retry.\n- `api_error` - an unexpected server-side or dependency error.\n",
                "enum": [
                  "invalid_request_error",
                  "authentication_error",
                  "permission_error",
                  "not_found_error",
                  "conflict_error",
                  "rate_limit_error",
                  "idempotency_error",
                  "api_error"
                ]
              },
              "code": {
                "type": "string",
                "description": "Stable machine-readable code. Per-operation codes include ENTITLEMENT_REQUIRED,\nINSUFFICIENT_SCOPE, IDEMPOTENCY_KEY_REUSED. Platform-wide codes any operation may\nreturn: API_V2_DISABLED (503, kill switch off), AUTH_UNAVAILABLE and\nLOOKUP_UNAVAILABLE (503, fail-closed on a dependency error), and, where an\noperation reaches a provisioning path outside your entitlements,\nPROVISIONING_NOT_IMPLEMENTED and\nVERIFICATION_SESSION_NOT_IMPLEMENTED (501).\n"
              },
              "message": {
                "type": "string",
                "description": "Human-readable description of what went wrong."
              },
              "param": {
                "type": "string",
                "description": "The request field the error refers to, when applicable."
              },
              "request_id": {
                "type": "string",
                "description": "Identifier of this exact request - include it in every support request."
              },
              "details": {
                "description": "Usually an array of error detail objects (`{code, message,\nparam}`). The router's 404 catch-all instead returns a\ndiagnostic object variant:\n`{\"diag\": {\"method\": \"...\", \"pathParts\": [\"...\"]}}`.\n",
                "oneOf": [
                  {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/ErrorDetail"
                    }
                  },
                  {
                    "type": "object",
                    "properties": {
                      "diag": {
                        "type": "object",
                        "description": "Router 404 diagnostic - the parsed method and path segments of the unmatched request.",
                        "properties": {
                          "method": {
                            "type": "string",
                            "description": "The HTTP method of the unmatched request."
                          },
                          "pathParts": {
                            "type": "array",
                            "description": "The path segments the router parsed from the unmatched request.",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                ]
              }
            }
          }
        }
      }
    }
  }
}