openapi: 3.1.0
info:
  title: OZAV API
  version: 2.0.0
  summary: Canonical OZAV platform API (v2)
  description: |
    OZAV gives you three products behind one API: wallet infrastructure your users control,
    compliance checks you act on, and a regulated platform that moves money for you. Use one of them,
    or all three under the same key.

    ## Three products

    | | Wallet as a Service | Compliance as a Service | Money Movement |
    |---|---|---|---|
    | **What you build** | Wallets inside your product, for your users | Sanctions, PEP and risk checks inside your own flows | Accounts, FX, payouts, custody and cards for your customers |
    | **Who holds the keys** | Your users | - | OZAV |
    | **Who decides** | You | You, on OZAV's evidence | OZAV, as the regulated operator |
    | **Going live** | Accept the terms in the portal and add prepaid credit. No KYB of your company | Accept the terms in the portal and add prepaid credit. No KYB of your company | KYB of your company and a signed contract |
    | **Billing** | Prepaid, per call | Prepaid, per call | Monthly invoice |

    Each section of this reference opens with an **Overview**: what the product does, and the calls
    in the order you make them. **Yield** and **Agents** (beta) have sections of their own.

    The machine-readable contract is at `https://ozav.io/openapi.yaml` (or `.json`). It is the file
    this page renders, so the two never drift:

    ```
    npx openapi-typescript https://ozav.io/openapi.json -o ozav.d.ts
    ```

    ## Get access

    1. **Apply** at [ozav.io/caas/apply](https://ozav.io/caas/apply). No key and no sales call needed.
    2. **OZAV reviews the application.** A person reviews it, not an automated rule.
    3. **Your sandbox opens.** You get a portal invite and mint your own `ozav_sk_test_` keys under
       *Developers -> API Keys*, scoped as you choose, up to ten per environment.
    4. **Go live.** Wallet as a Service and Compliance as a Service go live when an admin accepts the
       terms in the portal and adds prepaid credit. Money Movement goes live after KYB of your company
       and a signed contract; OZAV then issues your `ozav_sk_live_` keys.

    `GET /capabilities` returns, for the key you call it with, the products enabled for your
    organization, the key's scopes and what each feature needs.

    ## Authentication

    Authenticate every request with your secret key in the **`X-API-Key`**
    header (or as an `Authorization: Bearer` token - the same key, either way).

    ```
    X-API-Key: ozav_sk_test_5f8d…    # sandbox
    X-API-Key: ozav_sk_live_9a1c…    # production
    ```

    - **The key prefix selects the environment.** `ozav_sk_test_` keys resolve
      against the sandbox; `ozav_sk_live_` keys resolve against production. The
      base URL is identical for both - there are no separate hostnames and no
      code changes to promote from test to live.
    - **Two authorization axes.** Your organization holds **entitlements** (the
      products enabled for you: `liquidity`, `compliance`, `customers`, `cards`,
      `custody`, `self_custody`, `kyc_reliance`, `agents`). Each key additionally carries
      **scopes** (e.g. `quotes:write`, `transfers:read`) that restrict what
      that key may do. Effective permission is the intersection. A missing
      product returns `403 ENTITLEMENT_REQUIRED` ("talk to us"); a missing
      scope returns `403 INSUFFICIENT_SCOPE` ("reissue the key with that
      scope").
    - **Getting keys.** Mint, list, rotate, and revoke keys under the API Keys
      resource, or in the developer portal. A new key's secret is shown **once**
      at creation - store it securely. Rotate with an overlap window so the old
      secret keeps working while you cut over.
    - **Keep secrets server-side.** A live secret key can move money. Never ship
      it in a browser, a mobile binary, or a public repository.

    ## Sandbox & Environments

    There is one base URL - `https://developers.ozav.io/v2` - for both
    environments; the key prefix does the routing.

    It is `developers.`, not `api.`. `api.ozav.io` resolves and answers, but it
    is not this API: it returns `{"error":"requested path is invalid"}`, a shape
    that appears nowhere in this document and carries no `request_id`. If you
    get an error whose body does not match the `Error` schema below, check the
    hostname before checking anything else.

    | | Test | Live |
    |---|---|---|
    | Key prefix | `ozav_sk_test_` | `ozav_sk_live_` |
    | Moves real money | No | Yes |
    | Data | Isolated sandbox | Production |

    A `test` key exercises customers, verifications, accounts, quotes,
    transfers and screenings with no real settlement and no real bank-account
    opening. When you are ready, swap the key for its `live_` counterpart; your
    code does not change.

    **Your data does not carry over, and this is the part that surprises people.**
    Every resource belongs to exactly one environment. A customer created with a
    `test` key exists only in test; a customer created with a `live` key exists
    only in live. **The ids are different, and an id from one environment is not
    found in the other** - `GET /customers/{id}` with the wrong environment's key
    answers `404 CUSTOMER_NOT_FOUND`, and `GET /customers` lists only that
    environment's records. A brand new `test` key therefore lists **zero**
    customers, even for an organization with many customers in production. That
    is not an error and not a permission problem: there is simply nothing in test
    yet.

    So the first thing to do with a `test` key is **create your own test data**
    with it: `POST /customers`, then accounts, wallets, quotes. Do not reuse
    production ids. When you go live, the same code runs against the `live` key
    and operates on the live records it creates there.

    ## Idempotency

    Money-moving and creation endpoints accept an **`Idempotency-Key`** header
    so a retry can never execute twice.

    - **Format:** 16-256 characters of `[A-Za-z0-9_-]`. Generate a fresh key
      per logical operation (a UUID works well) and reuse it only when retrying
      that exact operation.
    - **Required vs optional.** `POST /transfers` **requires** it - a transfer
      without one is rejected. `POST /customers` and `POST /payout-destinations`
      accept it (optional, recommended). Reads never need one.
    - **`POST /accounts` is the exception, and it does not read the header at
      all.** Sending `Idempotency-Key` there is not an error and not a
      protection: none of the mechanics below apply to it - no
      `Idempotent-Replayed`, no `409 IDEMPOTENCY_KEY_REUSED` on a changed body,
      no in-flight `202`. This paragraph exists because the sentence above says
      "creation endpoints" and an integrator would reasonably include it.

      What protects that endpoint instead is the **natural key**: an account is
      unique per `(customer, currency, country, rail)`. Repeat the same request
      and you get **`200`** with the account that already exists, where the
      first call returned **`201`** - so the status code, not a header, is how
      you tell a fresh account from an existing one. You do not need to generate
      a key for it, and reusing one across different corridors will not confuse
      it.
    - **`POST /accounts/{account_id}/payouts` does not read the header either.**
      Its idempotency is the request itself: the same `reference` and `amount`
      return the payout that already exists (see that route). Build `reference`
      once per payout and keep it across retries.
    - **Replay.** A retry with the **same key and same body** replays the
      original response verbatim - same status code - plus the header
      **`Idempotent-Replayed: true`**, so you can tell a replay from a fresh
      call.
    - **In flight.** If the first request is still executing when the retry
      arrives, the retry returns **`202`** with `{"status":"processing"}` -
      wait, then retry with the same key to receive the stored result.
    - **A `202` that does not resolve means the outcome is unknown, not
      pending.** If the original request failed after the resource may already
      have been written, the key stays claimed and every retry with it answers
      `202` - there is no stored result coming. Do not loop. Reconcile with a
      `GET` (list by `customer_id`, or read the id you were given) to find out
      whether the resource exists, then continue with a **new** key if it does
      not. A claimed key that outlives its request is recoverable; a duplicate
      customer or payout destination is not.
    - **Conflicts.** The same key with a **different body** is a client bug and
      returns **`409 IDEMPOTENCY_KEY_REUSED`** - it never overwrites the first
      result. Use a new key when the request has genuinely changed.

    Retry idempotency-safe on `429` and `5xx` with the same key and exponential
    backoff; treat a `4xx` other than `409` as terminal. Cap the retries: if
    the response becomes a `202` that repeats, stop and reconcile as described
    above rather than retrying indefinitely.

    ## Pagination

    List endpoints are cursor-paginated and uniform.

    - **`limit`** - page size, `1`-`100`, default `20`.
    - **`starting_after`** - an opaque cursor; pass the previous page's
      `next_cursor`.

    Every list responds with the same envelope:

    ```json
    {
      "data": [ /* … */ ],
      "has_more": true,
      "next_cursor": "b3phdl9jdXNfMDFK…"
    }
    ```

    Loop until `has_more` is `false`. Cursors are opaque - do not parse or
    construct them; a malformed cursor fails loud with `400 INVALID_CURSOR`
    rather than silently resetting to page one.

    ## Rate limits

    OZAV enforces **90 requests per minute per organization**: every key, test and
    live alike, shares one counter across all routes. The OZAV portal counts too
    while your team uses it to quote, send or follow a transfer: an open quote
    screen refreshes about every 12 seconds (5 requests a minute), and an open
    transfer page polls its status every 3 seconds for its first 3 minutes (20 a
    minute), then every 15 seconds. Payouts an agent platform makes under a
    grant you gave it count against your organization as well: two requests
    each (a quote and a transfer), three when the payout waits for approval and
    is quoted again. When your organization is at its limit, those payouts fail
    too. One limit, no plan
    tiers. Exceeding it returns `429 RATE_LIMIT_EXCEEDED`. If OZAV cannot read the
    counter, the request is refused with `503 RATE_LIMIT_UNAVAILABLE` (`type: api_error`)
    instead: that is an OZAV-side failure, not your quota. Every counted response carries `X-RateLimit-Limit`,
    `X-RateLimit-Remaining` and `X-RateLimit-Reset`; read `Remaining` and slow
    down before you hit the wall. A request that matches no route (`404`), or
    that the route refuses before it runs because your key lacks the scope or
    the product the route requires (`403`), does not count: the limit measures
    use, not attempts. Once the route runs, the response counts whatever it
    is, a refusal included (a `422`, a `403` such as `COMPLIANCE_HOLD`, or an
    `ENTITLEMENT_REQUIRED` that depends on what the request names). The
    observable rule: a response that carries `X-RateLimit-Remaining` was
    counted, and one without it was not.

    Design against the response too, and read its `code` first: a
    **`429 rate_limit_error`** is either a request to slow down or a cap with
    a stated reset. `RATE_LIMIT_EXCEEDED` is the per-minute limit and clears
    when the window turns - `Retry-After` says when, and it is never more than
    60 seconds. A cap says so in `code` and `message` - for example
    `CREDENTIAL_ISSUANCE_LIMIT_REACHED` resets the next day, and retrying
    sooner cannot succeed. Otherwise wait the number of seconds in
    `Retry-After` when the header is present, and when it is not, back off
    exponentially starting at one second. Treat a `503` the same way.

    Before repeating a write, check whether it takes an `Idempotency-Key`: a
    route that documents one returns the stored result on a retry with the
    same key (see Idempotency); a route that does not may already have
    executed, so reconcile with a `GET` before sending it again.

    Card activity raises webhooks (`card.authorized`, `card.declined`,
    `card.settled`, `card.refunded`). A hold that is released without settling
    raises none: on `GET /cards/{card_id}/authorizations` it shows as
    `status: reversed` (released or expired) or `declined` (refused by the card
    processor after OZAV approved it); in both the money is back on the balance.
    A hold is open while its `status` is `pending` or `authorized`. `settled` is
    final. `reversed` and `declined` end the hold, but a merchant can still
    capture it late: the same row then turns `settled`, OZAV charges the amount
    to the balance again and `card.settled` fires. That row keeps its original
    `created_at`, so it can sit far back in the list. Poll that route only for cards that
    still have an open hold, read a card again on its next card event, and keep
    all polling well inside your organization's 90 requests per minute. See
    `GET /cards/{card_id}/transactions` for how to read it incrementally.

    ## Amounts & Currencies

    - **Amounts are decimal strings**, never floats: `"1250.00"`, `"0.50"`.
      This preserves precision across currencies - the float `0.1` cannot
      represent one tenth exactly, and money math must not drift.
    - **Precision.** Quote and balance amounts carry up to **6** fractional
      digits; the amount you submit when creating a transfer accepts up to
      **8**.
    - **Currency codes** are uppercase ISO 4217 (`BRL`, `USD`, `EUR`, `MXN`) or
      stablecoin symbols (`USDC`, `USDT`). Codes are accepted case-insensitively
      on input and always returned uppercase.
    - **Exchange conditions are a spread in integer basis points** - `150`
      means 150 bps (1.50%), stored as an integer and divided by 10,000 in rate
      math. Never a float or a percentage string. A quote's public
      `exchange_conditions.spread_bps` is only the additional spread selected
      by the client for that quote. OZAV cost, floor, and margin analytics stay
      internal.

    ## Errors

    Every error returns the same JSON envelope alongside the HTTP status:

    ```json
    {
      "error": {
        "type": "invalid_request_error",
        "code": "MISSING_FIELD",
        "message": "email is required.",
        "param": "email",
        "request_id": "req_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
        "details": [
          { "code": "MISSING_FIELD", "message": "email is required.", "param": "email" }
        ]
      }
    }
    ```

    - **`type`** is one of eight stable classes:

      | `type` | Meaning | Typical status |
      |---|---|---|
      | `invalid_request_error` | Malformed or unfulfillable input - a missing field, a bad value, or a well-formed request that cannot be executed. | `400`, `422` |
      | `authentication_error` | Missing, invalid, revoked, or expired key. | `401` |
      | `permission_error` | Authenticated but not permitted - product not enabled, or the key lacks the scope. | `403` |
      | `not_found_error` | Resource does not exist or is not visible to the caller. | `404` |
      | `conflict_error` | Conflicts with existing state - e.g. an idempotency key reused with a new body, or a duplicate customer. | `409` |
      | `rate_limit_error` | Too many requests - honor `Retry-After`. | `429` |
      | `idempotency_error` | The idempotency layer could not be reached; retry the same request. | `503` |
      | `api_error` | An unexpected server-side or dependency error; OZAV fails closed rather than guess. | `500`, `503` |

    - **`code`** is a stable, machine-readable string you can branch on
      (`ENTITLEMENT_REQUIRED`, `INSUFFICIENT_SCOPE`, `IDEMPOTENCY_KEY_REUSED`,
      `RAIL_NOT_AVAILABLE`, `QUOTE_EXPIRED`, …).
    - **`param`** names the offending field when applicable; **`details`** may
      carry per-field errors.
    - **`request_id`** identifies the exact call - include it in every support
      request.

    The complete, machine-readable catalog of `code` values ships alongside
    this reference as `v2-error-codes.json`, so you can generate typed error
    handling directly from it.

    ### Resource ids, and what a malformed one returns

    The prefixes below are the ones a path parameter accepts, plus `ozav_apr_`,
    which the deprecated `GET /approvals` returns. Other published ids, such as
    `ozav_q_` for quotes, are not used in a path and are not listed here:

    | prefix | resource |
    |---|---|
    | `ozav_cus_` | customer |
    | `ozav_ap_` | associated person |
    | `ozav_pd_` | payout destination |
    | `ozav_tr_` | transfer |
    | `ozav_acc_` | account |
    | `ozav_wal_` | wallet |
    | `ozav_card_` | card |
    | `ozav_key_` | API key |
    | `ozav_scr_` | screening |
    | `ozav_vrf_` | verification |
    | `ozav_wh_` | webhook endpoint |
    | `ozav_txi_` | transaction intent |
    | `ozav_apr_` | approval |
    | `ozav_lst_` | list entry (your own block/allow lists) |
    | `ozav_dec_` | decision |

    **An id whose prefix does not match the position it was used in returns
    `404`, not `400`.** The request never reaches a lookup - the id could not
    name a resource of that type - but the status is `404` so that one status
    covers every "you cannot have this resource" case. Branch on `code` and
    read `message`, not on the status, to tell the cases apart.

    The message names the prefix that position expects, and, when the id
    carries a prefix from the table above, names which resource it belongs to
    instead. Sending a `ozav_cus_…` where a wallet id belongs tells you exactly
    that.

    **What `404` never tells you is whether a resource exists.** A well-formed
    id belonging to another account, to the other environment, or to nothing at
    all all return the same generic phrase, indistinguishable from one another.
    Only the malformed case gets the extra sentence, because that sentence is a
    statement about the text you just sent - not about our records.

    ## Networks & Assets

    `components.schemas.Network` is the public v2 wire enum today. OZAV accepts
    only `USDC` and `USDT` as blockchain assets on this surface today. Other
    stablecoins such as DAI, USDB, PYUSD, and EURC are not accepted by public
    API v2 flows unless this table and `GET /capabilities` explicitly say so.

    | Network | Public v2 status | Custody stack status | Accepted assets today |
    |---|---|---|---|
    | `ethereum` | accepted | active | USDC, USDT |
    | `base` | accepted | active | USDC |
    | `optimism` | accepted | active | USDC |
    | `polygon` | accepted | active | USDC, USDT |
    | `avalanche_c_chain` | accepted | active | USDC, USDT |
    | `worldchain` | accepted | active | USDC |
    | `unichain` | accepted | active | USDC |
    | `linea` | accepted | active | USDC |
    | `sonic` | accepted | active | USDC |
    | `sei` | accepted | active | USDC |
    | `celo` | accepted | active | USDC |
    | `zksync_era` | accepted | active | USDC |
    | `solana` | accepted | active | USDC, USDT |
    | `tron` | accepted | active | USDT |
    | `stellar` | accepted | active | USDC |
    | `tempo` | not accepted | deferred | none |
    | `arbitrum` | not accepted | active | USDC, USDT |
    | `aptos` | not accepted | deferred | none |
    | `sui` | not accepted | deferred | none |

    Active custody stack status does not mean a public end-to-end flow is live.
    Wallet provisioning, live card issuance, live BRL/Pix account opening,
    managed wallet sends, and self-custody transaction building remain gated unless
    `GET /capabilities` says your organization can use them.
  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: wallet-as-a-service
    description: Wallets in your product - self-custody wallets where your users hold the keys, and vaults whose rules and approvers are your own keys. OZAV never decides.
  - key: compliance-as-a-service
    description: Screenings, decisions and lists for your own customers, as a prepaid service with no KYB of your company.
  - key: money-movement
    description: Customers, quotes, transfers, accounts, custodial wallets and cards on OZAV rails, after your company's KYB.
  - key: yield
    description: Yield on stablecoin balances, built for your user's wallet to sign.
  - key: agents
    description: Spending mandates for AI agents acting for your company (beta).
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: Wallet as a Service Overview
    x-displayName: Overview
    description: |
      **Wallets inside your product, with the keys in your hands or your users'.** There are two models, and
      in both the decisions are yours:

      | | Self-custody wallets | Vaults |
      |---|---|---|
      | What it is | An address your user already controls | A wallet whose signing key lives in an enclave OZAV runs |
      | Who signs | Your user, outside OZAV | The vault, after your approvers release the transaction |
      | Who sets the rules | You, in your product | Your administrator keys, sealed inside the vault |
      | Tag | Self-custody Wallets | Vaults |

      The rest of this overview describes **self-custody wallets**. For vaults see the Vaults tag.

      **Self-custody wallets.** Register the address your user
      already controls, read what it holds on the network it was registered on, and get transactions built for
      your user to sign.
      OZAV never holds the key and never moves the funds.

      | | |
      |---|---|
      | Who holds the keys | Your user |
      | Who decides | You, through your API keys. OZAV builds the transaction; your user signs and broadcasts it |
      | Rules | Yours. OZAV runs no KYB and no sanctions screening here: the wallet, the key and the decision are yours |
      | Going live | Accept the terms in the portal and add prepaid credit. No KYB of your company |
      | Networks | Transactions are built for EVM networks |

      **The flow**

      1. `POST /wallets` registers the address under your own `owner_ref` for that user. No OZAV customer is
         created.
      2. `GET /wallets/{wallet_id}/onchain-balances` reads what is at the address, per currency, on the network
         the user registered.
      3. `POST /wallets/{wallet_id}/transaction-intents` builds the unsigned transaction. Your user signs and
         broadcasts it.

      **Access.** The `self_custody` product is enabled per organization, in the sandbox as well: ask OZAV to
      turn it on, and it shows up in `GET /capabilities`.
  - name: Self-custody Wallets
    description: |
      **Register, read and build.** A wallet here is an address your user already controls: OZAV
      records it under your `owner_ref`, reads what it holds on chain, and builds the unsigned transactions your
      user signs. OZAV never holds the key and never moves funds.
  - name: Vaults
    description: |
      **Closed for now.** Every route here answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the
      platform. The model is built and tested, and it has not yet been proven end to end against the production enclave.
      The `rules` of a vault accept destinations, a cap per send with its asset, and `exportable`; other rule fields
      are refused with `400`, never ignored.

      A **vault** is a wallet whose rules and approvers belong to you. OZAV runs the secure enclave that signs,
      but it holds no key that can sign alone and it decides nothing: your administrator keys set the rules, your
      approver keys release each transaction, and the enclave checks every one of them again before it signs.

      **The flow**

      1. `POST /vault-roots` registers your administrator keys (Ed25519) and the administrator quorum.
      2. `POST /vault-roots/{root_id}/grants` stores a grant: the approvers (Ed25519, P-256 or passkey), how many
         must approve, the cap and the expiry. Your administrators sign it, and the API checks the quorum before it
         stores anything. `POST /vault-roots/{root_id}/grants/{grant_id}/revoke` ends it.
      3. `POST /vaults` creates the vault from a root and its rules, signed by your administrators. It answers
         `202`: poll `GET /vaults/{vault_id}` until `state` is `active`.
      4. `POST /vaults/{vault_id}/transactions` describes one transfer and returns the **challenge**, a 32-byte
         hash that names the vault, the grant, the rules, the derivation path and the exact transaction bytes.
      5. Each approver signs the challenge with their own key and calls `POST .../approvals`.
      6. When the quorum is met the vault signs. `GET .../transactions/{transaction_id}` returns the
         `signed_transaction`, and you broadcast it.

      `POST /vaults/{vault_id}/destination-grants` lets your administrators permit new destinations without
      recreating the vault. `GET /vaults/attestation` returns the enclave attestation, so you can check which code
      is running before you trust it.

      **What OZAV does not do.** It holds no key that signs alone: two of the three key shares are outside OZAV, with
      you. It never approves, overrides a rule or signs without your quorum. It never broadcasts a vault's funds.

      **Revoking.** A revoked grant authorizes no new transaction or approval (`409 GRANT_REVOKED`). The check is made
      by the API (`enforcement: "api"`): an approval you already signed stays valid inside the enclave until its own
      deadline.

      **Errors you will meet.** `409 VAULT_NOT_ACTIVE` (the vault is still `pending` or `failed`),
      `409 GRANT_REVOKED`, `409 TRANSACTION_NOT_AWAITING_APPROVALS`, `409 TRANSACTION_APPROVAL_WINDOW_CLOSED`,
      `422 GRANT_NOT_FOR_THIS_VAULT` (the grant belongs to another root), `422 APPROVER_NOT_IN_GRANT`,
      `422 APPROVAL_SIGNATURE_INVALID`, `422 GRANT_BELOW_THRESHOLD` and `422 DESTINATION_GRANT_BELOW_THRESHOLD`
      (not enough valid administrator signatures). The malformed-body `400` names the field in `param`.

      **Limits.** `user_envelope` up to 16 KiB, `raw_transaction` up to 64 KiB, up to 64 destinations per list,
      32 approvers per grant, 64 signatures per list, and approval windows of up to 7 days. A passkey approver
      needs `rp_id` and `origins`.

      **Access.** The `self_custody` product, with the `wallets:read` and `wallets:write` scopes. No KYB of your
      company.
  - name: Compliance as a Service Overview
    x-displayName: Overview
    description: |
      **Compliance checks inside your own flows.** Screen people and businesses against designated lists,
      ask a yes-or-no risk question in one call, and keep your own block and allow lists. OZAV runs the checks
      on its own engines and data and returns the evidence. It blocks nothing through these routes: what
      happens next is your decision, including on a sanctions-list hit. Your decisions apply to your
      screenings only; the customers OZAV moves money for go through OZAV's own screening.

      | | |
      |---|---|
      | Who decides | You, on OZAV's evidence |
      | Going live | Accept the terms in the portal and add prepaid credit. No KYB of your company |
      | Your data | No customer record is needed: send the subject inline. A decision stores the verdict, never your request |

      **The flow**

      1. `POST /decisions` answers one question in one call: `ALLOW`, `REVIEW` or `DENY`, with the reasons and
         the checks behind it.
      2. `POST /screenings` returns the matches themselves when you need them: which list, which name, how
         strong the match is.
      3. `PATCH /screenings/{screening_id}/hits/{hit_id}` records your decision on a hit, sanctions lists
         included.
      4. `POST /lists/entries` keeps your own block and allow lists, applied to every screening.
      5. `POST /decisions/{decision_id}/outcome` records what you did with a decision.
  - name: Decisions
    description: |
      **One call, one verdict.** Ask a yes-or-no risk question and get `ALLOW`, `REVIEW` or `DENY`, with
      the reasons and the checks behind it.

      | `type` | The question |
      |---|---|
      | `name.match` | Is this name on a sanctions list? |
      | `destination.ownership` | Is this Pix destination the customer's own account? |
      | `wallet.risk` | Is this address on a sanctions list? |
      | `transaction.outbound` | All of the above for one outgoing payment; the most severe verdict wins |

      Every verdict comes from OZAV's own engines and data, with no third-party call. Missing or stale data
      answers `REVIEW`, never `ALLOW`, and `DENY` comes only when OZAV holds the proof: an address on the list,
      or a listed name matched by the date of birth. The verdict is advice; what you do with it is your
      decision. `wallet.risk` covers sanctions lists only, not blockchain analytics.

      Report what you did with `POST /decisions/{decision_id}/outcome`. OZAV stores the verdict, never your
      request.
  - name: Screenings
    description: |
      **The matches themselves, when you need to see them.** Screen a person, a business or an existing
      customer against OZAV's designated-persons data (UN, OFAC, EU, UK and national sources, refreshed daily),
      with no customer record required. Each hit names the list, the matched name and how strong the match is.
      An inconclusive subject is flagged for review, never cleared silently.

      Record your decision on a hit with `PATCH /screenings/{screening_id}/hits/{hit_id}`. Your decision is
      the final one, on any hit.
  - name: Lists
    description: |
      **Your own block and allow lists**, applied to every screening on top of OZAV's data: names,
      documents, e-mails and e-mail domains.
  - name: Money Movement Overview
    x-displayName: Overview
    description: |
      **A regulated platform that moves money for your customers.** Onboard a customer once, then open
      named accounts, price and execute FX, pay out, hold balances in custody and issue cards. OZAV operates
      every step and runs the compliance behind it.

      | | |
      |---|---|
      | Who decides | OZAV, as the regulated operator: customer approval and limits |
      | Going live | KYB of your company and a signed contract. OZAV issues your live keys |
      | Billing | Monthly invoice |
      | Pricing | Exchange conditions: a spread in integer basis points, on every quote |

      **The flow**

      1. **Check your access.** `GET /capabilities` returns the products enabled for your organization and
         what your key can call.
      2. **Onboard the customer.** `POST /customers` with the profile, then the documents with
         `POST /customers/{customer_id}/documents`. In `reliance` mode, the default for API integrators,
         OZAV reviews the completed profile and the identity document (the company registration for a
         business). In `full` mode, `POST /verifications` runs the identity check, and on `live` the customer
         first accepts the terms: `POST /customers/terms-sessions` and `POST /customers/terms-sessions/accept`,
         then pass the accepted `terms_session_id` on `POST /customers`. OZAV decides; follow it on
         `GET /customers/{customer_id}` or the `customer.updated` webhook.
      3. **Receive.** `POST /accounts` opens a named account for the customer: in the customer's own name on
         Brazilian rails, and segregated and attributed to the customer under OZAV on foreign-currency rails.
      4. **Price.** `POST /quotes` returns the exchange conditions before anything moves.
      5. **Move.** `POST /transfers` for on-ramp, off-ramp and FX; `POST /payout-destinations` registers
         where the money lands. A Pix destination must be in the customer's own name, unless OZAV has
         enabled third-party payouts for your organization; the other rails can pay a third party.
      6. **Hold.** Custodial wallets: `GET /wallets`, balances, deposit addresses, send.
      7. **Spend.** `POST /cards` issues a card on the customer's balance.
      8. **Reconcile.** `GET /transactions` lists every movement in one place.

      ### Accounts and wallets are different things

      They are asked for separately, they hold different value, and they demand
      different evidence. Integrators conflate them constantly, so here is the
      contrast in one place.

      An **account** (`ozav_acc_`) is a named fiat receiving instruction. "Named"
      is the load-bearing word: the instruction carries your end customer's
      identity rather than being a shared OZAV address. That is why it needs
      identity - a bank credits a person, and the person has to be established
      before the instruction exists.

      How literal "named" is depends on the rail. On Brazilian rails the account
      is held in your end customer's own name at the institution. On the
      foreign-currency rails the holder of record is OZAV, and the account is
      segregated and attributed to your end customer - the same money separation,
      a different legal holder. If who holds the account at the institution
      matters to your contracts, treat those two as different products.

      Accounts are created with `POST /accounts`, which requires an `approved`
      customer and can still refuse: activation for the money paths is a further
      internal step with no public field (see "`approved` is not the finish
      line" in the **Customers** tag).

      A **wallet** (`ozav_wal_`) holds crypto. It comes in two kinds, and only one
      of them is ever created through the API:

      - **managed** - OZAV holds the key. It is provisioned as part of the
        customer when your organization has the custody product; you never request
        it directly.
      - **self-custody** - your end user holds the key and the address already
        exists. `POST /wallets` **records** it. Nothing is created, and OZAV never
        holds those funds.

      | | account | wallet |
      |---|---|---|
      | what it holds | fiat, at a bank | crypto, on a chain |
      | created by | `POST /accounts` | managed: with the customer. self-custody: `POST /wallets` |
      | needs identity? | yes - customer `approved`, and activation can still refuse | no - self-custody records an address |
      | needs an address? | no | self-custody: yes, and it must match the chain |

      Read `POST /wallets` for what recording an address does and does not prove.

      Recording a self-custody address needs no identity, but it does need the
      `self_custody` entitlement on your organization - a newly created
      organization does not get it by default and receives `403
      ENTITLEMENT_REQUIRED`. A customer that already holds a managed wallet is
      refused with `409 WALLET_ALREADY_MANAGED`: a customer is one or the other,
      never both.

      ### In the sandbox

      Read this before you plan a schedule around it. Each row below is a place
      where the resource is created but the flow cannot be finished with a
      self-serve `test` key, and each needs OZAV to act on your account. None of
      them is a bug you can work around by changing your request.

      | You want to | What happens | What unblocks it |
      |---|---|---|
      | 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 + screening are in and, for an individual, its `identity_document` is `received` (in sandbox its presence is enough) | 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. |
      | 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 for a `full` customer is refused with `422 TERMS_ACCEPTANCE_REQUIRED` unless you pass an accepted session (`reliance` customers skip this). Collect it with `POST /customers/terms-sessions` and `POST /customers/terms-sessions/accept` (both public), and pass the id on `POST /customers`. Build that into your flow now; the sandbox will not remind you. |
      | Render the Pix payment screen | 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`. |
      | Pay out to a Pix key (off-ramp) | Works end to end: the sandbox pays the destination, the transfer lands `completed` and `transfer.completed` fires. | Nothing to unblock. In `live` a Pix-key destination is payable as soon as it is `active` too; read `capabilities.receive` on the destination (true for an active Pix-key destination in both environments), never `status`. |
      | Test a card authorization | Every `/cards` route answers `403 ENTITLEMENT_REQUIRED` when your organization does not have the `cards` product. A sandbox organization receives it at creation; an older one may not have it | Check `entitlements` on `GET /capabilities` and ask OZAV to grant `cards` if it is missing. After that, a customer's first card is `active` but the customer's card balance starts at zero in the sandbox, so every purchase declines with `Insufficient balance` until you load it with `POST /cards/{card_id}/simulate/funding`. The 10,000 USDC the sandbox wallet shows does not fund cards. See `funding_type` on `Card`. |
      | 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`. |
      | Open a named account for a holder | The sandbox runs the same profile gate as `live`, only shallower. 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`. For a Brazilian business it also requires the two declarations, `business.bc_protege_disabled` and, when the institution asks for the terms, `business.account_opening_terms_accepted_by`. What it skips is everything else the institution asks for (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 named account opens for an individual on that rail today - and the sandbox opens it anyway. If your flow is individual + BRL, sending more documents will not change the answer; see "Opening an account in USD, EUR or GBP" in the **Customers** tag. |
      | 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. | 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 a round number, so nobody mistakes it for money. |

      Everything else in this section works end to end in the sandbox, including
      a full BRL on-ramp transfer, screening against designated lists with no
      customer record, document upload, and webhook delivery with signature
      verification.
  - name: Capabilities
    description: |
      **Know what your key can do before you call.** `GET /capabilities` returns the products enabled for
      your organization, the scopes on the key you call it with, and what each feature needs. Branch on it
      instead of guessing.
  - name: Customers
    description: |
      **Onboard once, then move money.** A customer is the person or business OZAV moves money for: an
      `individual` or a `business`, created once with the identity you hold, with associated persons (owners,
      directors, signers) for a business. Accounts, payout destinations, custodial wallets, quotes and transfers
      all reference its `customer_id`.

      OZAV, as the regulated operator, verifies the customer and decides; follow `decision` on
      `GET /customers/{customer_id}` or the `customer.updated` webhook. What is still owed comes back on the
      record itself, in `requirements`. Creating the customer asks for no documents: register them with
      `POST /customers/{customer_id}/documents` when `requirements` asks.

      ### What each customer type must provide

      The required set is driven by the **country on the tax identity**, not by
      nationality. A Brazilian tax id (`cpf`, or `country: BR`) puts the customer
      on the Brazilian profile; anything else puts them on the base profile. Send
      `nationality` if you have it - it feeds risk assessment and will not change
      a single required field.

      `POST /customers` always requires `type`, `email`, `legal_name` and the tax
      identity for the type - and a tax identity is three parts, not one:
      `value`, `type` and `country`. Every address also carries `country`.

      The tables below are the **request fields**: send all of them and the
      customer is created and complete. They are not a transcription of
      `requirements.missing_fields`, which reports one flat token per concept from
      a closed list (`phone`, `residential_address`, `tax_id`, ...) and never a
      sub-path like `phone.country_code`. Read the tables to build the request;
      read `missing_fields` to find out what a customer you already created is
      still missing.

      ##### Individual, tax id issued in Brazil

      | field | rule |
      |---|---|
      | `individual.legal_name` | full legal name |
      | `individual.tax_id.value` | the CPF; its check digit is verified |
      | `individual.date_of_birth` | ISO date |
      | `individual.phone.country_code` | dial code |
      | `individual.phone.number` | subscriber number |
      | `email` | top level, not inside the block |
      | `individual.residential_address.line1` | street |
      | `individual.residential_address.number` | required in Brazil |
      | `individual.residential_address.neighborhood` | required in Brazil |
      | `individual.residential_address.city` | non-blank |
      | `individual.residential_address.state` | required in Brazil |
      | `individual.residential_address.postal_code` | required in Brazil |
      | `individual.residential_address.country` | `BR`; required on every address |
      | `individual.source_of_funds` | declaration |
      | `individual.expected_monthly_volume_usd` | declaration |
      | `individual.monthly_income_usd` | declaration; the capacity the expected volume is measured against |
      | `individual.occupation` | profession |
      | `individual.nationality` | ISO 3166-1 alpha-2 |
      | `individual.is_pep` | optional politically-exposed-person declaration; see below the tables |
      | `individual.account_purpose` | why the account is being opened |

      ##### Individual, tax id issued outside Brazil

      | field | rule |
      |---|---|
      | `individual.legal_name` | full legal name |
      | `individual.tax_id.value` | `ssn`, `nif`, `passport` or `other`; no check digit is applied |
      | `email` | top level |
      | `individual.date_of_birth` | ISO date; every individual owes it, not only Brazilian ones |
      | `individual.phone.country_code` | dialling code, e.g. `+1`; a foreign-currency account cannot be opened without it |
      | `individual.phone.number` | national number |
      | `individual.residential_address.line1` | street |
      | `individual.residential_address.city` | non-blank |
      | `individual.residential_address.postal_code` | required for any account, not only Brazilian ones: the named-account holder registration refuses without it |
      | `individual.residential_address.country` | ISO 3166-1 alpha-2 |
      | `individual.phone.country_code` | dialling code, digits only, no `+` |
      | `individual.phone.number` | mobile; a landline is not accepted for the named-account holder record |
      | `individual.source_of_funds` | declaration |
      | `individual.expected_monthly_volume_usd` | declaration |
      | `individual.monthly_income_usd` | declaration; the capacity the expected volume is measured against |
      | `individual.occupation` | profession |
      | `individual.nationality` | ISO 3166-1 alpha-2 |
      | `individual.is_pep` | optional politically-exposed-person declaration; see below the tables |
      | `individual.account_purpose` | why the account is being opened |

      **`individual.is_pep` is optional.** OZAV screens every individual customer
      against politically-exposed-person lists. An individual you did not declare,
      whose recent OZAV screening read the PEP lists without a match, is recorded
      by OZAV as not a PEP, and `requirements.missing_fields` does not ask for
      `is_pep`. A declaration you send always takes precedence; `true`, or a PEP
      match in OZAV's screening, goes to OZAV compliance review.

      ##### Business, registration issued in Brazil

      | field | rule |
      |---|---|
      | `business.legal_name` | full legal name |
      | `business.registration.value` | the CNPJ; its check digit is verified |
      | `business.phone.country_code` | dial code |
      | `business.phone.number` | subscriber number |
      | `email` | top level |
      | `business.registered_address.line1` | street |
      | `business.registered_address.number` | required in Brazil |
      | `business.registered_address.neighborhood` | required in Brazil |
      | `business.registered_address.city` | non-blank |
      | `business.registered_address.state` | required in Brazil |
      | `business.registered_address.postal_code` | required in Brazil |
      | `business.registered_address.country` | `BR`; required on every address |
      | `business.company_type` | legal form |
      | `owners` | at least one associated person owning 25% or more |
      | `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 |
      | `business.source_of_funds` | declaration |
      | `business.expected_monthly_volume_usd` | declaration |
      | `business.annual_revenue_usd` | declaration; the capacity the expected volume is measured against |
      | `business.expected_daily_volume_usd` | declaration; not derived from the monthly figure |
      | `business.is_regulated_activity` | boolean; `false` counts as answered |
      | `business.nature_of_business` | what the company actually does |
      | `business.website` | public site or profile |
      | `business.account_purpose` | why the account is being opened |
      | `business.incorporation_date` | ISO date |

      ##### Business, registration issued outside Brazil

      | field | rule |
      |---|---|
      | `business.legal_name` | full legal name |
      | `business.registration.value` | `ein`, `company_number` or `other`; no check digit is applied |
      | `email` | top level |
      | `business.phone.country_code` | dialling code, e.g. `+1`; a foreign-currency account cannot be opened without it |
      | `business.phone.number` | national number |
      | `owners` | at least one associated person owning **25% or more**. The 25% floor is FATF, not a Brazilian rule |
      | `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 |
      | `business.registered_address.line1` | street |
      | `business.registered_address.city` | non-blank |
      | `business.registered_address.postal_code` | required for any account, not only Brazilian ones: the named-account holder registration refuses without it |
      | `business.registered_address.country` | ISO 3166-1 alpha-2 |
      | `business.phone.country_code` | dialling code, digits only, no `+` |
      | `business.phone.number` | mobile; a landline is not accepted for the named-account holder record |
      | `business.source_of_funds` | declaration |
      | `business.expected_monthly_volume_usd` | declaration |
      | `business.annual_revenue_usd` | declaration; the capacity the expected volume is measured against |
      | `business.expected_daily_volume_usd` | declaration; not derived from the monthly figure |
      | `business.is_regulated_activity` | boolean; `false` counts as answered |
      | `business.nature_of_business` | what the company actually does |
      | `business.website` | public site or profile |
      | `business.account_purpose` | why the account is being opened |
      | `business.incorporation_date` | ISO date |

      ##### Conditional: a business that declares a regulated activity

      The four tables above list what is asked of every customer of that shape.
      One requirement is conditional and therefore is not a row in them.

      When `business.is_regulated_activity` is `true`, two further fields become
      required and appear in `requirements.missing_fields`:

      | field | rule |
      |---|---|
      | `business.regulator` | name of the supervising authority |
      | `business.regulated_activity_details` | licence or scope of the regulated activity |

      They are asked for **only** on a `true` answer. `false` is an answer - the
      company has declared it is not regulated, and asking who supervises it would
      be asking for the detail of something it denied. An **absent**
      `is_regulated_activity` is an open question: `missing_fields` asks for the
      question itself and not for its follow-up, so you are never asked to name a
      regulator for an activity you never said was regulated.

      ##### Opening an account in USD, EUR or GBP

      **Read this before you build against it.** `sepa`, `swift` and
      `faster_payments` are **live behind enablement** - the corridor is served in
      production, but opening an account for a given customer also requires that
      OZAV has enabled that customer for foreign-currency named accounts. A `live`
      request for a customer who is not enabled is refused with a named error that
      says so.

      `fedwire` stays `sandbox only`: no US-domiciled account has ever been issued
      on this corridor. A USD account is issued as a **GB IBAN** and reached by
      `swift`; build against that shape.

      `Rail.x-ozav-rail-availability` is the authoritative table; this paragraph
      describes it and is checked against it.

      The field list below is published so you can build the request now and not
      change it later - it is what the holder record asks for. It is not a
      statement that the account opens on a `live` key today. Availability is
      answered by that table, never by this section.

      **Business customers only, today.** A foreign-currency account is filed
      against a holder record registered as a company, so
      `type: individual` is refused there with
      `422 ACCOUNT_HOLDER_TYPE_NOT_SUPPORTED`. Opening one would file a natural
      person as a company, with their personal tax id standing in for both the
      company's tax id and its registration number.

      **BRL/Pix is the same today, and the sandbox does not enforce it.** A
      `live` key asking for a BRL named account with `type: individual` is
      refused with `422 ACCOUNT_HOLDER_TYPE_NOT_SUPPORTED`. A `test` key opens it
      anyway - the sandbox provisioner does not read the holder type, so it
      answers `201` for exactly the request `live` refuses. An individual BRL
      flow built against a test key will not survive the switch to a live key,
      and the sandbox will not warn you.

      An individual can still be onboarded, verified, and hold a wallet. Build
      named accounts for `type: business`.

      **A company registered outside Brazil can hold one.** `registration.type`
      may be `ein`, `company_number` or `other`, and the value is sent as declared
      - no Brazilian format is imposed on a foreign registration, and
      `registration.country` is what determines the country of record.

      The reverse also holds: an account on a
      **Brazilian** rail does need a Brazilian tax identity. A customer whose
      registration is an `ein` is refused there with
      `422 ACCOUNT_HOLDER_TYPE_NOT_SUPPORTED`, naming the declared type - the
      customer is valid, only that corridor needs a CPF or CNPJ.

      Registering that holder needs a slightly fuller set than onboarding alone,
      and the difference is the part integrators hit late:

      | field | note |
      |---|---|
      | `email` | top level |
      | `business.phone.number` | the subscriber number |
      | `business.phone.country_code` | dial code, e.g. `55` |
      | address `line1` | street |
      | address `city` | non-blank |
      | address `postal_code` | **required here**, and onboarding does not ask for it |
      | address `country` | **required here**, ISO 3166-1 alpha-2 |
      | address `state` | optional |

      **And one authorised person, by name.** The holder record is not complete
      without a natural person attached to it - the individual who controls or
      signs for the company. A holder with nobody attached stays pending at the
      bank and never receives coordinates.

      | field | note |
      |---|---|
      | person `firstName` | given name, as on the identity document |
      | person `lastName` | family name |
      | person `birthDate` | ISO date, `YYYY-MM-DD` |
      | person `roles` | at least one of `BeneficialOwner`, `AuthorizedPerson`, `Director` |
      | person `gender` | `male`, `female` or `other`, as the person declares it - send `individual.gender` on the associated person |
      | person `ownershipPercent` | only when the person is an owner; omitted otherwise |

      **`legal_name` on an associated person must be at least two words.** OZAV
      splits it into a given and a family name for the bank record, and a
      single-word name cannot be split. A person whose name will not split is
      skipped when OZAV picks who signs, so if they were your only signer the
      holder is left with nobody attached. `requirements` reports it as
      `individual.legal_name`, the same token as a missing name, because the fix
      is the same: correct the name.

      `ownershipPercent` is sent **only** for a person declared as an owner.
      Declaring a stake for someone who does not hold one is a false ownership
      statement, and OZAV will not make it on your behalf.

      **`postal_code` and a structured `phone` are the two that catch people
      out.** Outside Brazil, `requirements.missing_fields` asks only for
      `line1`, `city` and `country` on the address, and it does not ask for a
      postal code at all - that set is enough to complete onboarding, and not
      enough to open this account. Send both up front and you will not have to go
      back to your customer for them later.

      **Ownership.** This account can only be opened for a holder with a single
      controlling owner, and OZAV has to be able to establish that it is one. A
      company whose ownership is split across several qualifying owners - or whose
      ownership OZAV cannot resolve - is refused rather than filed under a
      declaration that does not describe it. If that is your case, talk to us
      before integrating.

      **These refusals name the customer, not the corridor.** They arrive as
      `422 ACCOUNT_HOLDER_TYPE_NOT_SUPPORTED`, a different code from
      `ACCOUNT_RAIL_NOT_AVAILABLE`: requesting a different currency or
      rail will not change the answer, because the answer is about who the holder
      is.

      **Approval is not instant, and the wait has its own code.** Once the
      holder is registered, the institution that holds the account reviews it
      before the account can receive anything, and states 24 to 48 hours for that. A `POST /accounts`
      during that window returns `409 ACCOUNT_HOLDER_PENDING_APPROVAL`. Nothing in
      your request is wrong and there is no field to correct - send the same
      request again once the review clears.

      Do not poll it in a tight loop. The code is the same until the review
      finishes, so retrying every few seconds only produces the same answer.

      **Documents are required here too.** The `POST /accounts` call itself
      carries structured data only, with no file attached - but that is a fact
      about the call, not about the requirement. **A holder with no dossier is a
      bank account that never receives.**

      The dossier is the same list as for a Brazilian account - the six below,
      plus an identity document for each associated person. Send it once; it is
      not collected twice because a second currency was opened later.

      **Sending the dossier does not, by itself, make the account receive.** The
      documents are what a compliance review needs in order to run; the review
      itself happens at the institution, on its own clock, and the account becomes
      payable when that review clears - not when the upload succeeds. A holder
      still awaiting that review can sit unchanged for weeks.

      So: a complete dossier is the part you and OZAV control, and it is a
      precondition, not a switch. Plan the go-live of a foreign-currency corridor
      around a review you do not schedule, and do not quote your own customer a
      date that depends on it.

      ##### Which documents, for whom, and for which account

      Two mechanisms establish identity. An individual uses only the first; a
      business uses both.

      | | how identity is established | files you upload |
      |---|---|---|
      | individual | a hosted verification session captures the document, liveness and face match (`POST /verifications`) | none |
      | business | hosted session for KYB/UBO, **plus** the company dossier you upload (`POST /customers/{customer_id}/documents`) | the list below |

      An individual is not asked for file uploads. Start a verification instead
      and the session collects the identity artifacts directly from the person -
      that is what `kyc_document`, `liveness` and `face_match` mean in
      `CapabilityKind`. Uploading files for an individual is neither required nor
      a substitute for the session.

      **Proof of address and a bank statement are asked of every business**,
      whatever currency it holds: they are KYB, not a property of a payment rail.
      A Brazilian company is additionally asked for the rail's own list, so the
      Brazilian set is the larger one.

      **A tax return is not required of anyone** - not a business, not an
      individual, at any volume. What OZAV needs is economic capacity, and that is
      already a required field (`annual_revenue_usd` for a business,
      `monthly_income_usd` for an individual). A tax return varies by jurisdiction
      and much of the world has no direct equivalent; the structured field answers
      the same question and compares across borders.

      Both sets are collected the same way, and OZAV keeps one canonical record:
      nothing you already sent has to be sent again because a rail was chosen
      later. A Brazilian account chosen later is still the first time the rail's
      extra documents are asked for - plan for that if you may offer BRL later,
      because collecting from your customer after the fact is exactly the
      round trip this section exists to prevent.

      ##### Supporting documents a business must upload

      **Uploading a document is two calls, and the first one alone does not
      count.** `POST /customers/{customer_id}/documents` registers the document and
      returns an `upload_url`; the record sits at `pending_upload`, which is
      treated as **not sent**. The bytes go in the second call, and only then does
      the record become `received` - the state the account gate accepts.

      A `201` from the first call is not a delivered document. If you stop there,
      the dossier looks complete on your side and is empty on ours, and the account
      is refused for a document you believe you sent. (There are records sitting at
      `pending_upload` in production today for exactly this reason.)

      `requirements.missing_fields` covers request fields. Documents are uploaded
      through `POST /customers/{customer_id}/documents` **plus the upload call**,
      and are not reported there, so they are listed here instead - opening a Brazilian account asks
      for all seven, plus four documents per associated person (see below), before it will
      proceed. A `test` key does not enforce this: the sandbox opens the account
      with no documents at all, so sandbox success is not evidence the dossier is
      complete.

      **Documents are required in every currency.** Opening a `live` account in
      USD, EUR or GBP applies the same gate as BRL, with the same
      `422 ACCOUNT_PROFILE_INCOMPLETE` and the same `missing` list, entries
      prefixed `documents.`:

      | holder | documents required to open a non-BRL account |
      |---|---|
      | individual | `identity_document`, `proof_of_address`, `bank_statement` |
      | business | `company_registration`, `ownership_structure`, `proof_of_address`, `bank_statement` |

      A Brazilian business asks for more, because a Brazilian payment institution
      does: `company_registration`, `tax_registration` (the Cartão
      CNPJ), `ownership_structure`, `proof_of_address`, `financial_statement`
      (satisfied by `income_tax_return` too - a balance sheet, a DRE or the IRPJ;
      a bank statement is not accepted as economic capacity), `bank_statement` and
      `source_of_funds`; and, for each declared associated person (every person
      you register, whatever the share - the payment institution reviews all of
      them as representatives): `identity_document`, `selfie` (or `liveness`) and
      `proof_of_address` (upload with `subject_type: associated_person`). The
      person's politically-exposed-person declaration (`pep_declaration`) is
      optional; see below for the one account that asks for it. You do not have to
      reconcile the two lists yourself: `GET /customers/{customer_id}/documents`
      returns `still_required` for that customer, and it is computed by the same
      code the account gate runs - what it names is exactly what the gate will ask
      for.

      The per-person list comes back the same way: `GET /customers/{customer_id}/documents`
      returns `still_required_by_person`, one entry per associated person with that
      person's public id, `still_required` and `pending`; and `POST /accounts` refuses
      with the same names, `associated_persons[i].documents.<doc_type>`, in the same
      `missing` list as the company's `documents.<doc_type>`.

      | `doc_type` to send | what it establishes |
      |---|---|
      | `company_registration` | the company exists and is currently registered |
      | `ownership_structure` | who ultimately owns it |
      | `proof_of_address` | where it operates from |
      | `financial_statement` | its financial position |
      | `bank_statement` | recent account activity |
      | `source_of_funds` | where the money comes from |
      | `tax_registration` | The company's tax registration card (Brazil: Cartão CNPJ). Required for a Brazilian business. |
      | `income_tax_return` | The company's income tax return (Brazil: IRPJ). Satisfies `financial_statement` as economic capacity; a bank statement does not. |
      | `selfie` | An associated person's face image (`subject_type: associated_person`); `liveness` satisfies it too. Required per ≥25% owner or signer of a Brazilian business. |
      | `pep_declaration` | An associated person's own politically-exposed-person declaration (`subject_type: associated_person`). Optional; only a BRL account asks for it (see below). Instead of uploading it, you can answer `individual.is_pep` on each associated person. |

      **BC Protege+ must be off.** A Brazilian business also declares
      `business.bc_protege_disabled: true` (`PATCH /customers/{customer_id}`) once it
      has turned off BC Protege+ with its own gov.br login
      (https://www.gov.br/pt-br/servicos/proteger-se-com-o-bc-protege) - with the service on, no
      institution can open an account in its name. Until then `POST /accounts` lists
      `business.bc_protege_disabled` in `missing` (sandbox too, where the same
      `PATCH` clears it).

      **The account opening terms.** When the institution that opens the BRL
      account asks for it, a legal representative accepts the account opening
      terms on behalf of the business before the account is requested. You do
      not need to know whether it asks: you can declare the acceptance for any
      Brazilian business before `POST /accounts`, and when the institution does
      not ask for the terms it is recorded and has no effect. Send
      `business.account_opening_terms_accepted_by` - the id of that associated
      person of the business (pending or approved, not suspended), who needs a
      Brazilian CPF in `individual.tax_id` - with
      `PATCH /customers/{customer_id}`. OZAV records the acceptance as declared
      by your organization through the API and delivers the institution's term
      on its next account-opening pass (every 30 minutes); sending the same person again, with the same name and CPF, records no new acceptance. Until then
      `POST /accounts` refuses with `422 ACCOUNT_PROFILE_INCOMPLETE`, with
      `business.account_opening_terms_accepted_by` as an `error.details[].param`
      (code `missing_field`; sandbox too), and the open item in
      `requirements.documents_due` has the key `account_opening_terms_acceptance`.

      **The PEP declaration is optional.** OZAV screens every associated person
      against politically-exposed-person lists. A person you did not declare,
      whose recent OZAV screening read the PEP lists without a match, is recorded
      by OZAV as not a PEP, and nothing is asked of you. A declaration you send
      always takes precedence, and `true` (or a PEP match in OZAV's screening)
      goes to OZAV compliance. `GET /customers/{customer_id}/documents` and the
      account gate do not list `pep_declaration`.

      The exception is a BRL account: the Brazilian payment institution requires
      a declaration collected from the customer, and OZAV's screening does not
      replace it. For a BRL account, once the per-person documents above are in,
      `POST /accounts` lists `associated_persons[i].documents.pep_declaration` in
      `missing` until each person has one (it is checked with the institution's
      own requirements, after the general list), and the item appears in
      `requirements.documents_due` once the institution asks for it. Answering
      `individual.is_pep` early avoids that second round.

      **For a BRL account the PEP declaration can be an answer instead of a file.** Send
      `individual.is_pep` (`true`/`false`) when you create or update each associated
      person (`POST`/`PATCH /customers/{customer_id}/associated-persons`). When every
      associated person of the business answers `false`, OZAV issues the
      `pep_declaration` itself - one term per business, citing the field, your
      organization and when each answer was sent - on its next account-opening pass
      (every 30 minutes), and the requirement is met. OZAV only issues the term when
      its own recent screening of every associated person read the PEP lists without
      a match; until then the term waits (no action needed from you). `true` on
      anyone, a person without an answer, or a PEP match in OZAV's own screening of
      any person means OZAV does not issue the term: `pep_declaration` stays in the
      `missing` list, and you send it as a document for that person. The term speaks
      for the whole business, so it is withdrawn for the whole business: if, while
      the business account is being opened (on OZAV's next pass, every 30 minutes),
      any person has `is_pep: true` or OZAV's screening finds a PEP match on anyone,
      every `pep_declaration` document of the term (one per person) becomes
      `rejected`, `pep_declaration` goes back to `missing` and its item in
      `requirements.documents_due` back to `due` with a new 30-day `due_at` and
      `reason_code: document_invalid`, announced by a `requirement.due` webhook.
      From the moment a person has `is_pep: true`, the term already does not count
      for opening the account (`POST /accounts` lists `pep_declaration` in
      `missing`), even before that pass. Send a `pep_declaration` document for each
      person; OZAV issues a new term only when every answer is `false` again and its
      screening of everyone is clear. A `PATCH` of an associated person whose
      `individual` leaves out `is_pep` keeps the answer already given; send `is_pep`
      to change it.

      For USD, EUR and GBP accounts, the authorized person of a business must also
      declare `individual.gender` (`male`, `female` or `other`). Until it is sent, the
      account stays pending. As with `is_pep`, a `PATCH` that leaves it out keeps it.

      These are the literal `doc_type` values - any other string is rejected with
      `400`. For a non-Brazilian business one `bank_statement` counts for two of
      them (financial position and recent activity). For a Brazilian business it
      does not: a bank statement covers recent activity only, and the financial
      position needs `financial_statement` or `income_tax_return` - seven
      uploads for the seven requirements, plus the per-person ones.

      Three tokens in the `missing` list of `POST /accounts` are not `doc_type`
      values and must not be echoed as one: `documents.financial_statement_or_income_tax_return`
      (send either `financial_statement` or `income_tax_return`),
      `associated_persons[i].identity_document_or_approval` (send `identity_document`
      for that person, or have the person approved) and
      `associated_persons[i].representative_picture_or_waiver` (send `selfie`).
      The `i` in `associated_persons[i]` is the person's position ordered by
      creation time, oldest first - the opposite of `GET /customers/{customer_id}/associated-persons`,
      which lists newest first. `still_required_by_person` carries the person's
      public id precisely so you never have to resolve that index.

      A business also needs at least one associated person whose `roles` include
      `authorized_signer`, `officer` or `director`, in addition to the ≥25% owner
      that `missing_fields` reports as `owners` (role `ubo`). They can be the same
      person, and `roles` takes more than one value.

      **Each of those people needs an identity document of their own.** Send it
      as `identity_document` - it is not covered by any of the six company
      documents above, and it is the one integrators discover last, after the
      dossier is already assembled. A person OZAV has already approved does not
      need it re-sent.

      **You do not have to guess: `POST /accounts` tells you.** When the dossier
      is not yet complete the call is refused with `422
      ACCOUNT_PROFILE_INCOMPLETE`, and the error carries a `missing` list naming
      every field and document still needed, down to address components such as
      `business.registered_address.postal_code`. Once you have declared a person
      who is a ≥25% owner or an authorised signer, it also reports what is missing
      for that person specifically - `associated_persons[0].cpf`,
      `associated_persons[0].identity_document_or_approval`, and the person's own
      documents as `associated_persons[0].documents.selfie` and
      `associated_persons[0].documents.proof_of_address` (and, for a BRL account
      only, the person's `pep_declaration`). Before that it asks
      for the person itself (`associated_persons.ubo_25_plus`), so expect the list
      to get longer once you add one. That list is the
      authoritative checklist for opening a Brazilian account, and it is
      longer than `requirements.missing_fields` on the customer:
      `missing_fields` answers "is this customer complete?", the `422` answers "can
      this customer be given an account?" Drive your onboarding UI off the `422`
      when the customer already exists, and off the tables above when you are still
      building the create request.

      The categories above are a declaration vocabulary, not a list of which
      artifact satisfies which category in a given country: whether a specific
      national registry extract or utility bill is accepted is decided in review,
      and OZAV does not publish a per-jurisdiction acceptance table.

      **`approved` is not the finish line.** A customer can pass its checks and
      still be refused on the money paths, because OZAV activates a customer only
      once it also holds a payable identity. Today that means an account in
      Brazilian rails needs a CPF or CNPJ, so a customer whose tax identity is a
      passport can be created and verified but cannot be given a named
      Brazilian account.

      **There is no public field for that last step.** `status` goes no further
      than `approved` - the values are `pending`, `in_review`, `approved`,
      `rejected` and `suspended`, and nothing else is ever returned. Do not poll
      for a state that will not arrive. The way you find out is `POST /accounts`:
      it either opens the account or refuses with a coded error you can act on.

      ### Jurisdictions we serve

      Eligibility is decided by **declared residence and place of registration**,
      never by nationality alone.

      Customers whose tax identity or address is issued in one of these
      jurisdictions are refused at creation, with `422 JURISDICTION_NOT_SERVED`
      naming the field that caused it:

      | refused |
      |---|
      | `AF` Afghanistan, `BY` Belarus, `CU` Cuba, `IR` Iran, `KP` North Korea, `MM` Myanmar, `RU` Russia, `SD` Sudan, `SS` South Sudan, `SY` Syria, `ZW` Zimbabwe |

      These are accepted and carry enhanced due diligence. Expect longer review
      and requests for further evidence:

      | enhanced due diligence |
      |---|
      | `AL` `BB` `BF` `CD` `CM` `GH` `HT` `JM` `JO` `ML` `MZ` `NG` `NI` `PA` `PH` `PK` `SN` `TT` `TZ` `UG` `VE` `VN` `YE` |

      Everywhere else is served, subject to name screening against the
      consolidated UN, OFAC, UK and EU lists and to a risk-based decision. Being
      outside both tables means you may proceed; it does not by itself guarantee
      acceptance.
  - name: Verifications
    description: |
      **Identity and business verification for customers you onboard at OZAV.** Start checks for a
      customer (document, liveness, face match, proof of address, owners and more) and follow each outcome.
      OZAV uses the result in its own decision on the customer.
  - name: Quotes
    description: |
      **The price, before anything moves.** A quote returns OZAV's all-in rate for a corridor, with the
      exchange conditions as a spread in integer basis points, and you can add your own spread on top. Quotes
      are indicative: the rate is struck again when you create the transfer that references the quote.

      **Minimums.** Below the minimum for the fixed leg's currency a quote is refused with
      `400 AMOUNT_BELOW_MINIMUM`, and the message names the number that binds, so one refusal tells you what
      to send.

      **The BRL on-ramp floor is BRL 1,000.** Below it the order is refused, never reduced. The liquidity side
      sets its own minimum as BRL 1,000 sent or 200 USDT/USDC received; OZAV checks the BRL side because it is
      known before the rate is struck, so BRL 1,000 is the only number you need. The floor can rise if that
      minimum rises, and it never falls below BRL 1,000. A BRL to BRS purchase has no FX leg, and its minimum
      is BRL 10.
  - name: Transfers
    description: |
      **One resource for every money movement.** On-ramp, off-ramp, FX and wallet sends all go through
      `POST /transfers`; the pair `(source.type, destination.type)` selects the flow, not a different URL. A
      combination that is not served answers with a stable error code instead of a partial result. OZAV's
      compliance and limit checks run on every transfer.

      Creating a transfer checks the minimum again, so `400 AMOUNT_BELOW_MINIMUM` can be raised here and not
      only on the quote. See **Quotes** for the BRL on-ramp floor.
  - name: Transactions
    description: |
      **Every movement in one timeline:** the transfers you created through this API and the on-chain
      deposits credited into custody, newest first.
  - name: Accounts
    description: |
      **Named accounts for your customers.** Receive funds into an `ozav_acc_` account that carries your
      customer's identity: in the customer's own name on Brazilian rails, and segregated and attributed to
      the customer under OZAV on foreign-currency rails. Which currencies and rails are open depends on your organization, and the
      `Rail` schema gives each rail's state. Sandbox accounts are simulated; live opening is enabled per
      organization. The institution behind the account is never exposed.

      **Every live account needs the customer's documents, in every currency.** The list, the literal
      `doc_type` values and the upload sequence are in "What each customer type must provide" in the
      **Customers** tag. You do not have to guess it up front: `POST /accounts` refuses an incomplete dossier
      with `422 ACCOUNT_PROFILE_INCOMPLETE` and a `missing` list naming what is absent.
  - name: Payout Destinations
    description: |
      **Where a customer's money lands when it leaves.** One resource for every payout rail, with a
      rail-keyed `details`: a Pix key in Brazil, a bank account abroad. A Pix destination must be in the
      customer's own name, unless OZAV has enabled third-party payouts for your organization; the other
      rails can pay a third party.
  - name: Custodial Wallets
    description: |
      **Balances OZAV holds for your customers.** OZAV holds the key and signs: list wallets, read balances
      and deposit addresses, and send. Custody is part of the regulated platform and
      needs production access (KYB and a signed contract). For wallets your users control, see
      **Wallet as a Service**.
  - name: Swap
    description: |
      Quote a stablecoin conversion across the networks OZAV supports, with the network cost on its own
      line. This route quotes only; it does not execute.
  - name: Network Transfers
    description: |
      Move USDC you hold from one network to another, in the same wallet. The operation that moves value
      answers `403 INSUFFICIENT_SCOPE` for every key; see the operation.
  - name: Cards
    description: |
      **Cards on your customers' balances.** Issue virtual cards with opaque `ozav_card_` ids, funded from
      the customer's OZAV balance, and manage their lifecycle. Card data never travels in a plain response.
      The sandbox issues cards end to end; live issuance is enabled per organization once the card program is
      active.
  - name: Yield Overview
    x-displayName: Overview
    description: |
      **Yield on stablecoins your users hold, without handing over the keys.** OZAV admits a short list of
      yield strategies, reads positions with their cost and realized performance on chain, and builds the
      deposit and withdrawal transactions for your user's wallet to sign. OZAV never signs and never
      broadcasts them.

      | | |
      |---|---|
      | Who holds the keys | Your user, for the deposit and withdrawal builds |
      | Who decides | You |
      | Risk | Protocol risk stays with whoever holds the funds |

      **The flow**

      1. `GET /yield/strategies` lists the strategies OZAV admits, each with a stable, opaque `id`.
      2. `POST /yield/preview` shows what a deposit would look like.
      3. `POST /yield/deposits/build` builds the deposit for your user to sign.
      4. `GET /yield/wallets/{address}/positions` reads the positions at the address.
      5. `POST /yield/withdrawals/build` builds the withdrawal.

      **Access.** Steps 1 and 2 need the `custody` product today; steps 3 to 5 need `self_custody`. Building
      is a closed pilot with two gates in series: while the environment gate is closed the builds answer
      `503 YIELD_BUILD_NOT_ENABLED`, and once it is open an organization that is not enrolled answers
      `403 YIELD_BUILD_NOT_IN_PILOT`. The routes in this section that read or apply yield under managed custody
      need the `custody` product.
  - name: Yield
    description: |
      Strategies, previews, positions and transaction builds. Each strategy carries a stable, opaque `id`.
      The underlying venue is never named, and its contract address appears only as the `to` of a
      transaction built for you to sign.
  - name: Treasury
    description: |
      How much of your custodied balance can go to yield while your upcoming payments stay covered. Declare
      the payments (`PUT /treasury/obligations`), read the reserve and surplus per currency
      (`GET /treasury/forecast`), and allocate up to the surplus to managed yield
      (`POST /treasury/allocations`), which records the same order as `POST /yield/applications`. Every number
      in the forecast carries its parts. The reserve is checked at request time and serializes yield applications
      and redemptions, not other outflows (transfers, payouts, card spend). Beta: while treasury is not enabled
      for the environment, every route answers `503 TREASURY_NOT_ENABLED`.
  - name: Agents Overview
    x-displayName: Overview
    description: |
      **AI agents that pay on behalf of a company, under a mandate the company approves.** A platform
      registers agents under the grants companies gave it, and every payment request is checked against the
      mandate and recorded. A company can also run its own agents: it registers its platform with
      `first_party: true` and grants it a mandate like any other, with the same administrator approval.

      **A person approves once, when the company grants the mandate. Every action the mandate allows runs
      without human approval.** `human_approval_above_usd` is optional: leave it out and nothing waits for a
      person. The threshold only ever applies to spending (payments and payouts): a funding or a beneficiary
      registration inside the mandate never waits. A payout a person approved runs without a new request. An agent can pay resources (x402, MPP), pay a registered beneficiary in fiat currency, Pix in reais or a bank account in US dollars
      (`POST /agents/{agent_id}/payouts`) and ask for a Pix charge that becomes stablecoin in the company's
      wallet (`POST /agents/{agent_id}/fundings`, limited by `funding_limits`, never counted as spending). It can also
      register the beneficiary it will pay (`POST /agents/{agent_id}/payees`), inside the `payee_rules` the company set, with no
      approval per registration. It cannot
      create a grant, widen its own mandate, move money of another account holder, or use a corridor outside
      `allowed_corridors`.

      This product is in beta. In the sandbox the payment flow runs end to end with a simulated
      signature; agent payouts and fundings run the same decision path (mandate, limits, approval
      threshold, budget, webhooks) with simulated money: a fixed rate, a simulated transfer and a fake Pix
      code, every receipt marked `simulated: true`; a registered payee gets a simulated id (`ozav_pd_sim_...`) that only the
      sandbox payouts of the agent that registered it accept. Finish a simulated payout or funding with
      `POST /agent-actions/{action_id}/simulate`. In production the
      mandate decides and records every request; until signing is switched on, a payment within the
      mandate answers `503 AGENT_PAYMENT_SIGNING_UNAVAILABLE` and nothing is signed.

      Refusals that come from the company's compliance state carry a fixed message
      (`AGENT_PAYMENT_NOT_PERMITTED`): the platform acts for the company and is not told why.

      **First ten minutes in the sandbox.** Use a test key (`ozav_sk_test_...`) with the `agents:write` and
      `agents:read` scopes. Nothing here moves real money.

      1. Register your organization as a platform: `POST /agent-platforms`. Share the returned `id` with the company.
      2. The company creates a grant once: in the OZAV portal, with multi-factor authentication, or with its own
         admin key through `POST /agent-grants` (scope `agent_grants:write`), for itself or for one of its customers. The
         mandate in it is the whole approval: its actions (`fiat_payout`, `fiat_funding`, `payee_registration`, ...), limits
         (`per_payment_limit_usd`, `daily_limit_usd`, `total_budget_usd`, `recurring_budget`, `funding_limits`) and
         `payee_rules`. Find it with `GET /agent-grants`.
      3. Register the agent under the grant: `POST /agents` with `grant` and a `label`.
      4. Register the beneficiary the agent will pay: `POST /agents/{agent_id}/payees` with an `idempotency_key`.
         Inside `payee_rules` it is `executed` at once; the sandbox gives it a simulated id (`ozav_pd_sim_...`).
      5. Pay it: `POST /agents/{agent_id}/payouts`. Inside the mandate the response is `executed` with
         `simulated: true`. Above `human_approval_above_usd` (if the company set one) it is `202 awaiting_approval`
         until a person approves in the portal, and then it runs on its own.
      6. Ask for money to come in: `POST /agents/{agent_id}/fundings`. The response carries a simulated Pix code and
         the status `awaiting_payment`.
      7. Finish what the sandbox cannot finish by itself: `POST /agent-actions/{action_id}/simulate` with
         `{ "outcome": "settled" }` (the funding is paid and the stablecoin arrives) or `{ "outcome": "returned" }`
         (the charge expired or the transfer came back, and the budget is released).
      8. Watch it all as webhooks: register an endpoint for `agent_action.updated` and every status change of the
         actions above arrives once. `GET /agents/{agent_id}/actions` lists them.

      Use a new `idempotency_key` for each new request and the same one to repeat a request that did not answer. Try the
      refusals too: a payout above `per_payment_limit_usd` answers `AGENT_PAYMENT_OUTSIDE_MANDATE` and is recorded.

      **How a site verifies an agent passport.** A site, an API or a payee that receives an agent's passport
      (`POST /agents/{agent_id}/passport`) needs no OZAV account and no API key to check it. Two ways:

      1. Offline, then a status read. The passport is an SD-JWT (`typ` `vc+sd-jwt`, signed `ES256`). Fetch OZAV's
         public keys at `https://api.ozav.io/functions/v1/credencial-well-known/jwks.json` (cache them for minutes, not
         days, so a key rotation reaches you), pick the key by the token's `kid`, and check the signature, `iss`
         (`https://api.ozav.io`), `vct` (`ozav:agent-passport:v1`), `env` (`live`), `nbf` and `exp`. Each disclosure
         the agent chose to show must hash to an entry of `_sd`. Then read the status at the token's `status.uri`
         (`.../credential-status/{jti}`): only `valid` means the agent, its grant, its mandate and the company behind
         it still hold right now. `revoked`, `expired` and `unknown` mean do not trust it.
      2. One call. `POST https://api.ozav.io/functions/v1/credencial-well-known/credentials/verify` with
         `{ "credential": "<the passport>" }` checks the signature and the status together and answers `valid`, the
         `status`, the agent (`subject`), the `environment` and the disclosed claims.

      ```bash
      curl -s https://api.ozav.io/functions/v1/credencial-well-known/credentials/verify \
        -H 'content-type: application/json' \
        -d '{"credential":"<the passport the agent presented>"}'
      ```

      A sandbox passport (`vct` `ozav:agent-passport:sandbox:v1`, issuer ending in `/sandbox`) is never valid on these
      production paths. Test it with `.../credentials/verify-sandbox` and the keys at `.../sandbox-jwks.json`.
  - name: Agents
    description: |
      Platforms, grants, agents, passports, payments, payouts, fundings and exceptions. Every request is checked
      against the mandate the company approved, and recorded.
  - name: API Keys
    description: |
      Self-serve key lifecycle: create keys scoped to a subset of your
      entitlements, list them (prefixes only - secrets are shown once at
      creation), rotate with an overlap window, and revoke. Test and live keys
      differ only by prefix; the environment is derived from it.
  - name: Statements
    description: |
      Your organization's monthly service statements: what OZAV charges or
      credits you each month, with the itemized lines. The same statements the
      dashboard shows, readable with a key that holds `billing:read`.
  - name: Webhooks
    description: |
      Register HTTPS endpoints to receive signed event notifications. Each
      endpoint has its own signing secret (`ozav_whsec_`, shown once at create /
      rotate) and a list of subscribed event types. Endpoints are tenant-scoped.

      **Deliveries are also scoped by environment.** An event produced by a live
      key is delivered only to endpoints registered in `live`, and a sandbox
      event only to `test` endpoints. If you integrate in both, register one
      endpoint per environment. An endpoint created in the dashboard before it
      asked for an environment receives both: tell its events apart with the
      event's `livemode`.

      **Verifying a delivery.** Every delivery carries three headers:

      ```
      X-OZAV-Signature:   t=<unix>,v1=<hex>
      X-OZAV-Event-ID:    stable across retries, dedupe on it
      X-OZAV-Delivery-ID: unique per delivery attempt
      ```

      Parse `t` and `v1` out of `X-OZAV-Signature`, then recompute:

      ```
      v1 == HMAC_SHA256(key = <your endpoint secret, WITH the ozav_whsec_ prefix>,
                        message = "<t>." + <raw request body>)
      ```

      Three things break verification and are worth stating plainly, because each
      one fails silently on your side:

      1. **The key is the whole secret string, prefix included.** `ozav_whsec_` is
         part of the key, not a display label. Stripping it fails 100% of deliveries.
      2. **Sign the raw body bytes**, before any JSON parse. Re-serializing may or
         may not reproduce the same bytes, so do not rely on it.
      3. **Compare against `v1` only**, not the whole `t=...,v1=...` header value.

      Reject deliveries whose `t` is outside your tolerance window (five minutes is
      recommended) and dedupe by `X-OZAV-Event-ID`.

      **When deliveries are not arriving**, read
      `GET /webhook-endpoints/{id}/deliveries` before debugging signatures. It
      reports `response_status` (what your server answered) and `error` (transport
      failures). A `response_status` of `401` is a signature problem; a null
      `response_status` with an `error` is a network problem such as DNS or TLS,
      and no signature change will fix it.
  - name: Deprecated
    description: |
      **Routes that are leaving the API.** Each one keeps working for the integrations that use it until
      the date in OZAV's notice to them, and its description says what to use instead. Do not build new
      integrations on them.
x-tagGroups:
  - name: Wallet as a Service
    tags:
      - Wallet as a Service Overview
      - Self-custody Wallets
      - Vaults
  - name: Compliance as a Service
    tags:
      - Compliance as a Service Overview
      - Decisions
      - Screenings
      - Lists
  - name: Money Movement
    tags:
      - Money Movement Overview
      - Capabilities
      - Customers
      - Verifications
      - Quotes
      - Transfers
      - Transactions
      - Accounts
      - Payout Destinations
      - Custodial Wallets
      - Swap
      - Network Transfers
      - Cards
  - name: Yield
    tags:
      - Yield Overview
      - Yield
      - Treasury
  - name: Agents · Beta
    tags:
      - Agents Overview
      - Agents
  - name: Keys & Webhooks
    tags:
      - API Keys
      - Webhooks
      - Statements
  - name: Deprecated
    tags:
      - Deprecated
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
        organization's `entitlements` and the key's `scopes` - plus the resolved
        `environment` and a per-feature readiness map (`live`, `beta`,
        `coming_soon`). Call it first in any integration and branch on the access
        it reports rather than hard-coding assumptions. Requires only a valid key
        (any entitlement, no specific scope).
      x-ozav-entitlement: any
      x-ozav-scopes: []
      x-ozav-availability: live
      x-ozav-services: true
      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
                      `live` (generally available), `beta` (available to design partners,
                      may need enablement), or `coming_soon` (defined, and its write operations are refused).
                    additionalProperties:
                      type: string
                      enum:
                        - live
                        - beta
                        - coming_soon
                  jurisdictions:
                    type: object
                    description: |
                      Where OZAV onboards customers, decided by declared residence
                      and place of registration - never by nationality alone.

                      `refused` is enforced at creation: a customer whose tax
                      identity or address is issued there is rejected with
                      `422 JURISDICTION_NOT_SERVED`. `enhanced_due_diligence` is
                      accepted, with longer review and further evidence requested.

                      Anywhere else is served, subject to name screening and a
                      risk-based decision. Absence from both lists means you may
                      proceed; it is not by itself a guarantee of acceptance.
                    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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/capabilities", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/capabilities",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /customers:
    post:
      operationId: createCustomer
      tags:
        - Customers
      summary: Create a customer
      description: |
        Creates a customer - the anchor resource that quotes, verifications,
        accounts, payout destinations, wallets, and transfers all reference. Supply
        `type: individual` with an `individual` block (including
        `individual.tax_id`) or `type: business` with a `business` block
        (including `business.registration`); the required identity is enforced
        server-side. The customer always starts `status: pending` and is verified
        separately (see `POST /verifications`). Accepts an optional
        `Idempotency-Key` (recommended). Returns the created `Customer`.

        A customer already registered for your organization - through this
        API or through your onboarding link - is not created twice: the
        request returns 409 `CUSTOMER_ALREADY_EXISTS` (same tax identity)
        or `CUSTOMER_EMAIL_ALREADY_EXISTS` (same email), and `error.details`
        tells you which customer it is. When the existing customer was
        created through this API, `details` carries `existing_source: api`
        and `existing_id` - its `ozav_cus_` id in the same environment as
        your key (use it with `GET /customers/{customer_id}`). `details` is omitted
        when that id cannot be resolved. When the existing customer came
        through your onboarding link, `details` carries
        `existing_source: onboarding_link` and `existing_id` (its customer
        id, or `null` while that registration is still under verification).

        The record persists and reads back. Customer creation does
        not open a real wallet or bank account by itself. In sandbox,
        verification simulation can materialize simulated wallet and BRL/Pix
        account projections after approval; live provisioning requires
        enablement for your organization.
      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:
                        - 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.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/customers \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "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"
                  }
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "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"
                  }
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/customers",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "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",
                        },
                    },
                },
            )
            data = response.json()
    get:
      operationId: listCustomers
      tags:
        - Customers
      summary: List customers
      description: |
        Lists your customers, newest first, using the standard cursor pagination
        (`limit`, `starting_after`). Returns a `Page` whose `data` is an array of
        `Customer`.
      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:
                            - 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
                incomplete:
                  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.
                  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:
                            - 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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/customers",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        partner is fail-closed on an accepted terms session: `POST /customers`
        returns `422 TERMS_ACCEPTANCE_REQUIRED` until you submit the
        `terms_session_id` of a session that has been accepted.

        The response carries `id` - the session token - **once, at creation**.
        OZAV stores only its hash, so it is never returned again. Persist it
        before you present the terms to your end user.

        Present `terms_url` to the end user, collect their acceptance in your own
        flow, then record it with `POST /customers/terms-sessions/accept`.
        Sessions expire; `expires_at` is absolute.

        Accepts an optional `Idempotency-Key`. Replaying the same key returns the
        same session **without** `id` - the token is only ever issued once.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/terms-sessions", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/customers/terms-sessions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                },
            )
            data = 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
        with the evidence of that acceptance. You collect the acceptance in your
        own flow; this call is your attestation of it, and OZAV stores it against
        the terms version that was in force when the session was created.

        Idempotent: repeating the call returns the same result and never
        overwrites the original evidence. The session token is **not** returned.

        After a `200`, submit the same `terms_session_id` to `POST /customers`.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "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"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/terms-sessions/accept", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "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"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/customers/terms-sessions/accept",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "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",
                },
            )
            data = 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
        `status`, `verification` posture, and outstanding `requirements`. An id
        that does not exist or belongs to another organization returns `404`.
      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:
                        - 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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
    patch:
      operationId: updateCustomer
      tags:
        - Customers
      summary: Update a customer
      description: |
        Updates a customer's mutable attributes (contact fields, identity blocks,
        metadata). Only the fields you include are changed; omitted fields are
        left untouched. Returns the updated `Customer`.

        **Customers registered outside the API.** A customer registered through
        your public registration link, or created from the onboarding documents,
        accepts only the business declarations through this endpoint:
        `business.expected_daily_volume_usd`,
        `business.expected_monthly_volume_usd`, `business.annual_revenue_usd`,
        `business.source_of_funds`, `business.nature_of_business`,
        `business.website`, `business.account_purpose`,
        `business.incorporation_date`, `business.bc_protege_disabled` and
        `business.account_opening_terms_accepted_by`, and the phone (`business.phone`, with its `country_code` and `number`) so the account-opening requirements can be met. Your organization's own
        account accepts only `business.bc_protege_disabled` and
        `business.account_opening_terms_accepted_by` (its company declarations
        come from your organization's onboarding). Any other field returns
        `409 LEGACY_PROFILE_DECLARATIONS_ONLY` and nothing is written. If
        another change to the customer lands at the same time, the call
        returns `409 CONCURRENT_UPDATE`; retrying the same request is safe.
        The response does not return the declared values for these customers,
        and its `requirements.missing_fields` is not recalculated from them.
      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:
                        - 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"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \
              -X PATCH \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "email": "ana.ramos@example.com",
                "metadata": {
                  "tier": "gold"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "PATCH",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "email": "ana.ramos@example.com",
                "metadata": {
                  "tier": "gold"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.patch(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "email": "ana.ramos@example.com",
                    "metadata": {
                        "tier": "gold",
                    },
                },
            )
            data = 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
        signer - to a business customer, for KYB. Supply one or more `roles` and
        the person's `individual` identity block (and `ownership_percent` for
        owners). Valid only for `business` customers. Returns the created
        `AssociatedPerson`.

        **Gender.** `individual.gender` is recorded with the same rules as
        `PATCH` and the customer portal. If the customer already has this
        person (same CPF) with another declared gender and the gender request
        is closed, the call returns `409 GENDER_ALREADY_DECLARED` and the
        person is not created. Otherwise the value also applies to the
        customer's other records of the same person.
      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"
        "409":
          $ref: "#/components/responses/Conflict"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "roles": [
                  "ubo",
                  "director"
                ],
                "ownership_percent": 40,
                "individual": {
                  "legal_name": "Carlos Souza",
                  "tax_id": {
                    "country": "BR",
                    "type": "cpf",
                    "value": "39053344705"
                  }
                },
                "email": "carlos@acme.com.br"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "roles": [
                  "ubo",
                  "director"
                ],
                "ownership_percent": 40,
                "individual": {
                  "legal_name": "Carlos Souza",
                  "tax_id": {
                    "country": "BR",
                    "type": "cpf",
                    "value": "39053344705"
                  }
                },
                "email": "carlos@acme.com.br"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "roles": [
                        "ubo",
                        "director",
                    ],
                    "ownership_percent": 40,
                    "individual": {
                        "legal_name": "Carlos Souza",
                        "tax_id": {
                            "country": "BR",
                            "type": "cpf",
                            "value": "39053344705",
                        },
                    },
                    "email": "carlos@acme.com.br",
                },
            )
            data = 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
        cursor pagination. Returns a `Page` of `AssociatedPerson`.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        percentage, identity block, e-mail). Only included fields are changed, inside
        `individual` too: omitted identity fields keep their stored value.
        Returns the updated `AssociatedPerson`.

        **Gender.** `individual.gender` (`male`, `female` or `other`) is
        recorded with the same rules as the customer portal. The open
        gender request on the customer closes only when every person who
        signs for the company has a gender. Once that request is closed, a
        declared gender is locked: changing it returns `409
        GENDER_ALREADY_DECLARED`.

        **Records from onboarding documents.** An associated person created
        from the customer's onboarding documents accepts only
        `individual.gender`, `individual.phone`, `email` and, when the
        documents did not bring them, `individual.residential_address` and
        `individual.date_of_birth` through the API; any other field returns
        `409 LEGACY_RECORD_GENDER_ONLY`. The address and the date of birth can
        only be filled in. An address counts as already on record when it is
        complete (street, number, neighborhood, city, state and postal code),
        whether the stored record or the documents hold it; an incomplete one
        is completed by merging: what is already on record survives and only
        its blank components are taken from the call. A date of birth counts
        as soon as one exists. Replacing what is already on record returns
        `409 LEGACY_RECORD_FIELD_ALREADY_SET`, with the first such field in
        `param` and all of them in `details`. The call is refused as a whole
        and nothing in the body is written, so resend without those fields;
        the correction itself goes through OZAV support. Send the whole
        `residential_address`, not a part of it (a Brazilian one needs a
        postal code with 8 digits and a 2-letter state). A retry after a lost
        response may answer `409 LEGACY_RECORD_FIELD_ALREADY_SET` because the
        first call was applied. With `gender` in the body, a fill that lands
        between the checks can be refused after the gender was recorded:
        resend without `gender`. These records do not return an `individual`
        block, the phone or the e-mail, and the other identity fields (legal name, tax id)
        listed in their `requirements.missing_fields` are completed through
        OZAV support, not through this endpoint. For them `requirements` is
        computed from a person without an `individual` block, so it keeps
        listing `individual.phone` after you set it; the address, the date of
        birth and `email` leave the list once they are set.

        **Concurrent changes.** If the person changes between read and
        write, the call returns `409 CONCURRENT_UPDATE`; retrying the same
        request is safe.
      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
              gender:
                summary: Declare the gender of a signer
                value:
                  individual:
                    gender: female
      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
        "400":
          $ref: "#/components/responses/BadRequest"
        "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/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons/ozav_ap_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \
              -X PATCH \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "ownership_percent": 55
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons/ozav_ap_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "PATCH",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "ownership_percent": 55
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.patch(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/associated-persons/ozav_ap_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "ownership_percent": 55,
                },
            )
            data = 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 KYC/KYB document and returns a short-lived
        `upload_url` the client PUTs the file bytes to. Documents may belong to
        the customer itself or to one of its associated persons. Use generic
        document categories such as `company_registration`, `identity_document`,
        `selfie`, `liveness`, `proof_of_address`, `ownership_structure`,
        `financial_statement`, and `source_of_funds`.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "doc_type": "proof_of_address",
                "mime": "application/pdf"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "doc_type": "proof_of_address",
                "mime": "application/pdf"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "doc_type": "proof_of_address",
                    "mime": "application/pdf",
                },
            )
            data = 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
        pagination. Returns a `Page` of `CustomerDocument`. Never returns the raw
        storage location or a download URL.
      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_by_person:
                        type: array
                        items:
                          $ref: "#/components/schemas/PendingDocumentsOfPerson"
                        description: "What each associated person still owes. A Brazilian business owes, per ≥25% owner or signer, `identity_document`, `selfie` (or `liveness`) and `proof_of_address`; upload them with `subject_type: associated_person`. `pep_declaration` is optional and never listed here (only a BRL account asks for it, in the `POST /accounts` `missing` list). One entry per person (empty `still_required` when that person is complete). `[]` when this customer has no per-person requirement (individuals, non-Brazilian businesses) - and also when a Brazilian business has not declared any associated person yet: declare them first, then this list fills in (`POST /accounts` keeps asking for `associated_persons.ubo_25_plus` meanwhile). Omitted, like `still_required`, when it could not be measured. `POST /accounts` refuses with the same names, `associated_persons[i].documents.<doc_type>`."
                      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.

                          **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.

                          The set depends on the customer and the jurisdiction. 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.
                          This 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.
                      pending:
                        type: array
                        items:
                          $ref: "#/components/schemas/PendingDocument"
                        description: |-
                          The same categories as `still_required`, in the same order, each carrying why it is still pending. `still_required` is unchanged and is not deprecated, so an existing integration needs no edit.

                          **Use this one when you resend.** `still_required` cannot tell "never arrived" from "arrived and was rejected", and those ask for opposite actions: the first means upload, the second means fix the document and upload again. Reading only `still_required`, an integrator resends the same rejected file over and over.

                          Omitted under exactly the same conditions as `still_required`, and for the same reason.
              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
                    still_required:
                      - proof_of_address
                      - bank_statement
                    pending:
                      - doc_type: proof_of_address
                        state: rejected
                        reason: Issued more than 90 days ago.
                      - doc_type: bank_statement
                        state: missing
        "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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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`
        returned by `POST /customers/{customer_id}/documents`. Send the same
        API key used to create the grant. OZAV stores the file in private
        storage, recomputes size and SHA-256 server-side, and moves the
        document to `received` when the bytes are accepted.
      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 \
              -X PUT \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents/{document_id}/upload", {
              method: "PUT",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.put(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents/{document_id}/upload",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /customers/{customer_id}/deactivate:
    post:
      operationId: deactivateCustomer
      tags:
        - Customers
      summary: Deactivate a customer
      description: |-
        Stops new money movement for the customer. Card purchases are declined with `account_inactive`. New cards, transfers (`POST /transfers`), account payouts and wallet sends are refused with `403 CUSTOMER_DEACTIVATED`; withdrawals through the custody API are refused with `403 USER_DEACTIVATED`. Deposits sent to the customer's wallets and accounts still arrive and are credited, but cannot be spent until you reactivate. It applies to test keys too, so you can try it in the sandbox.

        Operations created before the deactivation finish, except wallet sends still waiting for approval or a delay, which are refused when they would execute. Self-custody transaction intents are not affected: OZAV does not sign them.

        Calling it again on a deactivated customer changes nothing and returns the date of the first deactivation. This is your own switch: it does not lift, and does not reveal, any restriction OZAV applies for compliance reasons. Your organization's own account cannot be deactivated (`422 CUSTOMER_DEACTIVATION_NOT_ALLOWED`).
      x-ozav-entitlement: customers
      x-ozav-scopes:
        - customers:write
      x-ozav-availability: live
      parameters:
        - name: customer_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The customer's deactivation state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerDeactivation"
        "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/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivate \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivate", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivate",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /customers/{customer_id}/reactivate:
    post:
      operationId: reactivateCustomer
      tags:
        - Customers
      summary: Reactivate a customer
      description: "Clears your deactivation, so the customer can move money again. It only clears what `POST /customers/{customer_id}/deactivate` set: a restriction OZAV applies for compliance reasons stays in force, and `deactivated: false` here does not mean there is none. Calling it on an active customer returns the same state."
      x-ozav-entitlement: customers
      x-ozav-scopes:
        - customers:write
      x-ozav-availability: live
      parameters:
        - name: customer_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The customer's deactivation state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerDeactivation"
        "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/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/reactivate \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/reactivate", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/reactivate",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /customers/{customer_id}/deactivation:
    get:
      operationId: getCustomerDeactivation
      tags:
        - Customers
      summary: Get a customer's deactivation state
      description: Whether you have deactivated this customer, and since when.
      x-ozav-entitlement: customers
      x-ozav-scopes:
        - customers:read
      x-ozav-availability: live
      parameters:
        - name: customer_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The customer's deactivation state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomerDeactivation"
        "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/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivation \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivation", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivation",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /customers/{customer_id}/destination-identifications:
    post:
      operationId: createDestinationIdentification
      tags:
        - Customers
      summary: Identify who holds a destination wallet
      description: "Records who holds a wallet the customer sent crypto to: the customer's own self-hosted wallet (with the ownership attestation), or the customer's account at an exchange. OZAV asks for it with a `requirement.due` webhook (`reason: destination_unidentified`) and a `documents_due` item with `purpose: travel_rule`; answering closes that item. You can also send it before being asked. The identification applies to every send of the customer to the same wallet. It does not allow or block any send."
      x-ozav-entitlement: customers
      x-ozav-scopes:
        - customers:write
      x-ozav-availability: live
      parameters:
        - name: customer_id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DestinationIdentificationRequest"
            examples:
              exchange:
                summary: The customer's account at an exchange
                value:
                  chain: tron
                  address: TGssfnTouNs2H9AYPRkeZo9zuRxTPB5KCq
                  wallet_type: external
                  exchange:
                    name: Example Exchange
              self_custodied:
                summary: The customer's own wallet
                value:
                  chain: tron
                  address: TGssfnTouNs2H9AYPRkeZo9zuRxTPB5KCq
                  wallet_type: self_custodied
                  wallet_attested_ownership_at: 2026-10-08T12:00:00Z
      responses:
        "201":
          description: The identification, as recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DestinationIdentification"
        "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/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/destination-identifications \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "chain": "tron",
                "address": "TGssfnTouNs2H9AYPRkeZo9zuRxTPB5KCq",
                "wallet_type": "external",
                "exchange": {
                  "name": "Example Exchange"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/destination-identifications", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "chain": "tron",
                "address": "TGssfnTouNs2H9AYPRkeZo9zuRxTPB5KCq",
                "wallet_type": "external",
                "exchange": {
                  "name": "Example Exchange"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/destination-identifications",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "chain": "tron",
                    "address": "TGssfnTouNs2H9AYPRkeZo9zuRxTPB5KCq",
                    "wallet_type": "external",
                    "exchange": {
                        "name": "Example Exchange",
                    },
                },
            )
            data = response.json()
    get:
      operationId: listDestinationIdentifications
      tags:
        - Customers
      summary: List a customer's destination identifications
      description: The wallets identified for this customer, newest first.
      x-ozav-entitlement: customers
      x-ozav-scopes:
        - customers:read
      x-ozav-availability: live
      parameters:
        - name: customer_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The identifications.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DestinationIdentificationList"
        "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/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/destination-identifications \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/destination-identifications", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/destination-identifications",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /customers/{customer_id}/limit-requests:
    parameters:
      - $ref: "#/components/parameters/CustomerId"
    post:
      operationId: createLimitRequest
      tags:
        - Customers
      summary: Ask for a higher limit for an existing customer
      description: |-
        Asks OZAV to raise the limit of an approved customer - most often one
        approved at the entry limit. A customer that is not approved, or whose
        account is closed or suspended, answers `409 CUSTOMER_NOT_ELIGIBLE`. Send the monthly amount you want
        (and, optionally, the per-transaction amount and a short reason).

        The response lists the proofs of economic capacity that are still
        missing for that amount in `documents_due`, each with the `doc_type`
        that closes it in `POST /customers/{customer_id}/documents`. The same
        items appear in `requirements.documents_due` of the customer, with
        `purpose: limit_increase`, and a `requirement.due` webhook follows within
        about 15 minutes.
        Proofs sent recently and not rejected are not asked again.

        The request moves on its own: `awaiting_documents` while a proof is
        missing, `in_review` once everything arrived, and `approved` (with
        `approved_limit`) when OZAV's compliance officer sets the new limit - it
        can be lower than what you asked. `declined` carries no reason;
        `canceled` means the customer stopped being eligible. Subscribe to `limit_request.updated` to
        be told of each change.

        One open request per customer: a second one answers
        `409 LIMIT_REQUEST_ALREADY_OPEN`. Do not put personal data in `reason`.
      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/LimitRequestCreate"
            examples:
              monthly:
                summary: A higher monthly limit
                value:
                  monthly_usd: "50000.00"
                  per_transaction_usd: "10000.00"
                  reason: Monthly supplier payments are growing.
      responses:
        "201":
          description: The limit request was recorded.
          headers:
            Idempotent-Replayed:
              $ref: "#/components/headers/IdempotentReplayed"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LimitRequest"
              examples:
                awaiting:
                  summary: Waiting for proofs of capacity
                  value:
                    id: ozav_lir_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                    object: limit_request
                    customer_id: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                    status: awaiting_documents
                    requested:
                      monthly_usd: "50000.00"
                      per_transaction_usd: "10000.00"
                    reason: Monthly supplier payments are growing.
                    documents_due:
                      - key: bank_statement
                        doc_type: bank_statement
                        label: Bank statements for the last 3 months - for the requested limit of USD 50,000/month
                        status: due
                      - key: tax_return
                        doc_type: source_of_funds
                        label: Personal income tax return (IRPF) with its filing receipt - for the requested limit of USD 50,000/month
                        status: due
                    approved_limit: null
                    created_at: 2026-09-30T12:00:00Z
                    updated_at: 2026-09-30T12:00:00Z
                    decided_at: null
        "202":
          $ref: "#/components/responses/IdempotencyProcessing"
        "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/limit-requests \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "monthly_usd": "50000.00",
                "per_transaction_usd": "10000.00",
                "reason": "Monthly supplier payments are growing."
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/limit-requests", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "monthly_usd": "50000.00",
                "per_transaction_usd": "10000.00",
                "reason": "Monthly supplier payments are growing."
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/limit-requests",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "monthly_usd": "50000.00",
                    "per_transaction_usd": "10000.00",
                    "reason": "Monthly supplier payments are growing.",
                },
            )
            data = response.json()
    get:
      operationId: listLimitRequests
      tags:
        - Customers
      summary: List a customer's limit requests
      description: |-
        Lists the limit requests of a customer, newest first, using the
        standard cursor pagination. Returns a `Page` of `LimitRequest`.
      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: Limit requests.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Page"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/LimitRequest"
              examples:
                page:
                  summary: One approved request
                  value:
                    data:
                      - id: ozav_lir_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                        object: limit_request
                        customer_id: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                        status: approved
                        requested:
                          monthly_usd: "50000.00"
                          per_transaction_usd: null
                        reason: null
                        documents_due: []
                        approved_limit:
                          monthly_usd: "40000.00"
                          per_transaction_usd: "10000.00"
                        created_at: 2026-09-30T12:00:00Z
                        updated_at: 2026-10-02T15:00:00Z
                        decided_at: 2026-10-02T15:00:00Z
                    has_more: false
                    next_cursor: null
        "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/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/limit-requests \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/limit-requests", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/limit-requests",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /customers/{customer_id}/evidence:
    parameters:
      - $ref: "#/components/parameters/CustomerId"
    get:
      operationId: getCustomerEvidencePack
      tags:
        - Deprecated
      summary: Retrieve the verification and decision record for a customer
      deprecated: true
      description: |
        **Deprecated.** In Money Movement a customer OZAV approved is approved,
        and one OZAV refused is refused: `GET /customers/{customer_id}` says
        which. This route stays available to the integrations that read it until
        the date in OZAV's notice to them.

        Returns the **verification and decision record** for one customer: what
        OZAV verified, which lists were screened, which documents were assessed,
        which decisions were taken on their movements, and what happened
        afterwards. Hand it to your bank or your regulator as evidence that the
        decision was made and can be defended.

        Ask for `format=html` to get a printable document instead of JSON; both
        come from the same record, so they can never disagree.

        **An empty section means there is no record, never approval.** A section
        that could not be read is listed in `secoesIndisponiveis` and the verdict
        falls back to `em_analise` - the record never asserts what it did not
        read.

        The record is assembled at request time and carries `geradoEm`. There is
        no frozen document id: a compliance document that freezes
        ages silently, and an "approved" from three months ago shown to a bank
        while a new alert is open is worse than no document at all.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/evidence", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/evidence",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        verified about this customer, when, and how - as an IETF SD-JWT
        (`vc+sd-jwt`, ES256). Hand it to the end user: they can present it to
        any third party and choose which attributes to disclose (for example
        `over_18` without `cpf`). Any verifier checks it offline against the
        public key at `jwks_url` and confirms it is still in force at
        `status_url`.

        **Selectively disclosable claims:** `cpf`, `full_name`, `birth_date`,
        `over_18`, `kyc_level` (`3` = hosted session with document, liveness and
        face match; `2` = document-based identification), `method`,
        `verified_at`, `screening`. The JWT body itself carries no personal
        data - only salted digests - so the credential leaks nothing the holder
        does not disclose.

        Each call issues a **new** credential (new `id`). Earlier ones stay in
        force until they expire or the customer's status changes: a credential
        is reported `revoked` at `status_url` as soon as the customer is no
        longer approved, is frozen, or is re-verified. There is no revocation
        step for you to call. Issuance is capped per customer per day
        (`429 CREDENTIAL_ISSUANCE_LIMIT_REACHED` past the cap): keep the
        credential you received rather than requesting one per use.

        Individual customers identified by a Brazilian CPF only. Not issued in
        the sandbox environment. Availability is enabled per organization by
        OZAV; until then the call returns `422 FEATURE_NOT_AVAILABLE`.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/credential", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/customers/ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1/credential",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /decisions:
    post:
      operationId: createDecision
      tags:
        - Decisions
      summary: Get OZAV's verdict on a risk question
      description: |
        Runs OZAV's own engines on the input and returns the verdict with its reasons and checks.
        The verdict is recorded (your input is not) so you can read it back and report its outcome.

        Missing or stale data is `REVIEW`, never `ALLOW`. If an engine cannot answer at all, the
        call returns `503 DECISION_UNAVAILABLE`, nothing is recorded and nothing is charged.

        Send an `Idempotency-Key` to retry safely: the same key and body return the same decision
        and are not charged again; the same key with a different body answers `409`.

        Requires the `decisions:write` scope. API keys created before this scope existed do not
        gain it: create a new key with the scopes you need.
      x-ozav-entitlement: compliance
      x-ozav-scopes:
        - decisions:write
      x-ozav-availability: live
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/X402PaymentSignature"
        - $ref: "#/components/parameters/XOzavSimulatePayment"
        - $ref: "#/components/parameters/IdempotencyKeyOptional"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DecisionCreateRequest"
            examples:
              name_match:
                summary: Is this person on a sanctions list?
                value:
                  type: name.match
                  input:
                    name: Maria Oliveira
                    date_of_birth: 1985-04-12
      responses:
        "201":
          description: The verdict.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Decision"
        "202":
          $ref: "#/components/responses/IdempotencyProcessing"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/decisions \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "type": "name.match",
                "input": {
                  "name": "Maria Oliveira",
                  "date_of_birth": "1985-04-12"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/decisions", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "type": "name.match",
                "input": {
                  "name": "Maria Oliveira",
                  "date_of_birth": "1985-04-12"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/decisions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "type": "name.match",
                    "input": {
                        "name": "Maria Oliveira",
                        "date_of_birth": "1985-04-12",
                    },
                },
            )
            data = response.json()
  /decisions/{decision_id}:
    get:
      operationId: getDecision
      tags:
        - Decisions
      summary: Retrieve a decision
      description: |
        Returns a decision your organization requested, with its outcome if you reported one.
      x-ozav-entitlement: compliance
      x-ozav-scopes:
        - decisions:read
      x-ozav-availability: live
      x-ozav-services: true
      parameters:
        - name: decision_id
          in: path
          required: true
          description: The decision id (`ozav_dec_...`).
          schema:
            type: string
      responses:
        "200":
          description: The decision.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Decision"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/decisions/ozav_dec_01J8Z9K2M3N4P5Q6R7S8T9U0V1 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/decisions/ozav_dec_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/decisions/ozav_dec_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /decisions/{decision_id}/outcome:
    post:
      operationId: recordDecisionOutcome
      tags:
        - Decisions
      summary: Report what happened after a decision
      description: |
        Tells OZAV how the case ended. An outcome is recorded once: a second report returns
        `409 OUTCOME_ALREADY_RECORDED`. Outcomes help OZAV measure and improve its rules.
      x-ozav-entitlement: compliance
      x-ozav-scopes:
        - decisions:write
      x-ozav-availability: live
      x-ozav-services: true
      parameters:
        - name: decision_id
          in: path
          required: true
          description: The decision id (`ozav_dec_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DecisionOutcomeRequest"
            examples:
              false_positive:
                summary: The name was a namesake
                value:
                  outcome: false_positive
                  note: Different date of birth confirmed by document.
      responses:
        "200":
          description: The decision with its outcome.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Decision"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/decisions/ozav_dec_01J8Z9K2M3N4P5Q6R7S8T9U0V1/outcome \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "outcome": "false_positive",
                "note": "Different date of birth confirmed by document."
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/decisions/ozav_dec_01J8Z9K2M3N4P5Q6R7S8T9U0V1/outcome", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "outcome": "false_positive",
                "note": "Different date of birth confirmed by document."
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/decisions/ozav_dec_01J8Z9K2M3N4P5Q6R7S8T9U0V1/outcome",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "outcome": "false_positive",
                    "note": "Different date of birth confirmed by document.",
                },
            )
            data = response.json()
  /agent-platforms:
    post:
      operationId: registerAgentPlatform
      tags:
        - Agents
      summary: Register your organization as an agent platform
      description: |
        Registers the calling organization as a platform that runs AI agents for other
        companies. One platform per organization and environment: calling again returns the same one.
        Share the returned `id` with the companies that will authorize your agents.

        To run agents with your own organization's money, register with `first_party: true`. The platform then
        receives grants only from your organization, and each grant is still an explicit approval, made in one
        of two ways: in the OZAV portal, where an administrator confirms it with multi-factor authentication
        (the authenticator code), or with `POST /agent-grants` and a key holding `agent_grants:write`, where the
        call with that admin key is the approval. Calling again with the other mode answers
        `409 AGENT_PLATFORM_MODE_CONFLICT`.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:write
      x-ozav-availability: beta
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentPlatformRequest"
            examples:
              platform:
                summary: Register a platform
                value:
                  name: Acme Agents
      responses:
        "200":
          description: The platform, already registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentPlatform"
        "201":
          description: The platform, registered now.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentPlatform"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agent-platforms \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "name": "Acme Agents"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-platforms", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "name": "Acme Agents"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agent-platforms",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "name": "Acme Agents",
                },
            )
            data = response.json()
  /agent-grants:
    get:
      operationId: listAgentGrants
      tags:
        - Agents
      summary: List agent grants
      description: |
        The grants your organization gave (as the company that owns the money) and the
        grants it received (as a platform). A company creates a grant in the OZAV portal, with
        multi-factor authentication, or with `POST /agent-grants`.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      responses:
        "200":
          description: The grants.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentGrantList"
        "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/agent-grants \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-grants", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agent-grants",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
    post:
      operationId: createAgentGrant
      tags:
        - Agents
      summary: Grant an agent mandate, for your organization or one of its customers
      description: |
        Your organization gives a platform's agents a mandate to move money: its own money, or the money of one of
        your customers (`customer_id`, a person or a business you onboarded). The grant is active at once. Calling
        this endpoint with your admin key is the approval: the customer does not approve it separately, so only
        grant for a customer whose instruction you hold.

        Only a key holding `agent_grants:write` can call it. The `operator` key role never receives that scope;
        an `admin` key does, and an existing key gains it only if you mint a new one with it.

        The same rules as a grant made in the portal apply:

        - the platform (`platform_id`) must be active and in the same environment as your key;
        - a third-party platform never receives a grant from its own organization (`422 SELF_AUTHORIZATION`),
          and a first-party platform only from it (`422 FIRST_PARTY_PLATFORM`);
        - the mandate is validated like everywhere else (`422 MANDATE_INVALID`).

        An agent under a customer's grant moves only that customer's money: its payouts leave the customer's
        wallets, go to the customer's payout destinations, and its fundings land in the customer's wallets.

        The same `idempotency_key` with the same body returns the same grant (`200`); with a different body it
        answers `409 IDEMPOTENCY_KEY_REUSED`.

        - `404 CUSTOMER_NOT_FOUND`: no such customer in your organization and environment.
        - `404 AGENT_PLATFORM_NOT_FOUND`: no active platform with this id in your environment.
        - `422 CUSTOMER_DEACTIVATED`: you deactivated this customer.
        - `422 CUSTOMER_NOT_APPROVED`: this customer has not passed KYC. A grant for a customer needs an approved customer.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_grants:write
      x-ozav-availability: beta
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentGrantRequest"
            examples:
              for_a_customer:
                summary: A monthly payout and funding mandate for one of your customers
                value:
                  idempotency_key: grant-cus-42
                  platform_id: ozav_apl_01J9Z8Y7X6W5V4T3S2R1Q0P9N8
                  customer_id: ozav_cus_01J9Z8Y7X6W5V4T3S2R1Q0P9N8
                  mandate:
                    actions:
                      - fiat_payout
                      - fiat_funding
                    per_payment_limit_usd: 50
                    recurring_budget:
                      period: month
                      amount_usd: 200
                      timezone: America/Sao_Paulo
                    funding_limits:
                      per_request_usd: 500
                      daily_usd: 800
                    valid_until: 2099-12-31T00:00:00Z
      responses:
        "200":
          description: The grant, already created with this idempotency key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentGrant"
        "201":
          description: The grant, created and active.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentGrant"
        "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/agent-grants \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "grant-cus-42",
                "platform_id": "ozav_apl_01J9Z8Y7X6W5V4T3S2R1Q0P9N8",
                "customer_id": "ozav_cus_01J9Z8Y7X6W5V4T3S2R1Q0P9N8",
                "mandate": {
                  "actions": [
                    "fiat_payout",
                    "fiat_funding"
                  ],
                  "per_payment_limit_usd": 50,
                  "recurring_budget": {
                    "period": "month",
                    "amount_usd": 200,
                    "timezone": "America/Sao_Paulo"
                  },
                  "funding_limits": {
                    "per_request_usd": 500,
                    "daily_usd": 800
                  },
                  "valid_until": "2099-12-31T00:00:00Z"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-grants", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "grant-cus-42",
                "platform_id": "ozav_apl_01J9Z8Y7X6W5V4T3S2R1Q0P9N8",
                "customer_id": "ozav_cus_01J9Z8Y7X6W5V4T3S2R1Q0P9N8",
                "mandate": {
                  "actions": [
                    "fiat_payout",
                    "fiat_funding"
                  ],
                  "per_payment_limit_usd": 50,
                  "recurring_budget": {
                    "period": "month",
                    "amount_usd": 200,
                    "timezone": "America/Sao_Paulo"
                  },
                  "funding_limits": {
                    "per_request_usd": 500,
                    "daily_usd": 800
                  },
                  "valid_until": "2099-12-31T00:00:00Z"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agent-grants",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "grant-cus-42",
                    "platform_id": "ozav_apl_01J9Z8Y7X6W5V4T3S2R1Q0P9N8",
                    "customer_id": "ozav_cus_01J9Z8Y7X6W5V4T3S2R1Q0P9N8",
                    "mandate": {
                        "actions": [
                            "fiat_payout",
                            "fiat_funding",
                        ],
                        "per_payment_limit_usd": 50,
                        "recurring_budget": {
                            "period": "month",
                            "amount_usd": 200,
                            "timezone": "America/Sao_Paulo",
                        },
                        "funding_limits": {
                            "per_request_usd": 500,
                            "daily_usd": 800,
                        },
                        "valid_until": "2099-12-31T00:00:00Z",
                    },
                },
            )
            data = response.json()
  /agent-grants/{grant_id}:
    get:
      operationId: getAgentGrant
      tags:
        - Agents
      summary: Retrieve an agent grant
      description: |
        A grant and the mandate in force. Readable by the company that gave it and by the
        platform that received it.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: grant_id
          in: path
          required: true
          description: The grant id (`ozav_agr_...`).
          schema:
            type: string
      responses:
        "200":
          description: The grant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentGrant"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agent-grants/{grant_id} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-grants/{grant_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agent-grants/{grant_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agent-grants/{grant_id}/prepaid-resources:
    post:
      operationId: registerAgentPrepaidResource
      tags:
        - Agents
      summary: Register credit the company bought outside OZAV, for its agents to consume
      description: |
        Credit the company already paid a seller for, outside OZAV (API credits, a prepaid plan), registered under a grant so
        its agents can declare what they consume. Nothing moves money: the resource is a balance in whole units of whatever the
        seller sells (`tokens`, `requests`, `credits`), and only goes down. Buying more is a new resource.

        Only the account holder registers resources, with its administrator key (`agent_grants:write`). Leave `agent_id` out
        to let every agent of the grant consume it, or name one agent. The same `idempotency_key` with the same body returns
        the same resource (`200`).

        - `409 PREPAID_RESOURCE_CONFLICT`: this `idempotency_key` was used for a different resource.
        - `409 AGENT_GRANT_NOT_ACTIVE`: the grant is revoked or suspended.
        - `422 PREPAID_RESOURCE_ALREADY_EXPIRED`: `expires_at` is not in the future.
        - `403 PREPAID_RESOURCE_HOLDER_ONLY`: the key lacks `agent_grants:write`.
        - `404 AGENT_GRANT_NOT_FOUND` / `404 AGENT_NOT_FOUND`: not your grant, or not an agent of it.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_grants:write
      x-ozav-availability: beta
      parameters:
        - name: grant_id
          in: path
          required: true
          description: The grant id (`ozav_agr_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentPrepaidResourceRequest"
            examples:
              credits:
                summary: Two million tokens bought from a model seller
                value:
                  idempotency_key: tokens-october
                  seller_domain: api.openai.com
                  unit: tokens
                  units: "2000000"
                  description: October token plan
      responses:
        "200":
          description: The resource registered earlier under this key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentPrepaidResource"
        "201":
          description: The resource was registered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentPrepaidResource"
        "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/agent-grants/{grant_id}/prepaid-resources \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "tokens-october",
                "seller_domain": "api.openai.com",
                "unit": "tokens",
                "units": "2000000",
                "description": "October token plan"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-grants/{grant_id}/prepaid-resources", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "tokens-october",
                "seller_domain": "api.openai.com",
                "unit": "tokens",
                "units": "2000000",
                "description": "October token plan"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agent-grants/{grant_id}/prepaid-resources",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "tokens-october",
                    "seller_domain": "api.openai.com",
                    "unit": "tokens",
                    "units": "2000000",
                    "description": "October token plan",
                },
            )
            data = response.json()
    get:
      operationId: listAgentPrepaidResources
      tags:
        - Agents
      summary: List a grant's prepaid resources and what is left of each
      description: |
        Every prepaid resource of the grant, newest first, with `balance_units` as it is now. The account holder and the
        grant's platform can read them; anyone else gets `404 AGENT_GRANT_NOT_FOUND`.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: grant_id
          in: path
          required: true
          description: The grant id (`ozav_agr_...`).
          schema:
            type: string
      responses:
        "200":
          description: The grant's prepaid resources.
          content:
            application/json:
              schema:
                type: object
                required:
                  - object
                  - data
                properties:
                  object:
                    type: string
                    const: list
                    description: Always the literal `list`.
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/AgentPrepaidResource"
                    description: The resources, newest first.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agent-grants/{grant_id}/prepaid-resources \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-grants/{grant_id}/prepaid-resources", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agent-grants/{grant_id}/prepaid-resources",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agent-grants/{grant_id}/revoke:
    post:
      operationId: revokeAgentGrant
      tags:
        - Agents
      summary: Revoke an agent grant
      description: |
        Either side can revoke: the company withdraws the authorization, or the platform
        gives it up. Revoking takes effect immediately: a payment already being recorded is refused by
        the database check. Revoking is final - a revoked grant never becomes active again. The response
        keeps the mandate that was granted, as `GET` does. Revoking again returns the revoked grant.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:write
      x-ozav-availability: beta
      parameters:
        - name: grant_id
          in: path
          required: true
          description: The grant id (`ozav_agr_...`).
          schema:
            type: string
      responses:
        "200":
          description: The revoked grant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentGrant"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agent-grants/{grant_id}/revoke \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-grants/{grant_id}/revoke", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agent-grants/{grant_id}/revoke",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agent-grants/{grant_id}/versions:
    post:
      operationId: publishAgentGrantVersion
      tags:
        - Agents
      summary: Publish a new version of a grant's mandate
      description: |
        Changes what a grant's agents may do (limits, actions, corridors, validity) without revoking the grant: version
        N+1 of the mandate becomes the current one, atomically. The previous versions are kept as they were.
        Calling this endpoint with your admin key is the approval, as for `POST /agent-grants`; the portal does the same
        with multi-factor authentication. Only the organization that gave the grant can publish, with a key holding
        `agent_grants:write`; the platform that received it cannot widen its own mandate.

        Send the whole new `mandate` (it replaces the current one, it is not a patch) and the `expected_version` you
        edited. If the current version moved meanwhile you get `409 STALE_MANDATE_VERSION`: read the grant and resend.
        Omit `mandate.valid_from` to keep the start of the current window.

        What happens to what already exists:

        - Agents keep working and are not suspended. A passport issued under an older version stops being valid
          (`mandate_changed` when verified); the agent requests a new passport under the new version.
        - A request decided under the old version keeps that decision. A payment that was authorized but not yet
          sent when the version changed is refused with `AGENT_MANDATE_CHANGED`; the agent asks again under the new version.
        - An agent registered with its own `own_mandate` follows the new mandate in every field it inherited
          (omitted at registration), and the change is recorded. A field it set explicitly stays, and must still fit
          inside the new mandate. If one does not, nothing is published and the answer is
          `422 AGENTS_WIDER_THAN_MANDATE`, listing each agent and the field it exceeds. Revoke or replace those
          agents, then publish again.

        The same `idempotency_key` with the same body returns the same version (`200`); with a different body it
        answers `409 IDEMPOTENCY_KEY_REUSED`. The key is scoped to the grant and covers real publishes only: sending
        the mandate that is already current changes nothing, records no key and returns the grant as it is (`200`).

        - `403 NOT_THE_ACCOUNT_HOLDER`: your organization received this grant as a platform; only the holder publishes.
        - `403 CLIENT_NOT_ACTIVE`: your organization cannot write right now.
        - `404 AGENT_GRANT_NOT_FOUND`: no such grant for your organization and environment.
        - `404 AGENT_PLATFORM_NOT_FOUND`: the grant's platform is not active.
        - `409 AGENT_GRANT_NOT_ACTIVE`: the grant is revoked or suspended.
        - `409 AGENTS_CHANGED`: an agent with its own mandate was registered while the check ran; retry.
        - `422 MANDATE_INVALID`: the mandate is not valid.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_grants:write
      x-ozav-availability: beta
      parameters:
        - name: grant_id
          in: path
          required: true
          description: The grant id (`ozav_agr_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentGrantVersionRequest"
            examples:
              lower_the_limit:
                summary: Lower the per-payment limit of version 1
                value:
                  idempotency_key: grant-42-v2
                  expected_version: 1
                  mandate:
                    actions:
                      - fiat_payout
                    per_payment_limit_usd: 20
                    valid_until: 2099-12-31T00:00:00Z
      responses:
        "200":
          description: The grant, with the version already published under this idempotency key (or the unchanged current one).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentGrant"
        "201":
          description: The grant, now on the new mandate version.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentGrant"
        "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/agent-grants/{grant_id}/versions \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "grant-42-v2",
                "expected_version": 1,
                "mandate": {
                  "actions": [
                    "fiat_payout"
                  ],
                  "per_payment_limit_usd": 20,
                  "valid_until": "2099-12-31T00:00:00Z"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-grants/{grant_id}/versions", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "grant-42-v2",
                "expected_version": 1,
                "mandate": {
                  "actions": [
                    "fiat_payout"
                  ],
                  "per_payment_limit_usd": 20,
                  "valid_until": "2099-12-31T00:00:00Z"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agent-grants/{grant_id}/versions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "grant-42-v2",
                    "expected_version": 1,
                    "mandate": {
                        "actions": [
                            "fiat_payout",
                        ],
                        "per_payment_limit_usd": 20,
                        "valid_until": "2099-12-31T00:00:00Z",
                    },
                },
            )
            data = response.json()
  /agents:
    post:
      operationId: registerAgent
      tags:
        - Agents
      summary: Register an agent under a grant
      description: |
        Only the platform the grant was given to can register agents under it, and only
        while the grant is active. An agent may carry its own narrower mandate. Fields you omit
        from `own_mandate` inherit the grant's mandate (or the parent agent's own mandate, when
        `parent_agent` has one), and the agent is stored with the explicit result; `valid_from`,
        when omitted, is the later of now and the parent's `valid_from`. A child agent
        (`parent_agent`) can never be wider than its parent or the grant: a wider `own_mandate` is
        refused with `422 MANDATE_WIDER_THAN_PARENT`, naming the fields as you wrote them
        (`per_payment_limit_usd`, `allowed_networks`, ...). A suspended platform gets `403
        AGENT_PLATFORM_SUSPENDED`.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:write
      x-ozav-availability: beta
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentRegisterRequest"
            examples:
              agent:
                summary: Register a purchasing agent
                value:
                  grant: ozav_agr_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  label: Data purchasing agent
                  platform_agent_ref: agent-7f3a
      responses:
        "201":
          description: The agent's passport.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        "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/agents \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "grant": "ozav_agr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "label": "Data purchasing agent",
                "platform_agent_ref": "agent-7f3a"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "grant": "ozav_agr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "label": "Data purchasing agent",
                "platform_agent_ref": "agent-7f3a"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "grant": "ozav_agr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "label": "Data purchasing agent",
                    "platform_agent_ref": "agent-7f3a",
                },
            )
            data = response.json()
  /agents/{agent_id}:
    get:
      operationId: getAgent
      tags:
        - Agents
      summary: Retrieve an agent's passport
      description: |
        Who the agent is, which grant it acts under, the mandate in force, and whether it
        can transact now (`can_transact`). Readable by the platform and by the company.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      responses:
        "200":
          description: The passport.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agents/{agent_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/revoke:
    post:
      operationId: revokeAgent
      tags:
        - Agents
      summary: Revoke an agent
      description: |
        The platform or the company revokes one agent. Its next payment request is refused
        and recorded. Revoking is final, and it reaches the agent's children (and theirs): every agent
        registered under it is revoked too. Revoking again returns the revoked agent.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      responses:
        "200":
          description: The revoked agent's passport.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/revoke \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/revoke", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/revoke",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/suspend:
    post:
      operationId: suspendAgent
      tags:
        - Agents
      summary: Suspend an agent
      description: |
        Pause an agent without revoking it. A suspended agent cannot pay, pay out, request a funding, register a payee or get a passport,
        and neither can any agent registered under it: every request re-reads the agent's whole lineage. The children keep their own status,
        so resuming the parent does not wake a child that was suspended on its own.

        Suspending is a brake, so it is easy: the platform the agent belongs to may suspend it, and so may the company that granted the
        mandate, with a key that has `agent_grants:write`. Suspending a suspended agent returns it unchanged.

        - `403 AGENT_SUSPEND_NOT_ALLOWED`: the company called with a key without `agent_grants:write`.
        - `409 AGENT_REVOKED`: a revoked agent is final; it cannot be suspended or resumed.
        - `404 AGENT_NOT_FOUND`: no such agent for this organization.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      responses:
        "200":
          description: The suspended agent's passport.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/suspend \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/suspend", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/suspend",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/resume:
    post:
      operationId: resumeAgent
      tags:
        - Agents
      summary: Resume a suspended agent
      description: |
        Give a suspended agent its power back. Only the company that granted the mandate can resume an agent, with a key that has
        `agent_grants:write`; the platform can suspend its agent but never resume it. Resuming an active agent returns it unchanged.
        An agent under a parent that is still suspended stays unable to act until the parent is resumed too.

        - `403 AGENT_RESUME_HOLDER_ONLY`: the caller is not the company that granted the mandate, or its key lacks `agent_grants:write`.
        - `409 AGENT_REVOKED`: a revoked agent is final.
        - `404 AGENT_NOT_FOUND`: no such agent for this organization.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      responses:
        "200":
          description: The resumed agent's passport.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/resume \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/resume", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/resume",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/passport:
    post:
      operationId: issueAgentPassport
      tags:
        - Agents
      summary: Issue the agent's signed passport
      description: |
        OZAV signs a passport for the agent: an SD-JWT (`vc+sd-jwt`, `vct`
        `ozav:agent-passport:v1`) that says who the agent is, which company it acts for, through which
        platform, and under which mandate (version, hash and limits). The platform or the company can
        request it.

        In the sandbox the passport is a test passport, so you can integrate issuing, presenting and verifying
        before going live. It is signed with a separate sandbox key, its issuer ends in `/sandbox`, its `vct`
        is `ozav:agent-passport:sandbox:v1`, `env` is `test` and the response says `sandbox: true`. Its
        `jwks_url` is the sandbox key set, and `POST .../credentials/verify-sandbox` verifies it. A production
        verifier never accepts it: the key, the issuer and the `vct` are all different. When OZAV has no
        sandbox key configured, the sandbox answers `503 AGENT_PASSPORT_UNAVAILABLE`.

        Visible in the token: the issuer, the agent id (`sub`), the environment (`env`), the validity
        and where to check the status. Selectively disclosed: `principal`,
        `platform`, `grant`, `mandate`, `agent_mandate` and `agent`. The agent chooses what to show.

        Anyone can verify it offline against `jwks_url`, and check the status at `status_url` without
        an API key. The status is computed when it is read. A passport stops being valid when the agent
        or the grant is revoked or suspended, when an agent above it is suspended or revoked, when the grant moves to a new mandate version, when the
        mandate expires, or when the company is no longer eligible (its organization, its verification
        or its account; the public reason is the same `principal_not_eligible` for all of them). It
        lasts at most 7 days, and never past the mandate's end.

        `409` when the agent cannot carry a passport right now: `AGENT_NOT_ACTIVE`,
        `AGENT_GRANT_NOT_ACTIVE`, `AGENT_MANDATE_NOT_IN_FORCE` or `PRINCIPAL_NOT_ELIGIBLE`. `503`
        `AGENT_PASSPORT_UNAVAILABLE` when OZAV cannot sign right now. `403 AGENT_PLATFORM_SUSPENDED`
        when the requesting platform is suspended.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      responses:
        "201":
          description: The signed passport.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentPassport"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/passport \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/passport", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/passport",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/request-signatures:
    post:
      operationId: signAgentRequest
      tags:
        - Agents
      summary: Sign an HTTP request the agent is about to make
      description: |
        **Beta.** OZAV signs the HTTP request your agent is about to send to a site (the IETF Web
        Bot Auth protocol, over RFC 9421 HTTP Message Signatures). Send the
        target `authority` (and, to bind the signature tighter, the `method` and `path`); OZAV returns
        the headers to add to that request: `Signature-Agent`, `Signature-Input`, `Signature` and
        `OZAV-Agent` (the agent id, covered by the signature).

        The site verifies the signature with OZAV's public key, from the directory at
        `directory_url` (`https://ozav.io/.well-known/http-message-signatures-directory`), with no
        OZAV account. The signature covers `@authority`, the `method` and `path` when you send them,
        `Signature-Agent` and `OZAV-Agent`; it is valid for 60 seconds, with a fresh nonce each time.
        The agent never holds a key: OZAV signs only while the agent (and every agent above it), its
        grant, its mandate and its company are in force, so revoking the agent or the grant takes
        effect on the next request.

        Only the platform the agent belongs to can request signatures. `409` when OZAV does not sign
        for the agent right now: `AGENT_NOT_ACTIVE`, `AGENT_GRANT_NOT_ACTIVE`,
        `AGENT_MANDATE_NOT_IN_FORCE` or `PRINCIPAL_NOT_ELIGIBLE`. `422
        REQUEST_SIGNATURE_TARGET_INVALID` when the authority, method or path is malformed. `503
        AGENT_WEB_IDENTITY_UNAVAILABLE` while signing is not enabled. In the sandbox the headers have
        the same form, `simulated` is `true`, and they are signed with a key no directory lists, so no
        site accepts them.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentRequestSignatureRequest"
            examples:
              get:
                summary: Sign a GET to a data API
                value:
                  authority: api.example.com
                  method: GET
                  path: /v1/quotes
      responses:
        "200":
          description: The headers to add to the request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentRequestSignature"
        "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/agents/{agent_id}/request-signatures \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "authority": "api.example.com",
                "method": "GET",
                "path": "/v1/quotes"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/request-signatures", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "authority": "api.example.com",
                "method": "GET",
                "path": "/v1/quotes"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/request-signatures",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "authority": "api.example.com",
                    "method": "GET",
                    "path": "/v1/quotes",
                },
            )
            data = response.json()
  /agents/{agent_id}/payouts:
    post:
      operationId: requestAgentPayout
      tags:
        - Agents
      summary: Pay a registered beneficiary in fiat currency
      description: |
        The agent pays a registered payout destination of the company (for example a
        Brazilian supplier by Pix, or a US bank account in dollars) from the company's managed stablecoin wallet. The
        currency the beneficiary receives is `amount.currency` and must match the destination's currency. OZAV quotes the
        payout, checks the grant's mandate and the agent's own against the quoted dollar amount, the corridor
        (`USDC>BRL`, `USDC>USD`, ...) and the allowed payees, and records the request. In the sandbox the same path runs
        with simulated money: fixed rates per US dollar of stablecoin (R$ 5.00, US$ 1.00, EUR 0.90; any other currency
        answers `422 AGENT_PAYOUT_QUOTE_UNAVAILABLE` with `UNSUPPORTED_CURRENCY`), a simulated transfer (the same id for the
        same action) and no call to any external service; the receipt in `payout` carries `simulated: true`.

        Within the mandate, OZAV creates the payout through the same transfer flow the company uses,
        with the same compliance checks, beneficiary ownership rules and ledger reservation. The
        response carries the transfer in `payout`.

        The stablecoin can also leave the agent's own wallet, the one OZAV keeps for it (`POST /agents/{agent_id}/wallet`):
        send `source: { type: "agent_wallet", currency: "USDC", chain: "polygon" }`. Production only, Polygon only, and
        only while the feature is enabled for your platform. OZAV first moves the exact amount from the agent's wallet
        to its own treasury (the agent's wallet signs inside OZAV's own infrastructure, and the per-payment limit sealed
        into that wallet bounds the amount), then creates the transfer. If anything is uncertain after that first move,
        nothing is released: the request answers `503 AGENT_PAYOUT_OUTCOME_UNKNOWN`, the budget stays held and OZAV
        reviews it.
        - `422 AGENT_WALLET_NOT_ACTIVE`: the agent has no active wallet on that network in OZAV's infrastructure (or
          the request is for the sandbox). Nothing was quoted.
        - `422 AGENT_PAYOUT_ABOVE_WALLET_LIMIT`: the amount is above the per-payment limit sealed in the agent's
          wallet. Nothing was paid; the budget was released.
        - `503 AGENT_ENCLAVE_PAYOUTS_NOT_ENABLED`: payouts from the agent's wallet are not enabled yet. Nothing was quoted.

        - Above the approval threshold: `202`. Nothing is paid until a person at the company approves
          it in the OZAV portal. The decision arrives as the webhook `agent_action.updated` (`authorized`,
          `refused` or `expired`). Once a person approves, OZAV runs the payout on its own, with no new
          request from you: the webhook then reports it `executed`. If it could not run (for example the
          new quote costs more than allowed), it stays `authorized` and you can repeat the request with
          the same `idempotency_key` and the same
          body: the destination, the amount and currency the beneficiary receives, the wallet and the
          network are what was approved, and a repeat that changes any of them is
          `409 AGENT_PAYMENT_CONFLICT`. Either way OZAV quotes again, and pays only if the new quote costs at most
          2% more than the approved amount (`409 AGENT_PAYOUT_DIFFERS_FROM_APPROVED` otherwise). A quote
          whose target is not the amount and currency you asked for is never paid: it answers
          `422 AGENT_PAYOUT_QUOTE_UNAVAILABLE`.
        - `503 AGENT_PAYOUT_OUTCOME_UNKNOWN`: OZAV could not confirm whether the payout was created.
          Repeat with the same `idempotency_key`, never a new one. OZAV marks the payout as dispatched, in
          one database transaction that also checks the grant, the mandate and the agent's lineage, before
          it calls the transfer service. Once dispatched, the repeat completes it even if the grant was
          revoked afterwards (the transfer may exist). Before that, a grant, an agent or a parent revoked
          or suspended, or a mandate that changed, makes the repeat `422 AGENT_PAYMENT_OUTSIDE_MANDATE` (or
          `409 AGENT_MANDATE_CHANGED`), and nothing is paid. The retry sends the same payout
          again and returns the original result; it never pays twice.
        - `422 AGENT_PAYOUT_REFUSED`: refused before any money moved. `details.upstream.code` names a
          problem you can fix (amount, currency, corridor, destination, quote, balance, or the balance on
          that network: `CHAIN_COVERAGE_INSUFFICIENT` means another network may cover it); any other
          reason reads `REFUSED`. The budget is released.
        - `422 AGENT_PAYOUT_QUOTE_UNAVAILABLE`: no quote for this corridor and amount right now
          (`details.upstream.code`, same rule).
        - `422 AGENT_PAYMENT_NOT_PERMITTED`: this payout cannot be made from this account right now.
          The message is fixed; nothing was paid.
        - `422 WALLET_QUORUM_REQUIRED`: the company's wallet policy requires several of its keys to
          approve this amount; an agent payout cannot wait for that.
        - `422 AGENT_PAYMENT_OUTSIDE_MANDATE`: outside the mandate, with the reasons (among them `above_recurring_budget`
          when the amount would pass the mandate's recurring budget for the current window, and `above_agent_allocation` when
          the agent has an allocation and the amount does not fit in what is left of it in the source currency). The refusal is
          recorded. The source asset and network are checked against `allowed_assets` and
          `allowed_networks` too. It is also returned, without reasons, when the grant, the agent or an
          agent above it is revoked or suspended, or the mandate expired, between the quote and the
          transfer; nothing was paid.
        - `409 AGENT_MANDATE_CHANGED`: the mandate changed after the payout was approved. Nothing was
          paid; request it again.
        - `409 AGENT_PAYMENT_CONFLICT`: this `idempotency_key` was used under this grant for a different
          payout, or by a different agent, or the repeat changes the destination, the amount or currency
          the beneficiary receives, the wallet or the network.
          Only that: a budget or limit that a parallel request used up first is the recorded `422 AGENT_PAYMENT_OUTSIDE_MANDATE` refusal.
        - `409 AGENT_PAYMENT_FAILED`: a repeat of a payout that failed. Use a new key to try again.
        - `409 AGENT_GRANT_NOT_ACTIVE` / `409 AGENT_NOT_ACTIVE`: checked before quoting, so a revoked
          grant never spends the company's request limit.
        - `403 AGENT_PLATFORM_SUSPENDED`: your platform is suspended.
        - `503 AGENT_PAYOUTS_NOT_ENABLED`: agent payouts are not enabled yet in production; nothing was quoted. The
          sandbox does not need it.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentPayoutRequest"
            examples:
              pix:
                summary: Pay a supplier R$ 1.250,00 by Pix, from USDC on Polygon
                value:
                  idempotency_key: invoice-2026-0917
                  payout_destination_id: ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  amount:
                    amount: "1250.00"
                    currency: BRL
                  source:
                    wallet_id: ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V2
                    currency: USDC
                    chain: polygon
              agent_wallet:
                summary: Pay a supplier R$ 1.250,00 by Pix from the agent's own wallet
                value:
                  idempotency_key: invoice-2026-0918
                  payout_destination_id: ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  amount:
                    amount: "1250.00"
                    currency: BRL
                  source:
                    type: agent_wallet
                    currency: USDC
                    chain: polygon
      responses:
        "200":
          description: The recorded payout request (a retry of one already created).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "201":
          description: The payout was created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "202":
          description: Recorded and waiting for a person at the company to approve.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "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/agents/{agent_id}/payouts \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "invoice-2026-0917",
                "payout_destination_id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "amount": {
                  "amount": "1250.00",
                  "currency": "BRL"
                },
                "source": {
                  "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                  "currency": "USDC",
                  "chain": "polygon"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/payouts", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "invoice-2026-0917",
                "payout_destination_id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "amount": {
                  "amount": "1250.00",
                  "currency": "BRL"
                },
                "source": {
                  "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                  "currency": "USDC",
                  "chain": "polygon"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/payouts",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "invoice-2026-0917",
                    "payout_destination_id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "amount": {
                        "amount": "1250.00",
                        "currency": "BRL",
                    },
                    "source": {
                        "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                        "currency": "USDC",
                        "chain": "polygon",
                    },
                },
            )
            data = response.json()
  /agents/{agent_id}/yield-applications:
    post:
      operationId: requestAgentYieldApplication
      tags:
        - Agents
      summary: Apply the company's custodied balance to managed yield
      description: |
        The agent applies part of the company's custodied stablecoin balance to a managed yield strategy
        (`GET /yield/strategies`). It is the same managed path as `POST /yield/applications`: OZAV records an order and
        applies it under its custody quorum, with the same exposure caps and compliance checks. The money stays the
        company's: it becomes a yield position in its name. The agent's own wallet is not used.

        The mandate decides: the grant's mandate and the agent's own must have the `yield_application` action and
        `yield_rules` naming the strategy; the amount must fit `yield_rules.per_application_usd` and
        `yield_rules.daily_usd`. An application is not spending, so the spending limits (`per_payment_limit_usd`,
        `daily_limit_usd`, `total_budget_usd`, `recurring_budget`) do not apply. At or above
        `human_approval_above_usd` the response is `202` and nothing is applied until a person at the company approves
        it in the OZAV portal; then repeat the request with the same `idempotency_key` and body.

        The agent never gets more than the company could do with its own API key. Before anything is recorded, OZAV
        checks that the company has managed custody, a live API key with the `yield:apply` scope, and managed yield
        turned on for its account; otherwise the request is `422 AGENT_YIELD_APPLICATION_REFUSED` with
        `details.upstream.code`.

        The response is `executed` once the order exists (`yield_application.application_id`); the order then runs on
        its own. The action becomes `settled` when the order is applied on chain, or `returned` when it is refused or
        fails before money moves (the daily limit is released). Follow it with
        `GET /agents/{agent_id}/yield-applications/{action_id}` or the `agent_action.updated` webhook. In the sandbox the
        decision runs for real and the order is simulated; `POST /agent-actions/{action_id}/simulate` settles or returns it.

        To take money back out, use `POST /agents/{agent_id}/yield-redemptions` (the `yield_redemption` action).

        - `503 AGENT_YIELD_NOT_ENABLED`: agent yield applications are not enabled yet (live and sandbox). Nothing was
          recorded.
        - `422 AGENT_YIELD_APPLICATION_REFUSED`: the company cannot apply to yield right now. `details.upstream.code` is
          `CUSTODY_NOT_ENABLED`, `YIELD_APPLY_NOT_GRANTED`, `MANAGED_YIELD_NOT_ENABLED` or
          `MANAGED_YIELD_CLIENT_NOT_ENABLED` (nothing was recorded), or `REFUSED` when the order was refused before it
          existed (the action is `failed`).
        - `404 YIELD_STRATEGY_NOT_FOUND`: no such strategy. `422 AGENT_YIELD_CURRENCY_MISMATCH`: `amount.currency` is
          not the strategy's asset, or has more decimal places than it.
        - `409 AGENT_YIELD_APPLICATION_IN_FLIGHT`: another application to the same strategy is still running for this
          company. Nothing was applied; use a new `idempotency_key` once it finishes.
        - `422 AGENT_PAYMENT_OUTSIDE_MANDATE`: outside the mandate, with the reasons (`yield_strategy_not_allowed`,
          `above_yield_per_application`, `above_yield_daily`, `yield_rules_missing`, `action_not_allowed`, ...). The
          refusal is recorded.
        - `409 AGENT_PAYMENT_CONFLICT`: this `idempotency_key` was used for a different request.
          Only that: a budget or limit that a parallel request used up first is the recorded `422 AGENT_PAYMENT_OUTSIDE_MANDATE` refusal.
        - `409 AGENT_PAYMENT_FAILED`: a repeat of an application that failed, or was refused or failed before money moved. Use a new
          key to try again.
        - `409 AGENT_GRANT_NOT_ACTIVE` / `409 AGENT_NOT_ACTIVE` / `409 AGENT_MANDATE_CHANGED`. A grant, an agent or a parent revoked
          or suspended after the approval, or after a request that did not finish, refuses the repeat
          (`422 AGENT_PAYMENT_OUTSIDE_MANDATE`) and no order is created.
        - `403 NOT_THE_GRANTED_PLATFORM`: only the platform this agent belongs to can apply to yield for it.
        - `403 AGENT_PLATFORM_SUSPENDED`: your platform is suspended.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentYieldApplicationRequest"
            examples:
              usdc:
                summary: Apply US$ 2,500 of USDC to a strategy on Base
                value:
                  idempotency_key: treasury-sweep-2026-10-16
                  strategy_id: base-usdc-prime
                  amount:
                    amount: "2500.00"
                    currency: USDC
      responses:
        "200":
          description: The recorded application (a retry of one already created).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "201":
          description: The order was created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "202":
          description: Recorded and waiting for a person at the company to approve.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "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/agents/{agent_id}/yield-applications \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "treasury-sweep-2026-10-16",
                "strategy_id": "base-usdc-prime",
                "amount": {
                  "amount": "2500.00",
                  "currency": "USDC"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/yield-applications", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "treasury-sweep-2026-10-16",
                "strategy_id": "base-usdc-prime",
                "amount": {
                  "amount": "2500.00",
                  "currency": "USDC"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/yield-applications",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "treasury-sweep-2026-10-16",
                    "strategy_id": "base-usdc-prime",
                    "amount": {
                        "amount": "2500.00",
                        "currency": "USDC",
                    },
                },
            )
            data = response.json()
  /agents/{agent_id}/yield-applications/{action_id}:
    get:
      operationId: getAgentYieldApplication
      tags:
        - Agents
      summary: Read an agent's yield application and its order
      description: |
        The yield application (the company or the platform reads it), with the managed yield order as it is now
        (`pending`, `running`, `applied`, `refused`, `failed`, `indeterminate`). The action itself closes from the
        order on OZAV's schedule; this read shows the order in between. The order's `detail` and `tx_hash` are the
        company's: the platform reads them as `null`.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
        - name: action_id
          in: path
          required: true
          description: The action id (`ozav_aga_...`).
          schema:
            type: string
      responses:
        "200":
          description: The application and its order.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentYieldApplication"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/yield-applications/{action_id} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/yield-applications/{action_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agents/{agent_id}/yield-applications/{action_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/yield-redemptions:
    post:
      operationId: requestAgentYieldRedemption
      tags:
        - Agents
      summary: Take the company's balance back out of managed yield
      description: |
        The agent redeems part of the company's managed yield position (or all of it) back to the company's custody
        address. It is the same managed path as `POST /yield/redemptions`: OZAV records an order and executes it under
        its custody quorum. The money stays the company's: it returns to its own address. The agent's own wallet is not
        used.

        The mandate decides, and only on the action: the grant's mandate and the agent's own must have the
        `yield_redemption` action and be in force. There are no `yield_rules`, no limit and no approval threshold: the
        money returns to the company, so it is not spending and the spending limits do not apply. An agent whose mandate
        lists only `yield_application` cannot redeem.

        Redemption is always allowed: turning managed yield off for the platform or for the company stops new
        applications and never an exit, and the agent yield-application switch does not apply. Redemption has its
        own environment switch, the same one `POST /yield/redemptions` uses.

        Send exactly one of `amount` and `all: true`. The request is checked against the company's position on chain
        before any order exists: above the position, or above what the vault releases now, it is refused and nothing is
        written. The agent never gets more than the company could do with its own API key: the company needs managed
        custody and a live API key with the `yield:apply` scope, otherwise the request is
        `422 AGENT_YIELD_REDEMPTION_REFUSED` with `details.upstream.code`.

        The response is `executed` once the order exists (`yield_redemption.redemption_id`); the order then runs on its
        own. The action becomes `settled` when the order is redeemed on chain, or `returned` when it is refused or
        fails before money moves (the balance is still in the strategy). Follow it with
        `GET /agents/{agent_id}/yield-redemptions/{action_id}` or the `agent_action.updated` webhook. In the sandbox the
        decision runs for real and the order is simulated; `POST /agent-actions/{action_id}/simulate` settles or returns it.

        - `503 AGENT_YIELD_REDEMPTION_NOT_ENABLED`: agent yield redemptions are not enabled yet (live and sandbox).
          Nothing was recorded.
        - `422 AGENT_YIELD_REDEMPTION_REFUSED`: the company cannot redeem right now. `details.upstream.code` is
          `CUSTODY_NOT_ENABLED` or `YIELD_APPLY_NOT_GRANTED`. Nothing was recorded.
        - `404 YIELD_STRATEGY_NOT_FOUND`: no such strategy. `422 AGENT_YIELD_CURRENCY_MISMATCH`: `amount.currency` is
          not the strategy's asset, or has more decimal places than it.
        - `409 YIELD_REDEMPTION_EXCEEDS_POSITION`, `409 WITHDRAW_EXCEEDS_AVAILABLE`: the amount is above the position,
          there is nothing to redeem, or the vault will not release that much now. The action is `failed` and nothing
          was written. `503 YIELD_POSITION_UNREADABLE` / `503 WITHDRAW_LIMIT_UNREADABLE`: the chain could not be read;
          repeat the request with the same `idempotency_key`.
        - `409 AGENT_YIELD_REDEMPTION_IN_FLIGHT`: another order on the same strategy is still running for this company.
          Nothing was redeemed; use a new `idempotency_key` once it finishes.
        - `422 AGENT_PAYMENT_OUTSIDE_MANDATE`: outside the mandate, with the reasons (`action_not_allowed`,
          `mandate_expired`, ...). The refusal is recorded.
        - `409 AGENT_PAYMENT_CONFLICT`: this `idempotency_key` was used for a different request.
          Only that: a budget or limit that a parallel request used up first is the recorded `422 AGENT_PAYMENT_OUTSIDE_MANDATE` refusal.
        - `409 AGENT_PAYMENT_FAILED`: a repeat of a redemption that failed, or was refused or failed before money moved.
          Use a new key to try again.
        - `409 AGENT_GRANT_NOT_ACTIVE` / `409 AGENT_NOT_ACTIVE` / `409 AGENT_MANDATE_CHANGED`: the grant, the agent or a
          parent is not active, or the mandate changed.
        - `422 AGENT_PAYMENT_OUTSIDE_MANDATE` on a repeat: a grant, an agent or a parent revoked or suspended after a
          request that did not finish refuses the repeat, and no order is created.
        - `403 NOT_THE_GRANTED_PLATFORM`: only the platform this agent belongs to can redeem yield for it.
        - `403 AGENT_PLATFORM_SUSPENDED`: your platform is suspended.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentYieldRedemptionRequest"
            examples:
              amount:
                summary: Take US$ 2 of USDC back from a strategy on Base
                value:
                  idempotency_key: treasury-exit-2026-10-16
                  strategy_id: base-usdc-prime
                  amount:
                    amount: "2.00"
                    currency: USDC
              all:
                summary: Take the whole position back
                value:
                  idempotency_key: treasury-exit-all-2026-10-16
                  strategy_id: base-usdc-prime
                  all: true
      responses:
        "200":
          description: The recorded redemption (a retry of one already created).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "201":
          description: The order was created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "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/agents/{agent_id}/yield-redemptions \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "treasury-exit-2026-10-16",
                "strategy_id": "base-usdc-prime",
                "amount": {
                  "amount": "2.00",
                  "currency": "USDC"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/yield-redemptions", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "treasury-exit-2026-10-16",
                "strategy_id": "base-usdc-prime",
                "amount": {
                  "amount": "2.00",
                  "currency": "USDC"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/yield-redemptions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "treasury-exit-2026-10-16",
                    "strategy_id": "base-usdc-prime",
                    "amount": {
                        "amount": "2.00",
                        "currency": "USDC",
                    },
                },
            )
            data = response.json()
  /agents/{agent_id}/yield-redemptions/{action_id}:
    get:
      operationId: getAgentYieldRedemption
      tags:
        - Agents
      summary: Read an agent's yield redemption and its order
      description: |
        The yield redemption (the company or the platform reads it), with the managed yield order as it is now
        (`pending`, `running`, `redeemed`, `refused`, `failed`, `indeterminate`). The action itself closes from the
        order on OZAV's schedule; this read shows the order in between. The order's `detail` and `tx_hash` are the
        company's: the platform reads them as `null`.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
        - name: action_id
          in: path
          required: true
          description: The action id (`ozav_aga_...`).
          schema:
            type: string
      responses:
        "200":
          description: The redemption and its order.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentYieldRedemption"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/yield-redemptions/{action_id} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/yield-redemptions/{action_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agents/{agent_id}/yield-redemptions/{action_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/funding-from-wallet:
    post:
      operationId: fundAgentAllocation
      tags:
        - Agents
      summary: Set aside part of the company's balance for one agent
      description: |
        Raises the agent's allocation: the part of the account holder's balance this agent may spend, per currency. Once an
        agent has an allocation, its payments and payouts must fit in what is left of it, on top of every mandate limit, and an
        asset with no allocation cannot be spent at all. An agent with no allocation is not limited this way.

        **No money moves.** The allocation is a limit kept by OZAV inside the account holder's balance, not a separate account:
        the holder can still spend the same balance by other paths. At the moment of the request, every unspent allocation of
        the holder in that currency plus this amount must fit in the holder's available balance. A spend that is returned,
        expires or fails gives its amount back to the allocation by itself.

        Called by the account holder or by the agent's platform. The same `idempotency_key` with the same body returns the same
        result (`200`). In the sandbox the holder balance is simulated and large, so only the allocations themselves limit it.

        - `422 AGENT_ALLOCATION_ABOVE_BALANCE`: the holder does not have this much available beyond what its agents already hold.
        - `409 AGENT_NOT_ACTIVE`: the agent or its grant is not active.
        - `409 IDEMPOTENCY_KEY_REUSED`: this key was used with a different body.
        - `403 AGENT_PLATFORM_SUSPENDED`: your platform is suspended.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentAllocationMoveRequest"
            examples:
              monthly:
                summary: Allocate 200 USDC to the agent
                value:
                  idempotency_key: agent-7-2026-11
                  amount:
                    amount: "200.00"
                    currency: USDC
      responses:
        "200":
          description: A repeat of an allocation already recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAllocation"
        "201":
          description: The allocation was raised.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAllocation"
        "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/agents/{agent_id}/funding-from-wallet \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "agent-7-2026-11",
                "amount": {
                  "amount": "200.00",
                  "currency": "USDC"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/funding-from-wallet", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "agent-7-2026-11",
                "amount": {
                  "amount": "200.00",
                  "currency": "USDC"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/funding-from-wallet",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "agent-7-2026-11",
                    "amount": {
                        "amount": "200.00",
                        "currency": "USDC",
                    },
                },
            )
            data = response.json()
  /agents/{agent_id}/defunding:
    post:
      operationId: defundAgentAllocation
      tags:
        - Agents
      summary: Give back what the agent has not spent
      description: |
        Lowers the agent's allocation by an amount it has not spent. No money moves: the amount simply stops being set aside for
        this agent. Works even after the agent or its grant was revoked.

        - `422 AGENT_ALLOCATION_INSUFFICIENT`: the agent does not have this much unspent in that currency.
        - `409 IDEMPOTENCY_KEY_REUSED`: this key was used with a different body.
        - `403 AGENT_PLATFORM_SUSPENDED`: your platform is suspended.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentAllocationMoveRequest"
            examples:
              giveBack:
                summary: Give back 50 USDC
                value:
                  idempotency_key: agent-7-giveback-1
                  amount:
                    amount: "50.00"
                    currency: USDC
      responses:
        "200":
          description: A repeat of a defunding already recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAllocation"
        "201":
          description: The allocation was lowered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAllocation"
        "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/agents/{agent_id}/defunding \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "agent-7-giveback-1",
                "amount": {
                  "amount": "50.00",
                  "currency": "USDC"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/defunding", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "agent-7-giveback-1",
                "amount": {
                  "amount": "50.00",
                  "currency": "USDC"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/defunding",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "agent-7-giveback-1",
                    "amount": {
                        "amount": "50.00",
                        "currency": "USDC",
                    },
                },
            )
            data = response.json()
  /agents/{agent_id}/allocation:
    get:
      operationId: getAgentAllocation
      tags:
        - Agents
      summary: How much of its allocation the agent has spent
      description: |
        Per currency: what was allocated, what the agent spent from it (including requests still waiting for a person or not yet
        settled) and what is left. An agent with no allocation returns an empty `balances`. A request the agent opened before the
        allocation existed, and that is still open, counts as spent: `spent` can then be larger than `allocated`, and `available` is zero.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      responses:
        "200":
          description: The agent's allocation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAllocation"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/allocation \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/allocation", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agents/{agent_id}/allocation",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/wallet:
    post:
      operationId: registerAgentWallet
      tags:
        - Agents
      summary: Give an agent a wallet (bring your own Safe, or a wallet OZAV holds)
      description: |
        Two ways, chosen by `custody`.

        **`custody: ozav_enclave`: a wallet OZAV holds for the agent.** OZAV creates, in its own secure signing infrastructure, a
        wallet for this one live agent. The wallet can only sign a USDC payment authorization (x402 and MPP `authorization`) from
        itself, and never one above the per-payment cap of the mandate: that cap is `teto_por_operacao_usd` of the grant's mandate (the
        tighter one, when the agent has its own), read when the wallet is created and sealed inside the wallet, where nothing outside it
        can change it. The same address serves Base and Polygon. Idempotent: the first call reserves the wallet and answers `202`
        (`status: pending`, no address yet); OZAV then creates it, which is not immediate, so call again later until
        `status: active`, which records the address (`201` once, `200` after) and returns it. The grant's mandate must set a per-operation cap. If the mandate's cap changes later, the wallet keeps
        the cap it was created with: a payment above it is refused by the wallet, a payment below it is still limited by the mandate; a
        wallet with a new cap is a new wallet and is not offered yet. Only the account holder's organization, with `agent_grants:write`;
        live agents only; off until the account has it enabled (`503 AGENT_ENCLAVE_WALLETS_NOT_ENABLED`). An agent has either this
        wallet or a Safe, never both.

        - `422 AGENT_ENCLAVE_WALLET_LIVE_ONLY`: a sandbox agent's money is simulated; it has no wallet of its own.
        - `422 AGENT_ENCLAVE_WALLET_NEEDS_A_PER_OPERATION_CAP`: the mandate sets no per-operation cap, so there is nothing to seal.
        - `409 AGENT_WALLET_HAS_A_SAFE`, `AGENT_REVOKED`, `AGENT_GRANT_NOT_ACTIVE`, `AGENT_ENCLAVE_WALLET_FAILED` (OZAV replaces the wallet
          with a new one with the same cap; call again later).

        **`custody: bring_your_own`: a Safe the company deployed (non-custodial).** The money stays in a Safe the company deployed, and nothing leaves it without OZAV's signature. The company
        deploys a Safe 1.4.1 on Base or Polygon with exactly two owners, its own key (or its agent's) and OZAV's policy key, a
        threshold of 2, no module, and the standard Safe compatibility fallback (it answers EIP-1271 when a USDC payment settles).
        OZAV reads the network and registers the Safe only if all of that holds; nothing is written to any network.

        Only the account holder's organization registers it, with a key that has `agent_grants:write`. Off until bring-your-own
        wallets are enabled for the account (`422 AGENT_SMART_ACCOUNT_NOT_ENABLED`). Once registered, the agent's payments, payouts,
        fundings and allocations are refused with `422 AGENT_SMART_ACCOUNT_SIGNING_UNAVAILABLE` until signing through the Safe is
        switched on: they never fall back to a custodial wallet. The company's owner must be a key (an address without code).

        - `422 SMART_ACCOUNT_NOT_DEPLOYED`, `SMART_ACCOUNT_NOT_A_SAFE`, `SMART_ACCOUNT_THRESHOLD_NOT_2`, `SMART_ACCOUNT_OZAV_NOT_OWNER`,
          `SMART_ACCOUNT_HAS_MODULES`, `SMART_ACCOUNT_NO_SIGNATURE_HANDLER`, `SMART_ACCOUNT_OWNER_NOT_A_KEY`, `SMART_ACCOUNT_UNREADABLE`: what the network answered
          does not meet the rule above. Nothing is recorded.
        - `409 AGENT_WALLET_ALREADY_REGISTERED`: the agent already has another Safe on this network, or this Safe is registered for another agent.
        - `409 AGENT_WALLET_HAS_AN_ENCLAVE_WALLET`: the agent already has a wallet OZAV holds for it.
        - `409 AGENT_WALLET_OPEN_CUSTODIAL_ACTIONS`: the agent has a custodial payout awaiting approval or approved; let it finish or expire first.
        - `403 AGENT_WALLET_HOLDER_ONLY`: only the account holder's organization, with `agent_grants:write`.
        - `503 AGENT_POLICY_SIGNER_UNAVAILABLE` or `NETWORK_UNAVAILABLE`: nothing was registered; retry later.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_grants:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentWalletRequest"
            examples:
              safe:
                summary: Register a Safe on Base
                value:
                  custody: bring_your_own
                  safe_address: "0xa52928484b43dfa36f8795340e84cf38a83c8569"
                  network: base
              ozav_enclave:
                summary: A wallet OZAV holds, on Polygon
                value:
                  custody: ozav_enclave
                  network: polygon
      responses:
        "200":
          description: This Safe was already registered for the agent on this network, or the wallet OZAV holds was already recorded for this network.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentWallet"
        "201":
          description: The Safe was verified and registered, or the wallet OZAV holds is active and its address was recorded for this network.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentWallet"
        "202":
          description: The wallet OZAV holds is being created (`status` is `pending`). Call again; the address comes once it is active.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentWallet"
        "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/agents/{agent_id}/wallet \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "custody": "bring_your_own",
                "safe_address": "0xa52928484b43dfa36f8795340e84cf38a83c8569",
                "network": "base"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/wallet", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "custody": "bring_your_own",
                "safe_address": "0xa52928484b43dfa36f8795340e84cf38a83c8569",
                "network": "base"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/wallet",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "custody": "bring_your_own",
                    "safe_address": "0xa52928484b43dfa36f8795340e84cf38a83c8569",
                    "network": "base",
                },
            )
            data = response.json()
    get:
      operationId: getAgentWallet
      tags:
        - Agents
      summary: Which wallet the agent pays from
      description: |
        `custody: ozav` when the agent pays from the account holder's OZAV wallet; `bring_your_own` with the registered Safes when
        the company brought its own; `ozav_enclave` with the recorded addresses (`enclave_wallets`) when OZAV holds a wallet for the agent.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      responses:
        "200":
          description: The agent's wallet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentWallet"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/wallet \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/wallet", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agents/{agent_id}/wallet",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/wallet/withdraw:
    post:
      operationId: withdrawFromAgentWallet
      tags:
        - Agents
      summary: Take USDC back from the agent's wallet
      description: |
        The account holder moves USDC out of the wallet OZAV holds for its agent (`custody: ozav_enclave`) and back to the
        holder. Omit `amount` to withdraw the whole balance. The money leaves the agent's address on Polygon for OZAV's treasury,
        signed inside OZAV's own infrastructure; the holder's balance with OZAV does not change, because the agent's money already
        counts as the holder's. It is not the agent spending: the mandate budget is not used and no agent action is recorded.

        The wallet signs no single payment above the per-payment limit sealed into it (`per_payment_cap` of
        `POST /agents/{agent_id}/wallet`), so a larger amount is split into parts of at most that limit, run one after the
        other (at most 50 parts). A part waits for the network: when the request runs out of time before every part has started,
        it answers `202` with `status: processing`. Call again with the same `idempotency_key` to carry on from the first part
        that has not settled; OZAV also carries on by itself. The same key always answers the same withdrawal and never signs a
        part twice.

        When an agent or its grant is revoked, OZAV withdraws the agent wallet's whole balance on its own (`trigger: revocation`),
        and tries again later when a part was refused. USDC that reaches a revoked agent's wallet after that is withdrawn with
        this call.

        Each status of a withdrawal, whichever way it started, reaches the holder's organization as the webhook
        `agent_wallet_withdrawal.updated`, once per status, with the withdrawal in `data`.

        Only the account holder's organization, with a key that has `agent_grants:write`. Production only. Works after the
        agent or its grant was revoked. Off until the account has it enabled.

        - `400 INVALID_AMOUNT`: `amount.amount` is not a positive decimal with at most 6 places.
        - `404 AGENT_NOT_FOUND`: no such agent for this key.
        - `403 AGENT_WALLET_HOLDER_ONLY`: the caller is not the account holder's organization, or the key lacks `agent_grants:write`.
        - `503 AGENT_WALLET_WITHDRAW_NOT_ENABLED`: withdrawals from agent wallets are not enabled yet. Nothing was withdrawn.
        - `422 AGENT_WALLET_NOT_ACTIVE`: the agent has no wallet on Polygon in OZAV's infrastructure (or it is a sandbox agent).
        - `422 AGENT_WALLET_WITHDRAW_ABOVE_BALANCE`, `422 AGENT_WALLET_EMPTY`, `422 AGENT_WALLET_WITHDRAW_TOO_MANY_PARTS`: nothing was withdrawn.
        - `409 AGENT_WALLET_WITHDRAW_PAUSED`: withdrawals are paused right now; nothing was withdrawn, retry later.
        - `409 AGENT_WALLET_WITHDRAW_IN_PROGRESS`: another withdrawal from this wallet is still processing or under review.
        - `409 IDEMPOTENCY_KEY_REUSED`: this key was used with a different body.
        - `503 NETWORK_UNAVAILABLE`: the wallet's balance could not be read; nothing was withdrawn.
        - `422 AGENT_WALLET_WITHDRAW_REFUSED`, `422 AGENT_WALLET_WITHDRAW_ABOVE_WALLET_LIMIT`: a part was refused before it moved;
          nothing after it runs. `details.withdrawal.settled_amount` says what was withdrawn.
        - `503 AGENT_WALLET_WITHDRAW_OUTCOME_UNKNOWN`: OZAV could not confirm a part and is reviewing it; nothing more runs on its
          own. Retry with the same key to read it, never with a new one.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_grants:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentWalletWithdrawRequest"
            examples:
              wholeBalance:
                summary: Withdraw the whole balance
                value:
                  idempotency_key: agent-7-withdraw-1
              someOfIt:
                summary: Withdraw 25 USDC
                value:
                  idempotency_key: agent-7-withdraw-2
                  amount:
                    amount: "25.00"
                    currency: USDC
      responses:
        "200":
          description: A repeat of a withdrawal that already settled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentWalletWithdrawal"
        "201":
          description: Every part settled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentWalletWithdrawal"
        "202":
          description: Still processing. Call again with the same `idempotency_key`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentWalletWithdrawal"
        "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/agents/{agent_id}/wallet/withdraw \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "agent-7-withdraw-1"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/wallet/withdraw", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "agent-7-withdraw-1"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/wallet/withdraw",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "agent-7-withdraw-1",
                },
            )
            data = response.json()
  /agents/{agent_id}/consumptions:
    post:
      operationId: recordAgentConsumption
      tags:
        - Agents
      summary: Record what the agent consumed from a prepaid resource
      description: |
        The agent declares that it used part of a prepaid resource the company registered (`POST
        /agent-grants/{grant_id}/prepaid-resources`). Nothing moves money: the consumption has no dollar amount, counts in no
        spending limit or budget, and its `nature` is `prepaid` (`simulated` in the sandbox). OZAV debits the resource's balance
        in the same step that records the action, so two agents can never consume the same unit and the balance never goes
        below zero.

        A person approved once, when the company granted the mandate: it needs the `prepaid_consumption` action, and when the
        mandate lists `allowed_domains`, the resource's `seller_domain` must be among them. Inside the mandate the
        consumption is `executed` at once, with no approval per action.

        The response is the `AgentAction` (`action: prepaid_consumption`) with `consumption`: the resource, the units, and the
        balance it left (`balance_after`). The same `idempotency_key` with the same body returns the same action (`200`) and
        never debits twice.

        - `422 AGENT_PAYMENT_OUTSIDE_MANDATE`: refused and recorded, with the reasons: from the mandate (`action_not_allowed`,
          `domain_not_allowed`, `mandate_expired`, ...) or from the resource (`prepaid_balance_insufficient`,
          `prepaid_resource_expired`, `prepaid_resource_closed`, `prepaid_resource_other_agent`). Nothing was debited.
        - `404 PREPAID_RESOURCE_NOT_FOUND`: no such resource under this agent's grant.
        - `409 AGENT_PAYMENT_CONFLICT`: this `idempotency_key` was used under this grant for a different request.
          Only that: a budget or limit that a parallel request used up first is the recorded `422 AGENT_PAYMENT_OUTSIDE_MANDATE` refusal.
        - `409 AGENT_GRANT_NOT_ACTIVE` / `409 AGENT_NOT_ACTIVE`: checked before anything is recorded, and again when it is written.
        - `409 AGENT_MANDATE_CHANGED`: the mandate changed while this consumption was being recorded.
        - `403 NOT_THE_GRANTED_PLATFORM`: only the platform this agent belongs to can record its consumptions.
        - `403 AGENT_PLATFORM_SUSPENDED`: your platform is suspended.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentConsumptionRequest"
            examples:
              tokens:
                summary: The agent used 12,000 tokens
                value:
                  idempotency_key: run-2026-10-13-0001
                  resource_id: ozav_ppr_01JA0000000000000000000000
                  units: "12000"
      responses:
        "200":
          description: The consumption recorded earlier under this key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "201":
          description: The consumption was recorded and the resource debited.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "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/agents/{agent_id}/consumptions \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "run-2026-10-13-0001",
                "resource_id": "ozav_ppr_01JA0000000000000000000000",
                "units": "12000"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/consumptions", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "run-2026-10-13-0001",
                "resource_id": "ozav_ppr_01JA0000000000000000000000",
                "units": "12000"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/consumptions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "run-2026-10-13-0001",
                    "resource_id": "ozav_ppr_01JA0000000000000000000000",
                    "units": "12000",
                },
            )
            data = response.json()
  /agents/{agent_id}/payees:
    post:
      operationId: registerAgentPayee
      tags:
        - Agents
      summary: Register a beneficiary the agent will pay, inside the mandate
      description: |
        The agent registers a payout destination (a bank account or Pix key) in the company's name, so a later payout can name it.
        A person approved once, when the company granted the mandate: inside `payee_rules` the destination is created with no
        approval per registration, and outside them the request is refused and recorded. Registering moves no money and has no
        amount, so no spending limit applies. The body is the one of `POST /payout-destinations` plus your `idempotency_key`;
        the destination always belongs to the account holder of the grant (there is no `customer_id` to send), and every rule of
        `POST /payout-destinations` applies in full.

        The mandate needs the `payee_registration` action and `payee_rules`; without rules nothing can be registered.
        `payee_rules.holder` decides whose account: `self` accepts only the account holder's own account (the document declared in
        `account_holder.tax_id` must be the one on file for the account holder: a Brazilian holder's `cpf` or `cnpj`, or for a holder
        outside Brazil the tax id or registration on the customer, with the same `country`, the same `type` and the same value; whether
        the account itself belongs to that holder is checked by the same ownership rules as `POST /payout-destinations`),
        `third_party_identified` also accepts another holder, as long as the request declares a tax id. The rules can also list
        `currencies`, `rails` and `tax_ids` (compared without punctuation) and cap how many destinations the agent registers per
        UTC day with `max_per_day`.

        The response is the `AgentAction` (`action: payee_registration`). When it is `executed`, `payee.id` is the new
        destination (`ozav_pd_...`). A mandate whose `allowed_payees` holds `{ type: agent_created }` lets a payout name any
        destination this agent registered, and no other.

        The same `idempotency_key` with the same body returns the same destination (`200`) and never creates a second one.
        In the sandbox nothing is created: the destination is simulated (`receipt.simulated` is `true`) and the checks that
        `POST /payout-destinations` runs on the account details do not run.

        - `503 AGENT_PAYEE_OUTCOME_UNKNOWN`: OZAV could not confirm whether the destination was created. Repeat with the same
          `idempotency_key`, never a new one; the repeat returns the original destination.
        - `422 AGENT_PAYEE_REFUSED`: refused before it was created. `details.upstream.code` names a problem you can fix (a
          field, the rail or the currency); any other reason reads `REFUSED`.
        - `422 AGENT_PAYMENT_OUTSIDE_MANDATE`: outside the mandate, with the reasons (`action_not_allowed`, `payee_rules_missing`,
          `payee_currency_not_allowed`, `payee_rail_not_allowed`, `payee_holder_not_allowed`, `payee_tax_id_not_allowed`,
          `above_payee_daily`, `payee_unreadable`). The refusal is recorded. It is also returned, without reasons, when the grant, the
          agent or an agent above it is revoked or suspended before the destination is created; nothing was created.
        - `409 AGENT_MANDATE_CHANGED`: the mandate changed while this registration was being processed.
        - `409 AGENT_PAYMENT_CONFLICT`: this `idempotency_key` was used under this grant for a different registration, or by a
          different agent.
          Only that: a budget or limit that a parallel request used up first is the recorded `422 AGENT_PAYMENT_OUTSIDE_MANDATE` refusal.
        - `409 AGENT_PAYMENT_FAILED`: a repeat of a registration that failed. Use a new key to try again.
        - `409 AGENT_GRANT_NOT_ACTIVE` / `409 AGENT_NOT_ACTIVE`: checked before anything is recorded.
        - `403 NOT_THE_GRANTED_PLATFORM`: only the platform this agent belongs to can register payees for it.
        - `403 AGENT_PLATFORM_SUSPENDED`: your platform is suspended.
        - `503 AGENT_PAYEES_NOT_ENABLED`: agent actions are not enabled yet in production; nothing was created. The sandbox
          does not need it.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentPayeeRequest"
            examples:
              pix:
                summary: Register a supplier's Pix key
                value:
                  idempotency_key: supplier-acme-001
                  currency: BRL
                  rail: pix
                  account_holder:
                    name: Acme Fornecedora Ltda
                    type: business
                    tax_id:
                      country: BR
                      type: cnpj
                      value: "11222333000181"
                  details:
                    pix_key: financeiro@acme.example
      responses:
        "200":
          description: The recorded registration (a retry of one already created).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "201":
          description: The destination was created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "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/agents/{agent_id}/payees \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "supplier-acme-001",
                "currency": "BRL",
                "rail": "pix",
                "account_holder": {
                  "name": "Acme Fornecedora Ltda",
                  "type": "business",
                  "tax_id": {
                    "country": "BR",
                    "type": "cnpj",
                    "value": "11222333000181"
                  }
                },
                "details": {
                  "pix_key": "financeiro@acme.example"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/payees", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "supplier-acme-001",
                "currency": "BRL",
                "rail": "pix",
                "account_holder": {
                  "name": "Acme Fornecedora Ltda",
                  "type": "business",
                  "tax_id": {
                    "country": "BR",
                    "type": "cnpj",
                    "value": "11222333000181"
                  }
                },
                "details": {
                  "pix_key": "financeiro@acme.example"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/payees",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "supplier-acme-001",
                    "currency": "BRL",
                    "rail": "pix",
                    "account_holder": {
                        "name": "Acme Fornecedora Ltda",
                        "type": "business",
                        "tax_id": {
                            "country": "BR",
                            "type": "cnpj",
                            "value": "11222333000181",
                        },
                    },
                    "details": {
                        "pix_key": "financeiro@acme.example",
                    },
                },
            )
            data = response.json()
  /agents/{agent_id}/accounts:
    post:
      operationId: openAgentAccount
      tags:
        - Agents
      summary: Open a named fiat account for the account holder, inside the mandate
      description: |
        The agent opens a named fiat account (the same account `POST /accounts` opens) for the account holder of its grant.
        A person approved once, when the company granted the mandate: inside `account_rules` the account is requested with no
        approval per action, and outside them the request is refused and recorded. Opening moves no money and has no amount,
        so no spending limit applies. The body is the one of `POST /accounts` plus your `idempotency_key`; the account always
        belongs to the account holder of the grant (there is no `customer_id` to send), and every rule of `POST /accounts`
        applies in full: the holder's verification, the corridors OZAV serves and the account institution's own checks. The
        agent cannot skip any of them.

        The mandate needs the `account_opening` action and `account_rules`; without rules nothing can be opened. The rules can
        list `currencies`, `countries` and `rails`, and must cap how many accounts the agents of the grant open with
        `max_accounts` (over the grant's life). When the rules list countries or rails, the request must name them.

        The response is the `AgentAction` (`action: account_opening`) with `named_account`, the account as `GET /accounts/{account_id}`
        shows it (it may still be `pending` while the institution reviews it). Only accounts the agent itself opened are its
        own: an account counts only when it comes back carrying this action's id in its metadata (`ozav_agent_action`), which
        `POST /accounts` writes when it creates the account. If the account holder already has an account in that corridor
        (or one appears from elsewhere at the same time), the answer is `409 AGENT_ACCOUNT_ALREADY_EXISTS` and that account
        stays out of the agent's reach.

        The same `idempotency_key` with the same body returns the same account (`200`) and never opens a second one. In the
        sandbox nothing is opened: the account is simulated (`receipt.simulated` is `true`). An opening whose outcome OZAV could
        not confirm is settled by OZAV within minutes when the account it created exists (`executed`, `receipt.reconciled`), or
        `expired` after 24 hours when none appeared, which gives its `max_accounts` slot back.

        - `422 AGENT_ACCOUNT_HOLDER_NOT_ELIGIBLE`: the account holder cannot have this account right now. `details.reason` is
          `holder_verification_pending` (the holder still has to finish verification or activation, or the institution is
          reviewing it) or `holder_not_eligible` (refused, not enabled or deactivated). Nothing in the request can fix it.
        - `422 AGENT_ACCOUNT_REFUSED`: refused before it was opened. `details.upstream.code` names a problem you can fix (the
          rail or the corridor); any other reason reads `REFUSED`.
        - `409 AGENT_ACCOUNT_ALREADY_EXISTS`: the account holder already had this account.
        - `503 AGENT_ACCOUNT_OUTCOME_UNKNOWN`: OZAV could not confirm whether the account was requested (or a provisioning is
          still under way). Repeat with the same `idempotency_key`, never a new one.
        - `422 AGENT_PAYMENT_OUTSIDE_MANDATE`: outside the mandate, with the reasons (`action_not_allowed`,
          `account_rules_missing`, `account_currency_not_allowed`, `account_country_not_allowed`, `account_rail_not_allowed`,
          `above_max_accounts`, `accounts_unreadable`). The refusal is recorded.
        - `409 AGENT_MANDATE_CHANGED`, `409 AGENT_PAYMENT_CONFLICT`, `409 AGENT_PAYMENT_FAILED`, `409 AGENT_GRANT_NOT_ACTIVE`,
          `409 AGENT_NOT_ACTIVE`, `403 NOT_THE_GRANTED_PLATFORM`, `403 AGENT_PLATFORM_SUSPENDED`: as on `POST /agents/{agent_id}/payees`.
        - `503 AGENT_ACCOUNTS_NOT_ENABLED`: agent accounts are not enabled yet; nothing was requested or recorded.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentAccountOpeningRequest"
            examples:
              brl:
                summary: Open a BRL account with Pix for the company
                value:
                  idempotency_key: brl-account-001
                  currency: BRL
                  country: BR
                  rail: pix
      responses:
        "200":
          description: The recorded opening (a retry of one already opened).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAccountActionWithAccount"
        "201":
          description: The account was requested and exists.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAccountActionWithAccount"
        "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/agents/{agent_id}/accounts \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "brl-account-001",
                "currency": "BRL",
                "country": "BR",
                "rail": "pix"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/accounts", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "brl-account-001",
                "currency": "BRL",
                "country": "BR",
                "rail": "pix"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/accounts",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "brl-account-001",
                    "currency": "BRL",
                    "country": "BR",
                    "rail": "pix",
                },
            )
            data = response.json()
    get:
      operationId: listAgentAccounts
      tags:
        - Agents
      summary: List the accounts the agent opened
      description: |
        The accounts this agent opened, newest first (up to 50; `has_more` says when there are more), with `named_account` as
        `GET /accounts/{account_id}` shows it now (`null` when it could not be read right now). Refused or failed openings are
        not accounts: read them in `GET /agents/{agent_id}/actions`. The account holder's organization can always read it; the
        agent's platform only while it can still act for the holder.

        - `409 AGENT_GRANT_NOT_ACTIVE` / `409 AGENT_NOT_ACTIVE` / `403 AGENT_PLATFORM_SUSPENDED`: for the platform, once the
          grant, the agent (or one above it) or the platform is no longer active.
        - `503 AGENT_ACCOUNTS_NOT_ENABLED`: agent accounts are not enabled yet.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      responses:
        "200":
          description: The agent's account openings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAccountList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/accounts \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/accounts", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agents/{agent_id}/accounts",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/accounts/{account_id}/movements:
    get:
      operationId: listAgentAccountMovements
      tags:
        - Agents
      summary: Read the statement of an account the agent opened
      description: |
        The statement of an account this agent opened, exactly as `GET /accounts/{account_id}/movements` returns it, read live.
        Any other account id (the account holder's own accounts included) answers `404 AGENT_ACCOUNT_NOT_FOUND`. In the
        sandbox the statement is empty and `simulated` is `true`. The account holder's organization can always read it; the
        agent's platform only while it can still act for the holder.

        - `409 AGENT_GRANT_NOT_ACTIVE` / `409 AGENT_NOT_ACTIVE` / `403 AGENT_PLATFORM_SUSPENDED`: for the platform, once the
          grant, the agent (or one above it) or the platform is no longer active.
        - `501 ACCOUNT_MOVEMENTS_NOT_AVAILABLE` / `503 ACCOUNT_MOVEMENTS_UNAVAILABLE`: as on `GET /accounts/{account_id}/movements`.
        - `503 AGENT_ACCOUNTS_NOT_ENABLED`: agent accounts are not enabled yet.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
        - name: account_id
          in: path
          required: true
          description: The account id (`ozav_acc_...`) of an account this agent opened.
          schema:
            type: string
      responses:
        "200":
          description: The account's credits, newest first.
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - has_more
                  - next_cursor
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/AccountMovement"
                  has_more:
                    type: boolean
                  next_cursor:
                    type:
                      - string
                      - "null"
                  simulated:
                    type: boolean
                    description: "`true` in the sandbox: nothing was read from an institution."
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "501":
          $ref: "#/components/responses/NotImplementedYet"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/accounts/{account_id}/movements \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/accounts/{account_id}/movements", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agents/{agent_id}/accounts/{account_id}/movements",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/accounts/{account_id}/payouts:
    post:
      operationId: createAgentAccountPayout
      tags:
        - Agents
      summary: Pay out from an account the agent opened, inside the mandate
      description: |
        The agent pays from a named account it opened to a payout destination of the same account holder, in the account's own
        currency (the same payout `POST /accounts/{account_id}/payouts` makes). Only accounts this agent opened can be used; any
        other id answers `404 AGENT_ACCOUNT_NOT_FOUND`. The money leaves from that account or not at all: if it is blocked,
        inactive or cannot send, the payout is refused and never paid from another account of the holder.

        The mandate decides it exactly like a `fiat_payout`, and needs the `account_payout` action: `allowed_corridors` (the
        account pays in its own currency, so the corridor is `BRL>BRL`, `USD>USD`, ...), `allowed_payees` (`agent_created`
        included), `per_payment_limit_usd`, `daily_limit_usd`, `total_budget_usd`, `recurring_budget` and
        `human_approval_above_usd`. It is spending: it counts in the same budget as every payout of the grant. A USD amount is
        measured as it is; another currency is measured by what the same amount costs in USDC on a live quote (the quote is
        never executed; in the sandbox, at the fixed sandbox rates). At or above the approval threshold the answer is `202`
        and a person at the company approves; then repeat the request with the same `idempotency_key`.

        The response is the `AgentAction` (`action: account_payout`) with `account_payout`, the order OZAV placed. The same
        `idempotency_key` with the same body returns the same order and never pays twice. In the sandbox nothing is sent.

        - `422 AGENT_ACCOUNT_PAYOUT_REFUSED`: refused before anything was sent, or rejected by the institution with nothing sent;
          the budget is released. `details.upstream.code` names a problem you can fix (amount, balance, destination); any
          other reason reads `REFUSED`.
        - `422 AGENT_ACCOUNT_PAYOUT_NOT_MEASURABLE`: the amount could not be measured in US dollars; nothing was recorded.
        - `503 AGENT_ACCOUNT_PAYOUT_OUTCOME_UNKNOWN`: OZAV could not confirm whether the payout was sent. Repeat with the same
          `idempotency_key`, never a new one. A repeat within a minute of the first attempt sends nothing (that attempt may still
          be running). After the grant is revoked or the mandate changes, a payout that was already dispatched is never sent
          again; it stays open until OZAV settles it.
        - `404 AGENT_ACCOUNT_NOT_FOUND`: this agent did not open that account.
        - `422 AGENT_PAYMENT_OUTSIDE_MANDATE`, `409 AGENT_MANDATE_CHANGED`, `409 AGENT_PAYMENT_CONFLICT`, `409 AGENT_PAYMENT_FAILED`,
          `409 AGENT_PAYMENT_IN_PROGRESS`, `409 AGENT_PAYMENT_EXPIRED`, `409 AGENT_GRANT_NOT_ACTIVE`, `409 AGENT_NOT_ACTIVE`,
          `403 NOT_THE_GRANTED_PLATFORM`, `403 AGENT_PLATFORM_SUSPENDED`: as on `POST /agents/{agent_id}/payouts`.
        - `422 AGENT_SMART_ACCOUNT_SIGNING_UNAVAILABLE`: the agent pays from its own Safe; nothing was recorded.
        - `503 AGENT_ACCOUNTS_NOT_ENABLED`: agent accounts are not enabled yet; nothing was recorded.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
        - name: account_id
          in: path
          required: true
          description: The account id (`ozav_acc_...`) of an account this agent opened.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentAccountPayoutRequest"
            examples:
              pix:
                summary: Pay R$ 250.00 to the company's own Pix destination
                value:
                  idempotency_key: payout-0001
                  amount: "250.00"
                  payout_destination_id: ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1
      responses:
        "200":
          description: The recorded payout (a retry of one already placed).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAccountPayoutAction"
        "201":
          description: The order was placed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAccountPayoutAction"
        "202":
          description: Above the approval threshold; a person at the company approves.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAccountPayoutAction"
        "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/agents/{agent_id}/accounts/{account_id}/payouts \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "payout-0001",
                "amount": "250.00",
                "payout_destination_id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/accounts/{account_id}/payouts", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "payout-0001",
                "amount": "250.00",
                "payout_destination_id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/accounts/{account_id}/payouts",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "payout-0001",
                    "amount": "250.00",
                    "payout_destination_id": "ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                },
            )
            data = response.json()
  /agents/{agent_id}/fundings:
    post:
      operationId: requestAgentFunding
      tags:
        - Agents
      summary: Ask for a charge that becomes stablecoin in the company's wallet
      description: |
        The agent asks for a charge that turns into stablecoin in the company's managed wallet: the
        on-ramp. A charge is either reais by Pix (`rail: pix`, the Pix copy-and-paste code in
        `funding.payment_instructions.br_code`) or US dollars by wire (`rail: wire`, `fedwire` or `swift`: the bank
        coordinates and the `reference` to write in the wire's memo, in `funding.payment_instructions`). OZAV quotes
        it, checks the grant's mandate and the agent's own, and returns the instructions for you to hand to whoever pays.
        In the sandbox the charge is simulated (fixed rates of R$ 5.00 and US$ 1.00 per US dollar of stablecoin, a Pix
        code that starts with `SIMULATED-PIX-` or bank fields marked `SIMULATED`, which no bank accepts;
        `funding.simulated` is `true`): finish it with `POST /agent-actions/{action_id}/simulate`.

        **The agent does not move money here, and the payer is the company.** A Pix charge is locked to the
        company's own CPF or CNPJ, so a payment from anyone else does not settle. A wire is credited only when the
        bank statement shows the account holder as the sender; a wire from anyone else, or one whose statement does
        not say who sent it, is held for review and never reaches the wallet. An account that has not turned the
        payer lock on gets `422 AGENT_FUNDING_PAYER_LOCK_REQUIRED` and nothing is quoted. The money only
        moves when the company pays, and the stablecoin lands in the account holder's own wallet
        (`target.wallet_id` must be theirs, otherwise `404 WALLET_NOT_FOUND`), never in a wallet of the agent
        or of another customer.

        **A funding is not spending.** It does not count against `per_payment_limit_usd`, `daily_limit_usd`,
        `total_budget_usd`, `recurring_budget` or `human_approval_above_usd`, and it never waits for a person. The mandate
        needs the `fiat_funding` action, may list the corridor (`BRL>USDC`, `USD>USDC`), asset and network, and
        limits it with `funding_limits` (`per_request_usd`, `daily_usd`). The dollar amount checked is what
        the quote delivers in stablecoin. A charge that expires unpaid releases the day's limit.

        The response is the `AgentAction` (`action: fiat_funding`) with the charge in `funding`. Its
        `status` walks `awaiting_payment` (the charge exists), then `settled` once the company paid and
        the stablecoin arrived, or `returned` if the charge expired or was cancelled unpaid. Each change
        arrives as the webhook `agent_action.updated`. A charge rejected at creation is recorded
        `failed` (the response is still `201`, with `status: failed`). A transfer that fails or needs review
        later stays `awaiting_payment` until OZAV reviews it. The charge is created only if the grant, the mandate and
        the agent's lineage still hold at that moment (checked in one database transaction that also marks the
        charge as dispatched); a repeat after dispatch completes it even if the grant was revoked afterwards, because the
        charge may already exist. The Pix code (`br_code`, `qr_code_image`) is only
        returned while the charge waits for payment; once it is settled, returned or expired the same object
        carries `payment_instructions: null`. Without `funding_limits` a funding has no amount limit: set
        them.

        The same `idempotency_key` with the same body returns the same charge (`200`) and never creates a
        second one.

        - `503 AGENT_FUNDING_OUTCOME_UNKNOWN`: OZAV could not confirm whether the charge was created. Repeat
          with the same `idempotency_key`, never a new one; the repeat returns the original charge.
        - `404 WALLET_NOT_FOUND`: `target.wallet_id` does not exist for this account holder (a wallet of
          another customer of the company reads the same). Nothing was quoted.
        - `422 AGENT_FUNDING_PAYER_LOCK_REQUIRED`: the Pix payer lock is not turned on for this account.
        - `403 NOT_THE_GRANTED_PLATFORM`: only the platform this agent belongs to can request fundings for it.
        - `409 AGENT_PAYMENT_EXPIRED`: a repeat of a funding that expired before it was used. Use a new key.
        - `422 AGENT_FUNDING_REFUSED`: refused before any charge existed. `details.upstream.code` names a
          problem you can fix (amount, wallet, network); any other reason reads `REFUSED`.
        - `422 AGENT_FUNDING_QUOTE_UNAVAILABLE`: no quote for this amount and asset right now. A quote whose
          origin is not the reais you asked for, or whose asset is not the one you asked for, is never used.
        - `422 AGENT_PAYMENT_OUTSIDE_MANDATE`: outside the mandate, with the reasons (`action_not_allowed`,
          corridor, asset, network, `above_funding_per_request`, `above_funding_daily`, `funding_unreadable`). The refusal is
          recorded. It is also returned, without reasons, when the grant, the agent or an agent above it is
          revoked or suspended between the quote and the charge; nothing was created.
        - `409 AGENT_MANDATE_CHANGED`: the mandate changed while this funding was being processed.
        - `409 AGENT_PAYMENT_CONFLICT`: this `idempotency_key` was used under this grant for a different
          funding (another amount, wallet or asset), or by a different agent.
          Only that: a budget or limit that a parallel request used up first is the recorded `422 AGENT_PAYMENT_OUTSIDE_MANDATE` refusal.
        - `409 AGENT_PAYMENT_FAILED`: a repeat of a funding that failed, or whose charge expired unpaid
          (`status: returned`). Use a new key to try again.
        - `409 AGENT_GRANT_NOT_ACTIVE` / `409 AGENT_NOT_ACTIVE`: checked before quoting.
        - `403 AGENT_PLATFORM_SUSPENDED`: your platform is suspended.
        - `503 AGENT_FUNDINGS_NOT_ENABLED`: agent fundings are not enabled yet in production; nothing was quoted. The
          sandbox does not need it.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentFundingRequest"
            examples:
              pix:
                summary: Ask for a R$ 500,00 Pix charge that becomes USDC on Polygon
                value:
                  idempotency_key: topup-2026-1012
                  amount:
                    amount: "500.00"
                    currency: BRL
                  rail: pix
                  target:
                    wallet_id: ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V2
                    currency: USDC
                    chain: polygon
      responses:
        "200":
          description: The recorded funding (a retry of one already created).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "201":
          description: The Pix charge was created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "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/agents/{agent_id}/fundings \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "topup-2026-1012",
                "amount": {
                  "amount": "500.00",
                  "currency": "BRL"
                },
                "rail": "pix",
                "target": {
                  "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                  "currency": "USDC",
                  "chain": "polygon"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/fundings", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "topup-2026-1012",
                "amount": {
                  "amount": "500.00",
                  "currency": "BRL"
                },
                "rail": "pix",
                "target": {
                  "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                  "currency": "USDC",
                  "chain": "polygon"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/fundings",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "topup-2026-1012",
                    "amount": {
                        "amount": "500.00",
                        "currency": "BRL",
                    },
                    "rail": "pix",
                    "target": {
                        "wallet_id": "ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V2",
                        "currency": "USDC",
                        "chain": "polygon",
                    },
                },
            )
            data = response.json()
  /agents/{agent_id}/payments:
    post:
      operationId: requestAgentPayment
      tags:
        - Agents
      summary: Pay a resource's payment challenge
      description: |
        Send the payment challenge your agent received (x402: the `PAYMENT-REQUIRED`
        header or, for x402 v1, the 402 body; MPP: the `WWW-Authenticate` header). OZAV picks an option
        it can pay (native USDC on Polygon, Base or Ethereum), checks the grant's mandate and the
        agent's own, and records the request.

        - Within the mandate: OZAV reserves the amount, signs the payment authorization from the
          company's wallet and returns it in `payment`: the header name and value to send back to
          the resource (x402: `PAYMENT-SIGNATURE`, or `X-PAYMENT` for v1; MPP: `Authorization`). The
          authorization is valid for five minutes and is delivered once: a retry with the same
          `idempotency_key` returns `409 AGENT_PAYMENT_ALREADY_SIGNED`. In production, while signing
          is not enabled the response is `503 AGENT_PAYMENT_SIGNING_UNAVAILABLE`, nothing is signed and
          the budget is released. On Cloudflare pay-per-crawl (x402 `batch-settlement`, network
          `cloudflare:402`, in whole US cents), nothing moves on-chain: OZAV debits the company's balance
          once, signs the request with its agent identity key, and `payment.headers` carries the four
          headers to send, valid for 60 seconds; the mandate must allow network `cloudflare:402` and
          asset `USD`, and `503 CLOUDFLARE_PAYMENTS_UNAVAILABLE` means it is not enabled yet (nothing was
          charged). The same applies to Cloudflare's current form, `protocol: pay_per_crawl`: send the page
          URL as `resource_url` and the `crawler-price` header of its 402 as `crawler_price` (`USD 0.01`, up
          to 2 decimals, in whole US cents); `payment.header_name` is `crawler-exact-price` (its value equals
          your `crawler_price`), the four headers go in `payment.headers`, and the site then answers 200 with
          `crawler-charged`. In the sandbox, `payment.simulated` is `true`, the signature is all
          zeros, and the company needs no wallet: OZAV signs from a stable sandbox address for it, so
          you can integrate the whole flow before going live.
        - Above the approval threshold: `202`, waiting for a person at the company to approve it in
          the OZAV portal (with their authenticator code). The decision arrives as the webhook
          `agent_action.updated` (`authorized` or `refused`). Once approved, collect the payment with
          `POST /agent-actions/{action_id}/payment`: it signs through the same path and returns `payment`
          (the header to send), delivered once, with no repeat of this request. Repeating this request with the
          same `idempotency_key` and the same challenge still works and never signs twice; a challenge with a
          different amount, network or payee is refused with `409 CHALLENGE_DIFFERS_FROM_APPROVED`.
          Cloudflare pay-per-crawl is the exception: collect is not available for it, repeat the request.
          A request nobody decides within 24 hours expires,
          and so does an approved request nobody collects or repeats within 24 hours
          (`409 AGENT_PAYMENT_EXPIRED`). If the mandate changed after the approval: `409
          AGENT_MANDATE_CHANGED`; if the grant, the agent or the mandate is no longer in force: `422
          AGENT_PAYMENT_OUTSIDE_MANDATE`. Nothing is signed in either case.
        - Outside the mandate: `422 AGENT_PAYMENT_OUTSIDE_MANDATE`, with the reasons (among them
          `above_recurring_budget` when the amount would pass the mandate's recurring budget for the current window, and
          `above_agent_allocation` when the agent has an allocation and the amount does not fit in what is left of it). The refusal is
          recorded. An agent whose parent agent is not active is outside the mandate too.
        - `409 AGENT_GRANT_NOT_ACTIVE` / `409 AGENT_NOT_ACTIVE`: the grant or the agent stopped being active while the request was written.
          A budget a parallel request used up first is the recorded `422` refusal above, never `409 AGENT_PAYMENT_CONFLICT`.

        Refusals after the mandate, each with nothing signed and the budget released:
        `422 AGENT_PAYMENT_NOT_PERMITTED` (this payment cannot be made right now: the reason may be the
        company's account or the payee, and it is never given; the message is fixed),
        `422 WALLET_QUORUM_REQUIRED` (the company's wallet policy wants several keys for this amount),
        `422 INSUFFICIENT_ONCHAIN_BALANCE` (the wallet's balance, minus authorizations it already signed
        and nobody used yet, does not cover it), `503 SCREENING_UNAVAILABLE` (the payee could not be
        checked; retry with a new key), `422 COSIGNER_REFUSED`, `403 CLIENT_COSIGNER_DENIED` and
        `503 CLIENT_COSIGNER_UNAVAILABLE` (the company's own co-signer refused or did not answer; this one
        applies in the sandbox too), and `409 CHALLENGE_ALREADY_PAID` (the same MPP challenge was already
        signed under another key; a challenge whose earlier attempt failed or expired unused can be paid
        again). These messages are fixed: they never carry a compliance, screening or co-signer reason.

        The paying company's wallet limits apply too, after the mandate and before anything is signed:
        `422 AMOUNT_ABOVE_PER_TRANSACTION_LIMIT`, `422 WALLET_DAILY_LIMIT_EXCEEDED` (the daily window
        counts the wallet's agent payments and its other sends together) and
        `422 DESTINATION_NOT_ALLOWLISTED` (a payee outside the wallet's approved destinations, above the
        wallet's automatic amount). A payment a person approved in the portal is not held to the
        approved-destinations rule; the per-payment and daily limits still apply. `503
        WALLET_LIMITS_UNAVAILABLE` means the limits could not be read; nothing was signed. These
        refusals do not reveal the company's limits or what it has spent; the request ends `failed`.

        The same `idempotency_key` returns the same recorded request and never counts twice. It belongs
        to the grant: another agent using it gets `409 AGENT_PAYMENT_CONFLICT`. A repeat while the first
        request is still running gets `409 AGENT_PAYMENT_IN_PROGRESS`; a repeat of a request that failed
        or expired gets `409 AGENT_PAYMENT_FAILED` or `409 AGENT_PAYMENT_EXPIRED` (a returned payout also gets `AGENT_PAYMENT_FAILED`) - use a new key.
        `503 AGENT_PAYMENT_OUTCOME_UNKNOWN`: OZAV could not confirm the signature; the reservation is
        held until the authorization expires. Do not retry under a new key.
        OZAV charges the account holder a spread on every payment (`spread` on the request, 5 bps by default)
        on top of the amount paid; the balance must cover both (`422 LEDGER_INSUFFICIENT` otherwise).
        `503 AGENT_SPREAD_UNAVAILABLE`: the spread could not be determined right now; nothing was signed or
        held. Retry shortly with a new key.
        `403 AGENT_PLATFORM_SUSPENDED`: your platform is suspended.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AgentPaymentRequest"
            examples:
              x402:
                summary: Pay an x402 challenge
                value:
                  protocol: x402
                  idempotency_key: task-8812-step-3
                  payment_required_header: eyJ4NDAyVmVyc2lvbiI6MiwiYWNjZXB0cyI6W119
      responses:
        "200":
          description: The recorded payment request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "202":
          description: Recorded and waiting for a person at the company to approve.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "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/agents/{agent_id}/payments \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "protocol": "x402",
                "idempotency_key": "task-8812-step-3",
                "payment_required_header": "eyJ4NDAyVmVyc2lvbiI6MiwiYWNjZXB0cyI6W119"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/payments", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "protocol": "x402",
                "idempotency_key": "task-8812-step-3",
                "payment_required_header": "eyJ4NDAyVmVyc2lvbiI6MiwiYWNjZXB0cyI6W119"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agents/{agent_id}/payments",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "protocol": "x402",
                    "idempotency_key": "task-8812-step-3",
                    "payment_required_header": "eyJ4NDAyVmVyc2lvbiI6MiwiYWNjZXB0cyI6W119",
                },
            )
            data = response.json()
  /agent-actions/{action_id}/exceptions:
    post:
      operationId: openAgentActionException
      tags:
        - Agents
      summary: Open an exception case on an agent action
      description: |
        Open a case when an agent payment went wrong: the resource was not delivered
        (`not_delivered`), the amount is wrong (`wrong_amount`), it was paid twice
        (`duplicate_payment`), or the company did not intend it (`not_authorized`). The platform or the
        company can open it, only where money moved or may have moved (a signed, settled or executed
        action; otherwise `409 AGENT_ACTION_NOT_DISPUTABLE`). The body takes a code, never free text.

        The case gets a due date (5 days) and goes to OZAV's queue. When it is opened, OZAV records the
        `evidence` from the action as it was at that moment (mandate version and its hash, decision and
        reasons, receipt, amounts, timestamps) with `evidence_sha256`; nobody can change it afterwards.

        `status` walks forward only: `open`, then `under_review` once a person at OZAV takes it, then
        `resolved_refunded` or `resolved_denied`. A resolved case never reopens. A resolution carries the
        `remedy`: the channel the money takes back (`pix_med` for a Pix payout or funding,
        `manual_transfer`, or `onchain_payee_block` for a stablecoin payment, which cannot be reversed on
        chain, so the payee is blocked in the mandate instead) and OZAV's protocol `reference`. A denial
        carries `channel: none`. Each change arrives as the webhook `agent_action.updated`, with the case in
        `exception`.

        One case per action: opening again returns the same case.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:write
      x-ozav-availability: beta
      parameters:
        - name: action_id
          in: path
          required: true
          description: The agent action id (`ozav_aga_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - reason_code
              properties:
                reason_code:
                  type: string
                  enum:
                    - not_delivered
                    - wrong_amount
                    - duplicate_payment
                    - not_authorized
                    - other
            examples:
              not_delivered:
                summary: Paid, and the resource never delivered
                value:
                  reason_code: not_delivered
      responses:
        "200":
          description: The case already open for this action.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentActionException"
        "201":
          description: The case was opened.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentActionException"
        "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"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agent-actions/{action_id}/exceptions \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "reason_code": "not_delivered"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-actions/{action_id}/exceptions", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "reason_code": "not_delivered"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agent-actions/{action_id}/exceptions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "reason_code": "not_delivered",
                },
            )
            data = response.json()
    get:
      operationId: getAgentActionException
      tags:
        - Agents
      summary: Read the exception case of an agent action
      description: |
        The case, its evidence and its current status (`open`, `under_review`, `resolved_refunded`,
        `resolved_denied`). `404 AGENT_EXCEPTION_NOT_FOUND` when the action has no case.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: action_id
          in: path
          required: true
          description: The agent action id (`ozav_aga_...`).
          schema:
            type: string
      responses:
        "200":
          description: The case.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentActionException"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agent-actions/{action_id}/exceptions \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-actions/{action_id}/exceptions", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agent-actions/{action_id}/exceptions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agent-actions/{action_id}/simulate:
    post:
      operationId: simulateAgentAction
      tags:
        - Agents
      summary: Finish a simulated payout, funding or stablecoin payment in the sandbox
      description: |
        In the sandbox, an agent's payout and funding run the whole decision path (mandate, limits, approval
        threshold, budget, idempotency, webhooks) with simulated money, and nothing ever pays a Pix charge or settles a
        transfer. Ask for the outcome you want to test:

        - `settled`: a funding becomes `settled` (the holder paid and the stablecoin arrived); a payout becomes `settled`
          (its transfer completed), as OZAV does for a live payout.
        - `returned`: a funding or a payout becomes `returned` (the charge expired unpaid, or the transfer was returned).
          That releases the budget or the funding limit exactly as it does in production.

        A stablecoin payment (x402 or MPP) stops at `signed` in the sandbox, because nothing is broadcast, and a signed payment holds
        the budget and the agent's allocation until it ends. Finish it the same way:

        - `settled`: a signed payment becomes `settled` (the resource used the authorization; the spend stays counted).
        - `expired` (or `returned`): a signed payment becomes `expired` (the authorization was never used); the budget and the
          allocation are released. `expired` only applies to a stablecoin payment.

        The same call twice answers the same result. The webhook `agent_action.updated` goes out once, from the call that
        changed the action. Only the platform the agent belongs to can call it, and only on a sandbox action.

        - `422 AGENT_SIMULATION_SANDBOX_ONLY`: a live action finishes with its real transfer.
        - `409 AGENT_ACTION_NOT_SIMULABLE`: only a payout or a funding whose simulated transfer was created
          (`executed` or `awaiting_payment`) or a `signed` stablecoin payment can be simulated, or the outcome contradicts what
          already happened (a settled funding cannot be returned, a settled payment cannot expire).
        - `403 NOT_THE_GRANTED_PLATFORM`: only the platform this agent belongs to can simulate its actions.
        - `404 AGENT_ACTION_NOT_FOUND`: the action does not exist for this platform.
        - `400`: `outcome` is missing or is not `settled` or `returned`.

        The `transfer_id` of a simulated payout or funding (`ozav_tr_sim_...`) is synthetic: no transfer with that id
        can be read from `/transfers`. Its receipt says `simulated: true`.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:write
      x-ozav-availability: beta
      parameters:
        - name: action_id
          in: path
          required: true
          description: The agent action id (`ozav_aga_...`).
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - outcome
              properties:
                outcome:
                  type: string
                  enum:
                    - settled
                    - returned
                    - expired
            examples:
              paid:
                summary: The holder paid the simulated Pix charge
                value:
                  outcome: settled
      responses:
        "200":
          description: The action after the simulation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "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/agent-actions/{action_id}/simulate \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "outcome": "settled"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-actions/{action_id}/simulate", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "outcome": "settled"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agent-actions/{action_id}/simulate",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "outcome": "settled",
                },
            )
            data = response.json()
  /agent-actions/{action_id}/payment:
    post:
      operationId: collectAgentPayment
      tags:
        - Agents
      summary: Collect an x402 or MPP payment a person approved
      description: |
        A payment above the approval threshold waits for a person at the company. When they approve it (the webhook
        `agent_action.updated` says `authorized`), call this to receive the signed payment without repeating the original
        request: the same claim, mandate, grant, lineage and 24 hour checks run, then the payment is signed and returned
        in `payment` (the header to send to the resource), once. The action is the idempotency scope: a second call never
        signs again.

        - `403 NOT_THE_GRANTED_PLATFORM`: only the platform this agent belongs to can collect its payments.
        - `409 AGENT_PAYMENT_NOT_APPROVED`: still waiting for a person, or the payment did not need one (repeat the request).
        - `409 AGENT_PAYMENT_ALREADY_SIGNED`: it was already collected; `409 AGENT_PAYMENT_IN_PROGRESS` while another call signs it.
        - `409 AGENT_MANDATE_CHANGED` and `422 AGENT_PAYMENT_OUTSIDE_MANDATE`: the mandate changed or the grant, agent or mandate is no
          longer in force. `409 AGENT_PAYMENT_EXPIRED`: 24 hours passed. Nothing is signed in any of them.
        - `409 AGENT_PAYMENT_REPEAT_REQUIRED`: Cloudflare pay-per-crawl, or no challenge was kept: repeat the original request.
        - `422 AGENT_SMART_ACCOUNT_SIGNING_UNAVAILABLE`: the agent pays from its own Safe. `503 AGENT_PAYMENT_SIGNING_UNAVAILABLE`:
          signing is not enabled in production yet; nothing was signed.

        - `409 AGENT_PAYMENT_FAILED`: it ended failed earlier. `422 NO_WALLET_ON_NETWORK`: the agent has no wallet on the network.
        - `503 AGENT_PAYMENT_OUTCOME_UNKNOWN`: the signature could not be confirmed and may exist; it is never delivered. Do not start a new
          payment for the same purpose: the reservation is held until the authorization expires, and the action then closes.

        The response is never cached. A lost response cannot be replayed: the signature is delivered once.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agent_payments:write
      x-ozav-availability: beta
      parameters:
        - name: action_id
          in: path
          required: true
          description: The action id (`ozav_aga_...`).
          schema:
            type: string
      responses:
        "200":
          description: The action, now `signed`, with the `payment` to send.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "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/agent-actions/{action_id}/payment \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-actions/{action_id}/payment", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/agent-actions/{action_id}/payment",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agent-actions/{action_id}:
    get:
      operationId: getAgentAction
      tags:
        - Agents
      summary: Retrieve an agent action
      description: |
        One recorded request: the mandate version it was checked against, the decision,
        the reasons, the status and, once settled, the receipt. Reading never changes the action. A payment
        a person approved reads as `authorized`: collect it with `POST /agent-actions/{action_id}/payment`.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: action_id
          in: path
          required: true
          description: The action id (`ozav_aga_...`).
          schema:
            type: string
      responses:
        "200":
          description: The action.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentAction"
        "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/agent-actions/{action_id} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agent-actions/{action_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agent-actions/{action_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /agents/{agent_id}/actions:
    get:
      operationId: listAgentActions
      tags:
        - Agents
      summary: List an agent's actions
      description: |
        The agent's evidence: every payment and payout it requested, newest first (by
        id, to the millisecond), each with the mandate version it was checked against, the decision,
        the reasons, the status and the receipt - the same object as `GET /agent-actions/{action_id}`.
        The platform and the company can read it. Filter by `status` with the public name
        (`refused`, `awaiting_approval`, `authorized`, `signed`, `settled`, `executed`, `awaiting_payment`,
        `returned`, `expired`, `failed`); `awaiting_payment` is a funding whose Pix charge waits for the payment; an unknown name answers `400 INVALID_STATUS_FILTER`. Follow `next_cursor` while
        `has_more` is true.
      x-ozav-entitlement: agents
      x-ozav-scopes:
        - agents:read
      x-ozav-availability: beta
      parameters:
        - name: agent_id
          in: path
          required: true
          description: The agent id (`ozav_agt_...`).
          schema:
            type: string
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - name: nature
          in: query
          required: false
          description: Only actions whose money has this nature. An unknown value answers `400 INVALID_NATURE_FILTER`.
          schema:
            type: string
            enum:
              - new_money
              - simulated
              - prepaid
        - name: status
          in: query
          required: false
          description: Only actions in this public status.
          schema:
            type: string
            enum:
              - refused
              - awaiting_approval
              - authorized
              - signed
              - settled
              - executed
              - awaiting_payment
              - returned
              - expired
              - failed
      responses:
        "200":
          description: One page of the agent's actions.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Page"
                  - type: object
                    properties:
                      object:
                        type: string
                        const: list
                        description: Always the literal `list`.
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/AgentAction"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/agents/{agent_id}/actions \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/agents/{agent_id}/actions", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/agents/{agent_id}/actions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /verifications:
    get:
      operationId: listVerifications
      tags:
        - Verifications
      summary: List verification sessions
      description: |
        Lists verification sessions, newest first, with the standard cursor
        pagination. Filter to one customer with the `customer_id` query
        parameter to recover an existing verification id after `POST
        /verifications` or after a duplicate `VERIFICATION_ALREADY_EXISTS`
        response. Returns a `Page` of `Verification`.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/verifications", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/verifications",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        requires end-user interaction). Check kinds are the canonical OZAV
        capability taxonomy.

        Verification records persist and can be listed with `GET
        /verifications`. Hosted `session_url` issuance is gated by your
        organization's capabilities and may be absent.

        Sandbox: a test key (`ozav_sk_test_`) never receives a `session_url` and
        never starts a real identity check - a sandbox verification has no real
        end-user to redirect. Create the verification, then drive it to its end
        state with `POST /verifications/{id}/simulate`.

        Reliance: for a customer created with `verification_mode: reliance`
        (your organization has the `kyc_reliance` product), your organization
        already identified the end user, so OZAV does not start a hosted
        identity, liveness or business verification session. Asking for any
        hosted check (`kyc_document`, `liveness`, `face_match`,
        `age_estimation`, `kyb`, `ubo`) answers `409
        VERIFICATION_NOT_REQUIRED_UNDER_RELIANCE`, in sandbox and live alike,
        and nothing is created or charged. Send the customer data and the
        identity document images with `POST /customers/{customer_id}/documents`
        instead; OZAV screens the customer and decides on its own. Checks OZAV
        runs itself (`aml_name_screening`, `economic_capacity`) can still be
        requested here.
      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"
        "409":
          description: |
            `VERIFICATION_NOT_REQUIRED_UNDER_RELIANCE` - the customer is under
            reliance and the request asked for a hosted check. No session was
            started and nothing was stored. `error.details.hosted_capabilities`
            lists the checks refused. Do not send the end user to a
            verification link: send the identity document images with `POST
            /customers/{customer_id}/documents`. Retrying the same body returns
            the same error.

            `VERIFICATION_ALREADY_EXISTS` - a verification already exists for
            this customer's hosted session; recover it with `GET
            /verifications?customer_id=`.
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
              examples:
                reliance:
                  summary: Customer under reliance - no hosted session
                  value:
                    error:
                      type: conflict_error
                      code: VERIFICATION_NOT_REQUIRED_UNDER_RELIANCE
                      message: "This customer is under reliance: your organization verified their identity, so OZAV does not start a hosted identity, liveness or business verification session for them, and none was started. Do not send the end user to a verification link. Instead, complete the customer profile and send the identity document images with POST /customers/{customer_id}/documents; OZAV screens the customer and decides on its own. Checks that OZAV runs itself (aml_name_screening, economic_capacity) can still be requested here without the hosted ones."
                      param: capabilities
                      details:
                        verification_mode: reliance
                        hosted_capabilities:
                          - kyc_document
                          - liveness
                      request_id: 5f0c2d1e-8a3b-4c7d-9e6f-1a2b3c4d5e6f
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/verifications \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "capabilities": [
                  "kyc_document",
                  "liveness",
                  "aml_name_screening"
                ],
                "redirect_url": "https://acme.example/kyc/return"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/verifications", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "capabilities": [
                  "kyc_document",
                  "liveness",
                  "aml_name_screening"
                ],
                "redirect_url": "https://acme.example/kyc/return"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/verifications",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "capabilities": [
                        "kyc_document",
                        "liveness",
                        "aml_name_screening",
                    ],
                    "redirect_url": "https://acme.example/kyc/return",
                },
            )
            data = 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
        `POST /verifications` to watch each requested check move through its
        `CheckOutcome` and the session `status` reach `completed`.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/verifications/ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/verifications/ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        or rejected and updates the linked customer's KYC status so downstream
        sandbox flows, including simulated wallet reads, BRL/Pix account reads,
        and card issuance, can be exercised without fake identity documents.
        Sandbox wallet and account records are projections only; they do not
        create an on-chain address or open a real bank account.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "outcome": "approved"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/verifications/ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1/simulate", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "outcome": "approved"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/verifications/ozav_vrf_01J8Z9K2M3N4P5Q6R7S8T9U0V1/simulate",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "outcome": "approved",
                },
            )
            data = response.json()
  /lists/entries:
    post:
      operationId: createListEntry
      tags:
        - Lists
      summary: Add an entry to your block or allow list
      description: |
        Adds a value to your organization's own block or allow list. Your lists apply to your
        screenings only: a block-list match escalates `suggested_decision` (`review`, or `decline`
        with `action: decline`) and always wins over an allow match; an allow-list match suppresses
        only contact signals - never a block-list, sanctions or PEP match. See `type` for which
        screenings each value type can match.

        Values are stored as a keyed hash plus a masked hint - never in full. The same value cannot be
        active twice: on the same list it returns 409 `LIST_ENTRY_EXISTS`, on the other list 409
        `LIST_ENTRY_CONFLICT`; both return the existing entry id in `error.details.existing_id`.

        Up to 10,000 active entries per organization and environment (a soft limit: concurrent
        creations right at the limit may pass it by a few entries).

        A `name` entry matches every subject with that name: a `name` block with `action: decline`
        declines homonyms too. Prefer `document` or `email` for decline, and `review` for names.

        Requires the `lists:write` scope (`lists:read` to list). API keys created before these scopes
        existed do not gain them: create a new key with the scopes you need.

        **Data protection.** Your organization is the controller of what it puts on its lists; OZAV
        processes it on your behalf. Deleting an entry stops it from matching at once and keeps a
        marked record as the audit trail of your due diligence; a physical purge is done by OZAV on
        your written request.
      x-ozav-entitlement: compliance
      x-ozav-scopes:
        - lists:write
      x-ozav-availability: live
      x-ozav-services: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ListEntryCreateRequest"
            examples:
              block_document:
                summary: Block a tax id (matches screenings by customer_id)
                value:
                  list: block
                  type: document
                  value: 12.345.678/0001-90
                  action: decline
      responses:
        "201":
          description: The entry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListEntry"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "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/lists/entries \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "list": "block",
                "type": "document",
                "value": "12.345.678/0001-90",
                "action": "decline"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/lists/entries", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "list": "block",
                "type": "document",
                "value": "12.345.678/0001-90",
                "action": "decline"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/lists/entries",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "list": "block",
                    "type": "document",
                    "value": "12.345.678/0001-90",
                    "action": "decline",
                },
            )
            data = response.json()
    get:
      operationId: listListEntries
      tags:
        - Lists
      summary: List your block and allow list entries
      description: |
        Lists your active entries, newest first, with the standard cursor pagination.
      x-ozav-entitlement: compliance
      x-ozav-scopes:
        - lists:read
      x-ozav-availability: live
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/StartingAfter"
        - name: list
          in: query
          description: Filter to `block` or `allow`.
          schema:
            type: string
            enum:
              - block
              - allow
      responses:
        "200":
          description: Paginated entries.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Page"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/ListEntry"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/lists/entries \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/lists/entries", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/lists/entries",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /lists/entries/{entry_id}:
    delete:
      operationId: deleteListEntry
      tags:
        - Lists
      summary: Remove an entry from your list
      description: |
        Removes the entry from your screenings from now on. A marked record is kept as your audit trail; a physical purge is done by OZAV on your written request.
      x-ozav-entitlement: compliance
      x-ozav-scopes:
        - lists:write
      x-ozav-availability: live
      x-ozav-services: true
      parameters:
        - name: entry_id
          in: path
          required: true
          description: The entry id (`ozav_lst_...`).
          schema:
            type: string
      responses:
        "200":
          description: The entry was removed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListEntryDeleted"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/lists/entries/{entry_id} \
              -X DELETE \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/lists/entries/{entry_id}", {
              method: "DELETE",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.delete(
                "https://developers.ozav.io/v2/lists/entries/{entry_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        dataset (UN, OFAC, EU, UK and national sources, refreshed daily).
        Subjects may be inline (person/business) or reference an existing
        customer.

        Send an `Idempotency-Key` to retry safely: the same key and body return the same
        screening, with `Idempotent-Replayed: true`, and on OZAV Services the retry is not
        charged again.
      x-ozav-entitlement: compliance
      x-ozav-scopes:
        - screenings:write
      x-ozav-availability: live
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/X402PaymentSignature"
        - $ref: "#/components/parameters/XOzavSimulatePayment"
        - $ref: "#/components/parameters/IdempotencyKeyOptional"
      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:
                    - `sanctions` - consolidated sanctions / designated-persons lists
                      (UN, OFAC, EU, UK and national sources). Consulted for every subject.
                    - `pep` - politically exposed persons. Consulted in-house for subjects
                      whose `country` is `BR` (the Brazilian PEP register, refreshed
                      monthly; `list_source` is `national`). For any other or missing
                      country the list cannot be consulted and the result is `review`
                      with an empty `matches` array.
                    - `adverse_media` - negative-news screening. Not consultable yet: the
                      result is `review` with an empty `matches` array.

                    Defaults to `[ sanctions ]` when omitted.
                  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"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/screenings \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "subject": {
                  "person": {
                    "name": "Jane Example Doe",
                    "date_of_birth": "1980-04-12",
                    "country": "BR"
                  }
                },
                "lists": [
                  "sanctions",
                  "pep"
                ]
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/screenings", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "subject": {
                  "person": {
                    "name": "Jane Example Doe",
                    "date_of_birth": "1980-04-12",
                    "country": "BR"
                  }
                },
                "lists": [
                  "sanctions",
                  "pep"
                ]
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/screenings",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "subject": {
                        "person": {
                            "name": "Jane Example Doe",
                            "date_of_birth": "1980-04-12",
                            "country": "BR",
                        },
                    },
                    "lists": [
                        "sanctions",
                        "pep",
                    ],
                },
            )
            data = response.json()
    get:
      operationId: listScreenings
      tags:
        - Screenings
      summary: List screenings
      description: |
        Lists screenings, newest first, with the standard cursor pagination.
        Filter to one subject with the `customer_id` query parameter to
        reconstruct a customer's screening trail for audit. Returns a `Page` of
        `Screening`.
      x-ozav-entitlement: compliance
      x-ozav-scopes:
        - screenings:read
      x-ozav-availability: live
      x-ozav-services: true
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/screenings", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/screenings",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        `result` and any `matches`. Retain the id as evidence of the check.
      x-ozav-entitlement: compliance
      x-ozav-scopes:
        - screenings:read
      x-ozav-availability: live
      x-ozav-services: true
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/screenings/ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/screenings/ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /screenings/{screening_id}/hits/{hit_id}:
    patch:
      operationId: decideScreeningHit
      tags:
        - Screenings
      summary: Decide a screening hit
      description: |
        Marks one hit of a screening as `false_positive` or `confirmed`, with a reason.

        **Decide once.** The decision is kept by the hit's fingerprint (list entry, strength, listed name)
        and reused by the following screenings of the same subject that hit the same entry - when the
        subject is identifiable: a `customer_id`, or a person with `date_of_birth`. Without that (a
        person without date of birth, or an inline business), the decision applies to this screening
        only, because a bare name matches every homonym. A changed list entry starts `open`.

        The screening you decide on keeps the `suggested_decision` it was created with; its
        `matches[].status` shows the decision. Your decision is the final one, on any hit, sanctions
        lists included. When OZAV compliance has also reviewed a hit, its decision shows until you
        decide. Screenings created before hit decisions existed return 422 `HIT_NOT_DECIDABLE`: screen
        the subject again and decide on the new screening.
        Sending the decision the hit already has (by you) returns 200 with the current state and
        records nothing new.
      x-ozav-entitlement: compliance
      x-ozav-scopes:
        - screenings:write
      x-ozav-availability: live
      x-ozav-services: true
      parameters:
        - name: screening_id
          in: path
          required: true
          description: The opaque screening id (`ozav_scr_...`).
          schema:
            type: string
        - name: hit_id
          in: path
          required: true
          description: The hit id (`hit_...`) from `matches[].id`.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ScreeningHitDecisionRequest"
            examples:
              false_positive:
                summary: A PEP hit that is a homonym
                value:
                  decision: false_positive
                  reason: Different date of birth and tax id checked against the register.
      responses:
        "200":
          description: The hit with its effective decision.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScreeningHit"
        "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/screenings/ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/hits/{hit_id} \
              -X PATCH \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "decision": "false_positive",
                "reason": "Different date of birth and tax id checked against the register."
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/screenings/ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/hits/{hit_id}", {
              method: "PATCH",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "decision": "false_positive",
                "reason": "Different date of birth and tax id checked against the register."
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.patch(
                "https://developers.ozav.io/v2/screenings/ozav_scr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/hits/{hit_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "decision": "false_positive",
                    "reason": "Different date of birth and tax id checked against the register.",
                },
            )
            data = response.json()
  /quotes:
    post:
      operationId: createQuote
      tags:
        - Quotes
      summary: Create an FX quote
      description: |
        Quotes are opaque and indicative (`floating`: the rate re-derives at
        execution), and carry the OZAV all-in client-facing rate. When enabled, they also carry the additional client-selected
        spread in basis points. A quote is executed by creating a transfer that
        references it.

        **Setting your own spread (revenue share).** Pass the optional
        `spread_bps` to add basis points on top of the OZAV all-in rate for
        this quote. This value is your additional client-selected spread; it
        rides the quote through to settlement and is echoed back as
        `exchange_conditions.spread_bps`. OZAV cost, floor, margin, and split
        analytics are internal and are never exposed in the public response.
        Availability of client-selected spread is gated on enablement for your
        organization.

        **Sandbox.** With a test key (`ozav_sk_test_`), quotes are simulated.
        Sandbox quotes use the current production reference for the currency
        pair plus a 1 bp sandbox margin and your configured spread, the way
        production prices them. When no current reference exists for a pair,
        a fixed table stands in. Sandbox quotes are not executable.
      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"
                    chain:
                      type: string
                      description: |
                        **Send this when the source is a stablecoin.** The network the
                        funds will leave from (`tron`, `ethereum`, `polygon`, `solana`,
                        and so on), the same value you send as `source.chain` on `POST /transfers`.
                        The network cost of moving the stablecoin is part of the price,
                        and it differs by orders of magnitude between networks, so a
                        quote for a stablecoin source needs to know the network. A quote
                        that cannot determine it is refused with `422
                        NETWORK_COST_UNPRICED`; a transfer whose `source.chain` differs
                        from the one the quote priced is refused with `422
                        QUOTE_NETWORK_MISMATCH`. Ignored for fiat sources.
                target:
                  type: object
                  required:
                    - currency
                  properties:
                    currency:
                      $ref: "#/components/schemas/Currency"
                    amount:
                      $ref: "#/components/schemas/DecimalAmount"
                    chain:
                      type: string
                      description: |
                        **Send this when the target is a stablecoin.** The network the
                        stablecoin will be delivered on (`tron`, `ethereum`, `polygon`,
                        `solana`, and so on).

                        Delivering a stablecoin is an on-chain transfer, and its network
                        cost differs by orders of magnitude: the same delivery costs about
                        US$ 3.39 per leg on `tron` and about US$ 0.001 on `solana` or
                        `base`. Sending this lets the quote price that cost instead of
                        guessing it.

                        Your account's configured network is used when you omit it. Where
                        neither is available the quote is still returned, without the
                        network cost priced in - but the network you actually deliver on
                        is the one that decides what the operation costs, so send it.
                        Ignored for fiat targets.
                source_rail:
                  $ref: "#/components/schemas/RequestRail"
                destination_rail:
                  $ref: "#/components/schemas/RequestRail"
                destination_country:
                  type: string
                  pattern: ^[A-Za-z]{2}$
                  example: CN
                  description: |
                    Optional. ISO 3166-1 alpha-2 country of the beneficiary's bank, for a
                    fiat target. Some currencies cost more to deliver in one country than
                    another (USD paid in China, say), and the quote prices the country you
                    send. Without it, the quote uses the currency's default country. A
                    currency priced only for other countries answers `422
                    CORRIDOR_NOT_PRICED_FOR_COUNTRY` (`param: destination_country`). It
                    cannot be combined with `via` yet (`400
                    DESTINATION_COUNTRY_NOT_SUPPORTED_WITH_VIA`).
                spread_bps:
                  type: integer
                  minimum: 0
                  maximum: 10000
                  description: |
                    Optional. Additional client-selected spread for this quote,
                    in integer basis points. **This is the spread you charge your
                    end customer, not an OZAV charge; it is revenue-shared with
                    OZAV under your commercial agreement.** Available only when
                    enabled for your organization. A non-integer, negative, or
                    out-of-range value is rejected with 400.


                    **It is applied inside `rate`.** The quote you receive is
                    already marked up by this many basis points, so comparing
                    that rate to a market reference (an exchange spot price, say)
                    will look worse by about this amount. It is echoed back as
                    `exchange_conditions.spread_bps`, and
                    `exchange_conditions.rate_without_spread` returns the rate
                    before it.


                    **Omitting it applies your configured default**, which can be
                    different from `0` if one was negotiated for your
                    organization. Send `0` explicitly to quote at the OZAV all-in
                    rate (all-in except for any `amount_deducted`).
                funding_source:
                  type: string
                  enum:
                    - incoming_transfer
                    - named_account_balance
                  description: |
                    Optional. How the source fiat is funded. `incoming_transfer`
                    includes the measured inbound cost (USD SWIFT US$ 35;
                    EUR SEPA / GBP Faster Payments US$ 5, deducted from the
                    credit). `named_account_balance` converts funds already on
                    the named account and includes the outbound cost to the
                    desk (USD wire US$ 35 + 12 bps; EUR/GBP local US$ 5 + 12
                    bps, charged on top). Omitted behaves as
                    `incoming_transfer` for USD, EUR and GBP.
                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.

                    **Silent 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.

                    **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.
                via:
                  type: object
                  required:
                    - currency
                  additionalProperties: false
                  properties:
                    currency:
                      type: string
                      description: The stablecoin the conversion passes through (`USDT` or `USDC`).
                    chain:
                      type: string
                      description: Optional. The network the stablecoin moves on. Omit it and OZAV uses the network the payout route runs on. A network the route does not serve is refused with `422 ASSET_NOT_AVAILABLE_ON_NETWORK`.
                  description: |
                    Optional. **Quote BRL into a foreign currency in a single quote**, passing
                    through a stablecoin: OZAV buys the stablecoin with the BRL and pays
                    the foreign currency out of it. The response carries the end-to-end
                    `rate`, plus `via` and `legs`. Each leg is priced on its own, and the
                    second leg is priced on exactly the stablecoin the first delivers.

                    - The source must be `BRL` and the target a foreign payout currency
                      (USD, EUR, GBP, ...). Send `source.amount`: `target.amount` is
                      refused with `400 SOURCE_AMOUNT_REQUIRED`.
                    - Live keys: enabled per organization, otherwise `422 VIA_NOT_ENABLED`
                      (`param: via`). Once enabled, every payout currency in the
                      `POST /payout-destinations` corridor table is open. Test keys quote it freely against the
                      sandbox simulator; while the simulator is unavailable they get `422
                      RAIL_NOT_AVAILABLE` (`param: via`).
                    - `spread_bps` applies once, on the first leg. The top-level
                      `exchange_conditions` echoes it as `spread_bps` and carries no
                      `rate_without_spread` (an end-to-end rate has no derivable
                      "without spread"); each leg carries its own
                      `exchange_conditions`.
                    - The payout route caps each transfer: below its minimum you get
                      `400 AMOUNT_BELOW_MINIMUM`, above its maximum `400
                      AMOUNT_ABOVE_MAXIMUM` (split the operation).
                    - The network is `via.chain`. Not combinable with `source.chain`,
                      `target.chain`, `facility_preview` or `funding_source` (`400
                      FIELD_NOT_ALLOWED`, `param` names the field).
                    - Floating, like every quote: each leg re-prices at execution.

                    Refusals specific to `via`:
                    - `400 UNSUPPORTED_PAIR`: the source is not `BRL` (`param: source.currency`)
                      or the target is not a foreign payout currency (`param: target.currency`).
                    - `400 UNSUPPORTED_CURRENCY` (`param: via.currency`) and
                      `400 SOURCE_CHAIN_UNSUPPORTED` (`param: via.chain`).
                    - `422 RAIL_NOT_AVAILABLE`: no payout route covers the currency through
                      that stablecoin right now (`param: target.currency`), or the requested
                      `destination_rail` is not served by it (`param: destination_rail`).
                    - `422 RAIL_NOT_AVAILABLE` (`param: via`): the network cost of the route
                      cannot be priced right now.
                    - `422 ASSET_NOT_AVAILABLE_ON_NETWORK` (`param: via.chain`).
                    - `422 TICKET_BELOW_COST` (`param: source.amount`): the amount does
                      not cover the route's costs, network cost included; increase
                      `source.amount`.
                    - `422 NO_QUOTE_AVAILABLE`: the two legs did not compose; request a new quote.

                    **Executing it.** Send the quote to `POST /transfers` with `source` a Pix
                    payment (`fiat_payment`, `BRL`, `rail: pix`) and `destination` a payout
                    destination in the target currency. The response carries the Pix to pay.
                    When the Pix settles, OZAV buys the stablecoin into the customer's OZAV
                    wallet and pays the target currency out on its own; the transfer stays
                    `converting` until the payout ends.
                    - The payout leg re-prices when the Pix settles, so the amount delivered
                      can differ from the quote's `target.amount`.
                    - Everything the payout can refuse (destination, documents, minimum,
                      maximum) is checked before the Pix is created, with the same codes as a
                      direct payout.
                    - The customer needs an OZAV wallet that holds the stablecoin: `422
                      WALLET_REQUIRED` (`param: customer_id`).
                    - If the payout is refused after the Pix settles, the transfer moves to
                      `needs_review` (event `transfer.updated`, not terminal) and the
                      stablecoin stays in the customer's OZAV wallet until OZAV resolves it.
                    - If the payout starts and then fails or is cancelled, the transfer ends
                      `failed` or `cancelled` with `source_leg: completed` (the Pix was
                      received). After `cancelled` the stablecoin is back in the customer's
                      OZAV wallet; after `failed` it stays held until OZAV resolves it.
                    - `target.amount` stays the quoted estimate; the transfer does not report
                      the re-priced amount delivered.
                    - Not available on test keys, nor with `facility_terms_accepted`: `422
                      NOT_AVAILABLE`. A `via` quote the API can no longer execute: `422
                      QUOTE_EXPIRED`, request a new one.
              description: Provide exactly one of source.amount or target.amount.
            examples:
              brl_to_usd:
                summary: BRL → USD at the OZAV all-in rate (start here)
                description: |
                  `spread_bps: 0`, sent explicitly: leaving it out applies your
                  account's default spread, if one is configured. The `rate` you
                  get back is the OZAV all-in rate and
                  `exchange_conditions.rate_without_spread` equals it. Compare
                  `rate_without_spread` against a market reference.
                value:
                  customer_id: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  source:
                    currency: BRL
                    amount: "50000.00"
                  target:
                    currency: USD
                  spread_bps: 0
              brl_to_usd_with_revenue_share:
                summary: BRL → USD adding a 40 bps spread you charge your end customer
                description: |
                  **`spread_bps` is what you charge your end customer**, not
                  an OZAV charge (it is revenue-shared with OZAV under your
                  commercial agreement). It is applied inside `rate`. The quote
                  you receive is already marked up by it, and comparing that rate
                  to a market reference will look worse by about this many basis
                  points; `exchange_conditions.rate_without_spread` shows the
                  rate before it.

                  Copy this example only if you mean to add your own spread. To
                  quote at the OZAV rate, use the example above.
                value:
                  customer_id: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  source:
                    currency: BRL
                    amount: "50000.00"
                  target:
                    currency: USD
                  spread_bps: 40
              brl_to_usd_via_usdt:
                summary: BRL → USD through USDT, one end-to-end quote
                description: |
                  One quote for the whole route: BRL buys USDT, and the USDT pays out
                  USD. The response carries the end-to-end `rate` plus `via` and the two
                  `legs`. Live keys need the route enabled for the organization.
                value:
                  customer_id: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  source:
                    currency: BRL
                    amount: "100000.00"
                  target:
                    currency: USD
                  via:
                    currency: USDT
      responses:
        "201":
          description: The quote.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Quote"
              examples:
                quote:
                  summary: OZAV all-in quote (no 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: 0
                      rate_without_spread: "0.1881"
                    expires_at: null
                    created_at: 2026-07-12T18:00:00Z
                quote_via_usdt:
                  summary: "BRL → USD via USDT: one end-to-end quote with its two legs"
                  value:
                    id: ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V2
                    object: quote
                    customer_id: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                    rate_type: floating
                    rate: "0.19074130"
                    source:
                      amount: "100000.00"
                      currency: BRL
                    target:
                      amount: "19074.13"
                      currency: USD
                    exchange_conditions:
                      spread_bps: 0
                    expires_at: null
                    created_at: 2026-10-05T12:00:00Z
                    via:
                      currency: USDT
                      chain: ethereum
                    legs:
                      - source:
                          amount: "100000.00"
                          currency: BRL
                        target:
                          amount: "19106.618234"
                          currency: USDT
                        rate: "0.19106618"
                        exchange_conditions:
                          spread_bps: 0
                          rate_without_spread: "0.19106618"
                      - source:
                          amount: "19106.610000"
                          currency: USDT
                        target:
                          amount: "19074.13"
                          currency: USD
                        rate: "0.9983"
                        exchange_conditions:
                          spread_bps: 0
                          rate_without_spread: "0.9983"
        "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"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/quotes \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "source": {
                  "currency": "BRL",
                  "amount": "50000.00"
                },
                "target": {
                  "currency": "USD"
                },
                "spread_bps": 0
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/quotes", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "source": {
                  "currency": "BRL",
                  "amount": "50000.00"
                },
                "target": {
                  "currency": "USD"
                },
                "spread_bps": 0
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/quotes",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "source": {
                        "currency": "BRL",
                        "amount": "50000.00",
                    },
                    "target": {
                        "currency": "USD",
                    },
                    "spread_bps": 0,
                },
            )
            data = 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`,
        `exchange_conditions`, and `expires_at`. Every quote today is
        `rate_type: floating` with `expires_at: null`: the rate is indicative
        and re-derives at execution (see `Quote.rate_type`).
      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: 0
                      rate_without_spread: "0.1881"
                    expires_at: null
                    created_at: 2026-07-12T18:00:00Z
                quote_via_usdt:
                  summary: "A quote requested via USDT: the same `via` and `legs` as when it was created"
                  value:
                    id: ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V2
                    object: quote
                    customer_id: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                    rate_type: floating
                    rate: "0.19074130"
                    source:
                      amount: "100000.00"
                      currency: BRL
                    target:
                      amount: "19074.13"
                      currency: USD
                    exchange_conditions:
                      spread_bps: 0
                    expires_at: null
                    created_at: 2026-10-05T12:00:00Z
                    via:
                      currency: USDT
                      chain: ethereum
                    legs:
                      - source:
                          amount: "100000.00"
                          currency: BRL
                        target:
                          amount: "19106.618234"
                          currency: USDT
                        rate: "0.19106618"
                        exchange_conditions:
                          spread_bps: 0
                          rate_without_spread: "0.19106618"
                      - source:
                          amount: "19106.610000"
                          currency: USDT
                        target:
                          amount: "19074.13"
                          currency: USD
                        rate: "0.9983"
                        exchange_conditions:
                          spread_bps: 0
                          rate_without_spread: "0.9983"
        "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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/quotes/ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/quotes/ozav_q_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        transfers you created through this API, and the on-chain deposits
        credited into custody. You no longer have to stitch two listings together
        and guess the order between them.

        Ordered newest first. Each row carries a `type` (`transfer` or
        `deposit`), so you can branch without a second lookup, and the `id` is
        the id of the underlying resource - use it to fetch the full record.

        Paginated with the standard opaque cursor. The cursor spans both sources
        at once: it is not one cursor per source, so a busy day on one side can
        never hide rows from the other.

        **Card transactions are not in this feed.** They are read per card at
        `GET /cards/{card_id}/transactions`.
      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
            both sources: transfers created for that customer, and the deposits
            credited to it.

            An id that does not exist, or belongs to another organization or
            another environment, returns an empty page rather than `404` - the
            same behaviour as `GET /transfers`, so a filter never tells you
            whether an id exists somewhere you cannot see.
          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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/transactions", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/transactions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /transfers:
    post:
      operationId: createTransfer
      tags:
        - Transfers
      summary: Create a transfer
      description: |
        The single money-movement resource. The pair (`source.type`,
        `destination.type`) selects the flow - you never call a different
        endpoint for on-ramp vs off-ramp:

        | source          | destination          | flow            | availability |
        |-----------------|----------------------|-----------------|--------------|
        | `fiat_payment`  | `payout_destination` | FX (fiat→fiat)  | live         |
        | `fiat_payment`  | `wallet`             | on-ramp         | live         |
        | `account`       | `wallet`             | on-ramp from the named account's balance | live |
        | `wallet`        | `payout_destination` | off-ramp        | live         |
        | `wallet`        | `account`            | off-ramp to the customer's own account | live |
        | `wallet`        | `wallet`             | wallet transfer | live         |
        | `wallet`        | `crypto_address`     | crypto send     | live         |

        **Sending to an on-chain address runs on this route.** A `wallet` source
        with a `crypto_address` destination is a custody move: OZAV signs from the
        source wallet's vault and broadcasts. No quote is involved (it is a
        same-asset move, not an exchange), so send `amount` and omit `quote_id`.
        It needs the `custody` or the `liquidity` product. The `/wallets` routes need `custody`:
        listing wallets (`GET /wallets`), reading a customer's wallet, its deposit addresses,
        addresses and policy, and `POST /wallets/{wallet_id}/transfers`. It is subject to the two controls below.

        **On-ramp from the account balance.** An `account` source spends the BRL balance of the
        customer's BRL named account: Pix payments to the account's key accumulate there, and nothing
        converts by itself. An account that cannot fund an order yet gets `422 ACCOUNT_NOT_READY`:
        it is not active, its institution cannot send Pix, or its balance could not be read just now.
        Converting more than the balance not already committed to an open order gets
        `422 ACCOUNT_BALANCE_INSUFFICIENT`. If OZAV pauses this flow, every organization gets
        `422 ACCOUNT_FUNDED_ORDERS_NOT_ENABLED` at once: nothing in your request causes it.

        Some of these refusals come after the transfer is recorded: it ends `failed`, and a retry
        with the same `Idempotency-Key` returns the same refusal. To try again later, send the
        request with a new key. An `ACCOUNT_NOT_READY` that persists across a few retries means the
        account cannot fund orders yet. Each conversion is one transfer with a quote, like any on-ramp, and the
        BRL 1,000 on-ramp floor applies per transfer, not per deposit. A smaller deposit stays in
        the account and adds up with the next ones.

        In the sandbox, a test key converts from a sandbox account: credit it first with
        `POST /accounts/{account_id}/simulate/credit`, then send the transfer. The order runs on the
        simulator and the conversion debits the simulated balance.

        **Wallet-to-wallet runs here too.** The source wallet must belong to the
        `customer_id` on the request - that is whose money moves. The destination
        only has to be a wallet in your organization, so you can move value between
        two of your customers' wallets. A wallet outside your organization is not
        found, and a deactivated wallet does not receive.

        The two controls:

        - **Managed wallets only.** A self-custody source is refused with
          `422 WALLET_NOT_MANAGED` - OZAV holds no key to sign with. That send
          goes through `POST /wallets/{wallet_id}/transaction-intents`, which
          returns unsigned calldata for the end-user's own key.
        - **Travel Rule (BCB Resolution 520)** on any send that leaves custody.
          A `crypto_address` destination without complete Travel Rule data is
          refused with `422 TRAVEL_RULE_REQUIRED`, and `details.missing_fields`
          names each field that is missing. A destination that is an `ozav_wal_`
          id is exempt: the move stays inside custody and both sides are already
          verified.

        `POST /wallets/{wallet_id}/transfers` also carries a destination `memo`,
        which networks like Stellar need for exchange deposits (an exchange
        credits by memo). `POST /transfers` has no `memo` field, so a memo-bearing
        send belongs on the wallets surface even after custody lands here.

        **Receiving crypto needs nothing from you, and is not a transfer.** Fund an
        address from `GET /wallets/{wallet_id}/deposit-addresses` and the credit
        lands on the wallet by itself. You see it in two places already:

        - `GET /wallets/{wallet_id}/balances` reflects the new amount;
        - `GET /transactions` lists the arrival alongside your transfers, in one
          timeline, so you do not have to stitch two listings together.

        Creating a transfer with a `crypto_payment` source is refused with
        `422 NOT_AVAILABLE`, and that refusal is permanent rather than pending: an
        arrival is observed, never requested, so there is nothing for you to create.
        The one thing still missing is a *push* notification for it - today you
        learn about an arrival by reading, not by being told.

        BRL/Pix on-ramp returns transaction-specific Pix payment
        instructions on `source.payment_instructions`. BRL/Pix off-ramp
        creates the operation only after the customer's stable
        funds are reserved; final Pix-out remains gated until settlement rails
        are enabled. Other combinations fail closed (`422 RAIL_NOT_AVAILABLE` /
        `NOT_AVAILABLE`) until settlement is enabled - never a silent partial
        result. See
        `x-ozav-transfer-flow-availability` for the machine-readable status.

        `quote_id` is required whenever source and target currencies differ, and
        is authoritative for the amount when present. For a same-currency move (no
        FX, so no quote) provide `amount` - it is how much to move. Exactly one of
        `quote_id` or `amount` must be present; supplying both is a 400. The
        `Idempotency-Key` header is required.
      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.
              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.
                source:
                  $ref: "#/components/schemas/TransferSource"
                destination:
                  $ref: "#/components/schemas/TransferDestination"
                travel_rule:
                  $ref: "#/components/schemas/TravelRule"
                metadata:
                  allOf:
                    - $ref: "#/components/schemas/Metadata"
                  description: |
                    Free-form string map, with **three reserved keys that some payout corridors require**. When a corridor requires one and it is absent, the transfer is refused at creation with a `400` that names the field and lists the accepted values - never later, with the funds already committed.

                    `purpose_code` - what the payment is for. Required by the US, EU/UK, China, Japan, Peru and Nigeria corridors, each with its own list (e.g. China accepts only `GDDS` and `SCVE`; Peru accepts `FAMI`, `SUPP`, `COMP`, `INVS`, `OTHR`). The refusal carries the list for the corridor you are paying into.

                    `source_of_cash` - where the money being sent comes from. Required by the Japan corridor. Closed list: `RETA` (retained earnings), `COMP` (compensation), `SVGS` (savings), `REST` (real estate sale), `SEAQ` (sale of equity), `CASH` (cash). This is a declaration about the sender's funds - OZAV never infers it.

                    `payment_memo` - the reference that travels with the payment, by which the beneficiary recognises the credit. Required by the Japan corridor. When absent, OZAV sends a reference derived from the transfer.

                    Every other key is carried through untouched.
                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.

                    Both 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.

                    **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`.

                    **Strictly `true`, not truthy.** `"true"`, `1` and `{}` all read as "no lock requested", silently - the transfer is created unlocked.

                    **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.

                    **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
                    chain: solana
      responses:
        "200":
          description: |
            Degraded success - the transfer was dispatched, but finalizing the record afterwards failed (projection persist, owner binding, or serialization). The payment may already be on its way, 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".

            Branch 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.
          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: 0
                    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, or a `via` quote sent from something other than a Pix payment, on a test key, or with `facility_terms_accepted`), WALLET_REQUIRED (a `via` quote needs the customer's OZAV wallet for the stablecoin), 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), QUOTE_NETWORK_MISMATCH (the quote priced the network cost for a different `source.chain`; request a new quote with the right one), QUOTE_WITHOUT_NETWORK_COST (the quote did not price the network cost; request a new quote with `source.chain`), 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).
          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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "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",
                  "chain": "solana"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/transfers", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "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",
                  "chain": "solana"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/transfers",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "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",
                        "chain": "solana",
                    },
                },
            )
            data = response.json()
    get:
      operationId: listTransfers
      tags:
        - Transfers
      summary: List transfers
      description: |
        Lists transfers, newest first, with the standard cursor pagination.
        Filter by `customer_id` and/or `status` (a `TransferStatus` value).
        Returns a `Page` of `Transfer`. BRL/Pix paths create real transfer
        records; combinations whose settlement rails are not enabled still fail
        closed before a transfer is persisted.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/transfers", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/transfers",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        independent `source_leg` / `destination_leg` settlement states. Poll this
        to track a transfer to completion. BRL/Pix paths create real
        transfer records; other combinations may remain gated.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
    patch:
      operationId: updateTransfer
      tags:
        - Transfers
      summary: Update mutable transfer attributes (metadata)
      description: |
        Updates the mutable attributes of a transfer - currently just
        `metadata`. Does not alter money movement. Returns the updated
        `Transfer`.
      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 \
              -X PATCH \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "metadata": {
                  "invoice": "INV-2026-0042"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "PATCH",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "metadata": {
                  "invoice": "INV-2026-0042"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.patch(
                "https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "metadata": {
                        "invoice": "INV-2026-0042",
                    },
                },
            )
            data = 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
        practice the invoice the payment settles. The receiving institution
        validates the purpose code against this document, so a payout can be
        accepted and then sit unsettled while it is missing.

        One document per transfer. Upload it before the payout is dispatched:
        the document is forwarded to the receiving institution only after the
        payment order exists, so this response does not say whether it has
        been forwarded.

        The bytes travel inline as base64 in a single call, unlike customer
        documents, which use a two-call signed upload. A payout document is one
        small file per operation, and a single call makes the result
        indivisible, with no window where the document is registered and its
        bytes are absent.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "file_name": "invoice-2026-08.pdf",
                "content_type": "application/pdf",
                "content_base64": "JVBERi0xLjQKJeLjz9MK"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "file_name": "invoice-2026-08.pdf",
                "content_type": "application/pdf",
                "content_base64": "JVBERi0xLjQKJeLjz9MK"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/documents",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "file_name": "invoice-2026-08.pdf",
                    "content_type": "application/pdf",
                    "content_base64": "JVBERi0xLjQKJeLjz9MK",
                },
            )
            data = 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
        whose destination is an external crypto address. Required by policy above
        a server-enforced amount threshold; it may also be supplied inline on
        `POST /transfers`. Returns the updated `Transfer`. Crypto-address
        transfer legs remain gated until enabled.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "originator": {
                  "name": "Acme Ltda",
                  "account": "0xA97b1f0c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70"
                },
                "beneficiary": {
                  "name": "Jane Roe",
                  "account": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/travel-rule", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "originator": {
                  "name": "Acme Ltda",
                  "account": "0xA97b1f0c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70"
                },
                "beneficiary": {
                  "name": "Jane Roe",
                  "account": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/travel-rule",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "originator": {
                        "name": "Acme Ltda",
                        "account": "0xA97b1f0c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f70",
                    },
                    "beneficiary": {
                        "name": "Jane Roe",
                        "account": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                    },
                },
            )
            data = 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
        sitting in `awaiting_payment`, so you can drive the state machine past
        the one step no test key can reach on its own: in `test` there is no
        real payer and no payment confirmation, so an on-ramp would otherwise wait
        in `awaiting_payment` until it expires.

        The transfer advances through both legs, in two steps, exactly as a real
        confirmed payment would: the pay-in leg settles (`payment_confirmed`,
        emitting `transfer.updated`), then the destination leg settles and the
        transfer reaches a terminal `completed`, emitting `transfer.completed`.
        Both events are delivered and both appear on `GET /events`, in that
        order - the response body carries the terminal state. The wallet balance
        on `GET /wallets/{wallet_id}/balances` moves with it.


        One case still stops at `payment_confirmed`: a transfer whose opening
        leg was settled by a real rail rather than by the sandbox.
        Fabricating a completion there would claim money arrived and was
        delivered when neither happened.

        **Only on-ramp and fx transfers can be simulated**, because they are the
        only ones with an incoming payment to confirm. An off-ramp opens on its
        payout leg and a wallet transfer is a custody move, so neither has a
        payer to simulate; both are refused with `409
        TRANSFER_NOT_AWAITING_PAYMENT`, and so is a transfer that has already
        moved past `awaiting_payment`. Create a new transfer to test another
        outcome.

        **Test keys only.** A live key is refused with `403 SANDBOX_ONLY`; the
        environment comes from the key prefix (`ozav_sk_test_` vs
        `ozav_sk_live_`), never from the URL.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/simulate", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1/simulate",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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. The same
        endpoint serves every currency and rail; which ones can be opened is in
        `Rail.x-ozav-rail-availability`.

        The customer must already be approved. In sandbox, BRL/BR/pix
        materializes a simulated account with deterministic Pix and
        bank-account instructions; no real bank account is opened and no real
        funds can move. Every corridor OZAV serves uses this same
        contract, and a corridor OZAV cannot currently open answers a named
        error rather than a partial account, so your integration learns the
        outcome from the response instead of from a status label. The public
        response never exposes the banking venue behind the account.
      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:
              minimal:
                summary: The whole request - OZAV picks the corridor
                description: |-
                  Ask for an account in a currency. OZAV resolves the country and
                  rail, because where the receiving bank sits is a fact about OZAV,
                  not about you. If that currency ever has more than one live
                  corridor, the call is refused with `ACCOUNT_RAIL_AMBIGUOUS` and
                  the candidates named, so nothing is chosen on your behalf.
                value:
                  customer_id: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  currency: USD
              declared_corridor:
                summary: Pinning the corridor yourself
                description: |-
                  Send `country` and `rail` when you want a specific one. The
                  request behaves exactly as before, including the refusal when the
                  corridor you named is not open.
                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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "currency": "USD"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/accounts", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "currency": "USD"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/accounts",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "currency": "USD",
                },
            )
            data = response.json()
    get:
      operationId: listAccounts
      tags:
        - Accounts
      summary: List named accounts
      description: |
        Lists named fiat accounts with standard cursor pagination. Filter with
        `customer_id`, `currency`, `country`, `rail`, or `status` to recover the
        account created after sandbox KYC approval or live opening. Returns
        only accounts visible in the current key environment.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/accounts", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/accounts",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        account must belong to the caller and match the current key environment.
      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} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/accounts/{account_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/accounts/{account_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        account's institution reports them. Where the institution exposes a live
        statement, it is read at request time, not from an OZAV copy, so it can lag the
        institution by a few seconds and it is never older than the account's
        `balance.as_of`.

        A BRL account lists the Pix credits OZAV has already read from its statement,
        which happens every 10 minutes, so a credit can take up to that long to appear.
        These are the same credits that move `balance`, and each one also fires
        `account.credited`. The 100 newest are returned. Sandbox BRL accounts are
        included: `POST /accounts/{account_id}/simulate/credit` adds a credit to them.

        Each item is a credit into the account. `reference` is the text the payer sent
        with the transfer, when the rail carries one; `received` is present only when the
        institution reports the amount that actually landed separately from the amount
        the payer sent (international wires lose correspondent charges on the way).

        `501 ACCOUNT_MOVEMENTS_NOT_AVAILABLE` means this account's institution does not
        expose a statement through OZAV (sandbox accounts in other currencies included);
        `503 ACCOUNT_MOVEMENTS_UNAVAILABLE` means it does, and the read failed - retry.
      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 (the 100 newest, on a BRL account) 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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/accounts/{account_id}/movements", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/accounts/{account_id}/movements",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /accounts/{account_id}/simulate/credit:
    post:
      operationId: simulateAccountCredit
      tags:
        - Accounts
      summary: Simulate a Pix credit into a sandbox BRL account (sandbox only)
      description: |
        Simulates a Pix that a payer sends into a sandbox BRL account. The credit is
        added to the account's `balance`, listed on `GET /accounts/{account_id}/movements`
        and announced with `account.credited`, the same three things a real Pix into a
        live account produces. Use it to build and test your deposit handling before a
        live account receives money.

        To test the conversion, create a transfer with `source.type: account` from this
        account to the customer's wallet. In the sandbox the balance is debited at once,
        the transfer completes, and the refusals are the live ones: below the BRL 1,000
        floor, or above the balance not already committed to an open order
        (`422 ACCOUNT_BALANCE_INSUFFICIENT`).

        Available only to test-environment API keys on sandbox BRL accounts. A live
        key, or an account in another currency, gets `404 SIMULATION_NOT_AVAILABLE`.

        This route takes no `Idempotency-Key`: every call is a new credit.
      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:
              type: object
              required:
                - amount
              properties:
                amount:
                  type: string
                  pattern: ^\d{1,12}(\.\d{1,2})?$
                  description: The amount the payer sends, in BRL, as a decimal string with at most 2 decimal places. Must be positive.
                  example: "1500.00"
            examples:
              pix:
                summary: A Pix of R$ 1,500.00
                value:
                  amount: "1500.00"
      responses:
        "201":
          description: The credit, in the same shape `GET /accounts/{account_id}/movements` lists it.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AccountMovement"
              example:
                id: ozav_amov_7c1e2d3f4a5b4c6d8e9f0a1b2c3d4e5f
                object: account_movement
                direction: credit
                amount: "1500.00"
                currency: BRL
                occurred_at: 2026-10-06T12:00:00Z
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/DependencyUnavailable"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/accounts/{account_id}/simulate/credit \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "amount": "1500.00"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/accounts/{account_id}/simulate/credit", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "amount": "1500.00"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/accounts/{account_id}/simulate/credit",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "amount": "1500.00",
                },
            )
            data = 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
        handling. Live account lifecycle changes remain gated until the
        operating procedure for your organization is enabled.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/accounts/{account_id}/deactivate", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/accounts/{account_id}/deactivate",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        account lifecycle changes remain gated until the operating procedure
        for your organization is enabled.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/accounts/{account_id}/reactivate", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/accounts/{account_id}/reactivate",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        customer's registered payout destinations, in the account's currency.
        No conversion happens on this endpoint: the destination must be in the
        same currency as the account (use `POST /transfers` to convert first).

        The payment leaves **the customer's account**, with the customer as the
        sender of record. It is refused, with a named code, when the account
        cannot send yet or the customer who owns it is not approved and active
        (`422 ACCOUNT_NOT_READY`, with the exact reason in `details.reason`), when
        a compliance hold applies to that customer (`403 COMPLIANCE_HOLD`), when the customer profile
        lacks fields the payment rail requires
        (`422 ACCOUNT_PAYOUT_SENDER_INCOMPLETE`), when the destination belongs to
        a different customer (`422 PAYOUT_DESTINATION_NOT_OWNED`) or lacks data
        the rail requires (`422 PAYOUT_DESTINATION_INCOMPLETE`), when the amount
        is above the per-payout limit for your organization
        (`422 ACCOUNT_PAYOUT_ABOVE_LIMIT`), or when the account's available
        balance does not cover it - payouts still in flight count
        (`422 ACCOUNT_BALANCE_INSUFFICIENT`). Payouts from named accounts are
        enabled per organization: until yours is, the endpoint answers
        `403 ACCOUNT_PAYOUT_NOT_ENABLED`.

        **BRL by Pix key.** A BRL account pays only to an account of the same
        holder: the destination document must be the account holder's
        (`422 PAYOUT_DESTINATION_HOLDER_MISMATCH`). Before anything is sent, OZAV
        asks the Central Bank of Brazil who owns the Pix key, with the same codes
        `POST /transfers` uses: a key of someone else is refused
        (`422 DESTINATION_PIX_KEY_HOLDER_MISMATCH`), a key that does not exist too
        (`422 DESTINATION_PIX_KEY_NOT_FOUND`), and when the check cannot be
        completed the payout is refused with
        `503 PAYOUT_DESTINATION_HOLDER_UNVERIFIABLE` - nothing was sent, and
        retrying the same request later is safe; if it keeps failing, the
        Central Bank's answer for that key does not show the holder's document,
        so pay to another key of the same holder. Unlike `POST /transfers`, a key
        whose holder cannot be confirmed is never paid here. The destination
        needs the holder's document (`422 PAYOUT_DESTINATION_INCOMPLETE` without
        it). Where the Pix-key check is not available for the account,
        Pix-key payouts answer `403 ACCOUNT_PAYOUT_NOT_ENABLED`. Repeating a
        payout that already went out returns it with `200` before any of these
        checks, and before the destination is checked again.

        **`reference` is the idempotency lever.** Repeating a request with the
        same `reference` and `amount` for the same account returns the existing
        payout with `200` instead of sending a second payment. The destination
        is not part of that match: the `200` shows the destination the existing
        payout went to, even when the repeated request names another one. It
        also travels to the receiving bank as the remittance information.
      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"
        "502":
          description: The payment institution did not accept the order (`ACCOUNT_PAYOUT_DISPATCH_FAILED`). Nothing left the account; read `GET /accounts/{account_id}/movements` before retrying.
          headers:
            X-Request-ID:
              $ref: "#/components/headers/XRequestId"
          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/accounts/{account_id}/payouts \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "amount": "50.00",
                "destination": {
                  "type": "payout_destination",
                  "payout_destination_id": "ozav_pd_01J00000000000000000000000"
                },
                "reference": "INV 0042"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/accounts/{account_id}/payouts", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "amount": "50.00",
                "destination": {
                  "type": "payout_destination",
                  "payout_destination_id": "ozav_pd_01J00000000000000000000000"
                },
                "reference": "INV 0042"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/accounts/{account_id}/payouts",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "amount": "50.00",
                    "destination": {
                        "type": "payout_destination",
                        "payout_destination_id": "ozav_pd_01J00000000000000000000000",
                    },
                    "reference": "INV 0042",
                },
            )
            data = 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
        discriminated union keyed by `rail`. BRL/Pix destinations use a Pix key
        (`details.pix_key`). Do not submit a Pix BR code here: BR codes are
        pay-in instructions for a specific charge, not reusable payout
        destinations. Rails that require external venue provisioning may still
        fail closed until enabled.

        #### Which corridor, which rail, which `details`

        A payout destination is a third party you pay. It is not the same thing
        as a named account you hold at OZAV, and the two do not share a table:
        `Rail.x-ozav-rail-availability` is about opening an account, and it does
        not describe the corridors below. OZAV keeps no nominal account in yen
        or in rand, and still pays a beneficiary in both.

        | Currency | `rail` | `details` shape | Required in `details` | Also required by the corridor | Availability |
        |---|---|---|---|---|---|
        | BRL | `pix` | Pix | `pix_key` | - | live |
        | USD | `fedwire` | UsBank | `account_number`, `routing_number` | - | live |
        | EUR | `sepa` | Sepa | `iban` | - | live |
        | GBP | `faster_payments` | FasterPayments | `account_number`, `sort_code` | a `business` account holder; an `individual` is refused with `422 ACCOUNT_HOLDER_TYPE_NOT_SUPPORTED` | live |
        | MXN | `spei` | Spei | `clabe` | - | live |
        | AED | `uaefts` | IbanLocal | `iban` | `bic` | live |
        | JPY | `zengin` | LocalBank | `account_number` | `bank_code`, `branch_code` | live |
        | NGN | `nip` | LocalBank | `account_number` | `bank_code`, a contact on the holder | live |
        | PEN | `cce` | LocalBank | `account_number` | `bic`, a contact, postal code and state on the holder, and `nationality` for an individual | live |

        Every corridor in the table is live for every organization with access to payouts.

        **Not currently served for payouts:** `USD/swift`, `EUR/swift`,
        `CAD/eft`, `CNY/cnaps` and `ZAR/rtc`. Registering a destination in one of them is refused.
        `ach` and `co_transfer` are not offered and cannot be requested. A `GBP` destination
        is registered on `faster_payments`: `swift` destinations are `USD` only.
        The `rail` values stay in the enum
        so records that already carry them keep reading. A corridor enters the
        table above only when it pays.

        **"Also required by the corridor" is not required to register.** Those
        fields are enforced when the payment is created, with a refusal that
        names the field - because a destination that serves one corridor must
        not be invalidated the day another corridor asks for more. The
        `bank_code` values in NGN and JPY come from a closed directory; the
        refusal carries the accepted list.

        `account_holder.birth_date` is required for every individual
        beneficiary in the corridors above, and `bank_name` is required on the
        rails that have a bank. Both are accepted here.
      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
              zengin:
                summary: JPY / zengin - the richest corridor, and an individual
                value:
                  customer_id: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  currency: JPY
                  rail: zengin
                  account_holder:
                    name: Kenji Tanaka
                    type: individual
                    birth_date: 1985-04-12
                    address:
                      line1: 2-7-3 Marunouchi
                      city: Tokyo
                      state: Tokyo
                      postal_code: 100-0005
                      country: JP
                  details:
                    account_number: "1234567"
                    bank_code: "0001"
                    branch_code: "001"
                    bank_name: MUFG Bank
              uaefts:
                summary: AED / uaefts - the account is an IBAN, and the bank is the BIC
                value:
                  customer_id: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  currency: AED
                  rail: uaefts
                  account_holder:
                    name: Falcon Trading LLC
                    type: business
                  details:
                    iban: AE070331234567890123456
                    bic: EBILAEAD
                    bank_name: Emirates NBD
      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"
        "403":
          $ref: "#/components/responses/Forbidden"
        "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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "currency": "BRL",
                "rail": "pix",
                "account_holder": {
                  "name": "Ana Ramos",
                  "type": "individual"
                },
                "details": {
                  "pix_key": "ana@example.com"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/payout-destinations", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "currency": "BRL",
                "rail": "pix",
                "account_holder": {
                  "name": "Ana Ramos",
                  "type": "individual"
                },
                "details": {
                  "pix_key": "ana@example.com"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/payout-destinations",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "currency": "BRL",
                    "rail": "pix",
                    "account_holder": {
                        "name": "Ana Ramos",
                        "type": "individual",
                    },
                    "details": {
                        "pix_key": "ana@example.com",
                    },
                },
            )
            data = response.json()
    get:
      operationId: listPayoutDestinations
      tags:
        - Payout Destinations
      summary: List payout destinations
      description: |
        Lists a customer's payout destinations with the standard cursor
        pagination; filter with the `customer_id` query parameter. Returns a
        `Page` of `PayoutDestination`. BRL/Pix key destinations can be created
        and read; externally provisioned rails may still be gated.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/payout-destinations", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/payout-destinations",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        `status`, `rail`, and rail-specific `details`.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
    patch:
      operationId: updatePayoutDestination
      tags:
        - Payout Destinations
      summary: Update a payout destination
      description: |
        Updates a payout destination's mutable attributes (account holder,
        metadata). Only included fields are changed. Returns the updated
        `PayoutDestination`.
      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 \
              -X PATCH \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "account_holder": {
                  "name": "Ana C. Ramos",
                  "type": "individual"
                },
                "metadata": {
                  "reviewed": "true"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "PATCH",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "account_holder": {
                  "name": "Ana C. Ramos",
                  "type": "individual"
                },
                "metadata": {
                  "reviewed": "true"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.patch(
                "https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "account_holder": {
                        "name": "Ana C. Ramos",
                        "type": "individual",
                    },
                    "metadata": {
                        "reviewed": "true",
                    },
                },
            )
            data = 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
        transfer destination; existing transfers are unaffected. Idempotent -
        deactivating an already-inactive destination is a no-op. Returns the
        `PayoutDestination` with `status: inactive`. Reverse it with the
        reactivate endpoint.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivate", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deactivate",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        again as a transfer destination. Idempotent. Returns the
        `PayoutDestination` with `status: active`.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1/reactivate", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/payout-destinations/ozav_pd_01J8Z9K2M3N4P5Q6R7S8T9U0V1/reactivate",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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.
        Eligibility is two conditions, not one: `verification.status` is
        `approved` and the customer's account is active. Approval alone is not
        enough, and a customer who never opened an account never becomes
        eligible. Missing either one returns `422
        CUSTOMER_NOT_CARD_ELIGIBLE`. In production the card spends the USDC in
        the customer's OZAV-managed wallet (see `funding_type` on `Card`), so a
        customer without one is refused with `422 CUSTOMER_WALLET_REQUIRED`:
        create them with `custody_model: managed`. `Idempotency-Key` is required. Sandbox keys create a simulated active card end-to-end;
        live keys require the organization's live card program to be enabled.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "type": "virtual",
                "funding_type": "prepaid_balance"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "type": "virtual",
                "funding_type": "prepaid_balance"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/cards",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "type": "virtual",
                    "funding_type": "prepaid_balance",
                },
            )
            data = 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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/cards",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/cards/{card_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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.

        **This replaces `spending_controls` in full; it does not merge.** Send the complete object: anything you omit is cleared. There is no merge, 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.

        Card 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:
              change_limit:
                summary: Change the monthly limit without re-issuing the card
                value:
                  spending_controls:
                    spending_limits:
                      - amount: 500000
                        interval: monthly
              clear_controls:
                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} \
              -X PATCH \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "spending_controls": {
                  "spending_limits": [
                    {
                      "amount": 500000,
                      "interval": "monthly"
                    }
                  ]
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}", {
              method: "PATCH",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "spending_controls": {
                  "spending_limits": [
                    {
                      "amount": 500000,
                      "interval": "monthly"
                    }
                  ]
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.patch(
                "https://developers.ozav.io/v2/cards/{card_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "spending_controls": {
                        "spending_limits": [
                            {
                                "amount": 500000,
                                "interval": "monthly",
                            },
                        ],
                    },
                },
            )
            data = response.json()
  /cards/{card_id}/balance:
    parameters:
      - name: card_id
        in: path
        required: true
        schema:
          type: string
        description: Opaque card id, prefixed `ozav_card_`.
    get:
      operationId: getCardBalance
      tags:
        - Cards
      summary: Retrieve a card's spendable balance
      description: |-
        How much this card can spend right now.

        The balance belongs to the **cardholder**, not to the card: every card issued to the same customer draws on the same balance, so two cards for one holder will report the same number.

        `available_amount` is the amount an authorisation will actually clear. It is already net of everything the card has spent, including authorisations that have not yet settled, and it is rounded **down** to the cent, so the figure shown is always spendable.

        In production this reads from the same USDC as `GET /wallets/{wallet_id}/balances`: the card spends the USDC in the cardholder's OZAV-managed wallet, and that route already deducts card spend from USDC. After any wallet movement (a deposit, a send, an off-ramp) this figure can differ from the wallet balance for a few minutes. The wallet figure is what the cardholder owns; this figure is what a purchase clears right now, and the two converge once OZAV reconciles them, usually within minutes. In the sandbox the two are independent: the card balance moves only with `POST /cards/{card_id}/simulate/funding` and simulated purchases.
      x-ozav-entitlement: cards
      x-ozav-scopes:
        - cards:read
      x-ozav-availability: live
      responses:
        "200":
          description: The card's spendable balance.
          content:
            application/json:
              schema:
                type: object
                required:
                  - card_id
                  - available_amount
                  - currency
                  - as_of
                properties:
                  card_id:
                    type: string
                    description: The card this balance was read for.
                    example: ozav_card_01J00000000000000000000000
                  available_amount:
                    type: string
                    description: Decimal string, rounded down to the cent. A string, not a number, so no client turns it into a float.
                    example: "250.50"
                  currency:
                    type: string
                    description: The asset backing the balance.
                    example: USDC
                  as_of:
                    type: string
                    format: date-time
                    description: When the balance was read.
                    example: 2026-09-29T12:00:00.000Z
        "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}/balance \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/balance", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/cards/{card_id}/balance",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/freeze", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/cards/{card_id}/freeze",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/unfreeze", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/cards/{card_id}/unfreeze",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/close", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/cards/{card_id}/close",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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:
              open_session:
                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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "purpose": "card_display"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/secure-display-session", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "purpose": "card_display"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/cards/{card_id}/secure-display-session",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "purpose": "card_display",
                },
            )
            data = 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
        declines. **Sandbox only** - a `live` key gets `404 SIMULATION_NOT_AVAILABLE`, because in
        production the card spends the USDC in the cardholder's OZAV-managed wallet, which arrives by
        deposit or transfer, never through a call that loads the card.

        Without this, a customer's first card is created `active` and declines every purchase with
        `Insufficient balance` forever, so authorisation, spending limits, declines and interchange
        could not be exercised at all.

        The credit lands on the same balance the authorisation debits, and the amount is yours to
        choose - so **declining for insufficient balance stays testable**: fund a small amount and
        simulate a purchase above it.

        The balance belongs to the **customer**, not to the card: funding here also funds that
        customer's other cards.
      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, with at most 2 decimal places.
            example:
              amount_usd: 50
            examples:
              credit:
                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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "amount_usd": 250
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/simulate/funding", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "amount_usd": 250
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/cards/{card_id}/simulate/funding",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "amount_usd": 250,
                },
            )
            data = 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.
        If the card was issued by a card network partner that offers its own test environment, the purchase is submitted there instead and the response has `status: submitted`. That response is not a decision: the authorisation reaches us as it would for a real purchase, so read `GET /cards/{card_id}/authorizations` (and `/transactions`) for the outcome. `is_refund`, `settle: false` and `presentment` are not supported on such a card and return 422 SIMULATION_REFUSED, as does a card whose issuer has no test purchase trigger.
        This route takes no `Idempotency-Key`: every call is a new purchase. A `503 CARD_SIMULATION_NOT_SETTLED` means the purchase was authorised and debited but its settlement was not recorded - do not retry it; read `GET /cards/{card_id}/authorizations` first. `503 CARD_FUNDING_UNAVAILABLE` on a refund moved nothing and can be retried.
      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, with at most 2 decimal places.
                  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
                settle:
                  type: boolean
                  default: true
                  description: "`false` leaves an approved purchase authorised and held, as on the card networks before settlement: `card.authorized` fires, the amount (and any surcharge) is debited, and the hold appears on `/authorizations` with `settled_at: null`. Then settle it with `POST /cards/{card_id}/simulate/settlement` (optionally with a different final amount, such as a tip) or release it with `POST /cards/{card_id}/simulate/reversal`, using the `authorization_ref` this call returns. A refund is never held."
                is_refund:
                  type: boolean
                  default: false
                  description: "Simulate a merchant refund instead of a purchase. It credits the customer's balance and is not an authorization: card status, spending limits and fraud rules do not apply to money coming back. It returns no surcharge. It cannot exceed what this card's settled purchases debited (surcharges not included) and not yet had refunded - above that it is refused with `422 SIMULATION_REFUSED` and `param: amount_usd`. It is listed on `/transactions` with `direction: credit` and 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.

                    Send 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.

                    The 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:
              international:
                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
              domestic:
                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...
                  settled:
                    type: boolean
                    description: "`true` when the purchase settled in this call. `false` on an approval means it is held (`settle: false`)."
                  client_surcharge:
                    $ref: "#/components/schemas/ClientSurcharge"
                  status:
                    type: string
                    enum:
                      - submitted
                    description: "Present only when the purchase was submitted to the issuer's test environment. `approved`, `decline_reason`, `transaction_ref` and `settled` are then absent: the decision arrives through the normal authorisation flow."
                  issuer_result_code:
                    type: string
                    description: The issuer's own code for the submission, opaque. Not the authorisation decision.
                  issuer_reference:
                    type:
                      - string
                      - "null"
                  outcome:
                    type: string
                    description: Where to read the decision.
        "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/purchase \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "amount_usd": 33.12,
                "merchant": {
                  "name": "Ramen Ichiban",
                  "category_code": "5812",
                  "country": "JP"
                },
                "presentment": {
                  "amount_minor": 5000,
                  "currency": "JPY"
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/simulate/purchase", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "amount_usd": 33.12,
                "merchant": {
                  "name": "Ramen Ichiban",
                  "category_code": "5812",
                  "country": "JP"
                },
                "presentment": {
                  "amount_minor": 5000,
                  "currency": "JPY"
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/cards/{card_id}/simulate/purchase",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "amount_usd": 33.12,
                    "merchant": {
                        "name": "Ramen Ichiban",
                        "category_code": "5812",
                        "country": "JP",
                    },
                    "presentment": {
                        "amount_minor": 5000,
                        "currency": "JPY",
                    },
                },
            )
            data = response.json()
  /cards/{card_id}/simulate/settlement:
    post:
      operationId: simulateCardSettlement
      tags:
        - Cards
      summary: Settle a held simulated purchase (sandbox only)
      description: "Settles a purchase that `POST /cards/{card_id}/simulate/purchase` left held (`settle: false`), through the same entry an issuer's settlement uses. Send `amount_usd` to settle a different final amount (a tip, a fuel top-up); the difference against the held amount is adjusted on the balance. `card.settled` fires with the same `transaction_id` as `card.authorized`. Test-environment keys only: a live key receives 404 SIMULATION_NOT_AVAILABLE. A purchase that is already settled or was reversed is refused with `422 SIMULATION_REFUSED`."
      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:
                - authorization_ref
              properties:
                authorization_ref:
                  type: string
                  description: The `authorization_ref` returned by simulate/purchase.
                  example: sim_auth_9k2m...
                amount_usd:
                  type: number
                  description: Final amount in USD. Omit it to settle the held amount. Must be positive, with at most 2 decimal places.
                  example: 57.5
            examples:
              with_tip:
                summary: Settle a held purchase for a different final amount
                value:
                  authorization_ref: sim_auth_9k2m4b8x
                  amount_usd: 57.5
      responses:
        "200":
          description: The settled transaction.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: card_settlement_simulation
                  card_id:
                    type: string
                  authorization_ref:
                    type: string
                  transaction_id:
                    type:
                      - string
                      - "null"
                    example: ozav_ctxn_2e9b3c1a5b7d4e2f9a1c8d3f5e7a9b1c
                  transaction_ref:
                    type: string
                    example: sim_txn_4b8x...
                  final_amount_usd:
                    type: string
                    example: "57.50"
        "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/settlement \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "authorization_ref": "sim_auth_9k2m4b8x",
                "amount_usd": 57.5
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/simulate/settlement", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "authorization_ref": "sim_auth_9k2m4b8x",
                "amount_usd": 57.5
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/cards/{card_id}/simulate/settlement",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "authorization_ref": "sim_auth_9k2m4b8x",
                    "amount_usd": 57.5,
                },
            )
            data = response.json()
  /cards/{card_id}/simulate/reversal:
    post:
      operationId: simulateCardReversal
      tags:
        - Cards
      summary: Reverse a held simulated purchase (sandbox only)
      description: "Releases a purchase that `POST /cards/{card_id}/simulate/purchase` left held (`settle: false`), as when a merchant cancels an authorisation or it expires, through the same entry an issuer's release uses. The held amount and its surcharge return to the balance, the authorisation shows as `reversed` on `/authorizations`, and `card.reversed` fires with the same `transaction_id` as `card.authorized`. Test-environment keys only: a live key receives 404 SIMULATION_NOT_AVAILABLE. A purchase that is already settled or reversed is refused with `422 SIMULATION_REFUSED`."
      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:
                - authorization_ref
              properties:
                authorization_ref:
                  type: string
                  description: The `authorization_ref` returned by simulate/purchase.
                  example: sim_auth_9k2m...
            examples:
              reverse:
                summary: Release a held purchase
                value:
                  authorization_ref: sim_auth_9k2m4b8x
      responses:
        "200":
          description: The reversal.
          content:
            application/json:
              schema:
                type: object
                properties:
                  object:
                    type: string
                    example: card_reversal_simulation
                  card_id:
                    type: string
                  authorization_ref:
                    type: string
                  reversed_amount_usd:
                    type: string
                    example: "49.90"
        "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/reversal \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "authorization_ref": "sim_auth_9k2m4b8x"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/simulate/reversal", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "authorization_ref": "sim_auth_9k2m4b8x"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/cards/{card_id}/simulate/reversal",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "authorization_ref": "sim_auth_9k2m4b8x",
                },
            )
            data = 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.

        **This is how you reconcile card spend.** `card.authorized`, `card.declined`, `card.settled` and `card.refunded` fire as a card is used; book from this route, which lists settled purchases and refunds, and read `/authorizations` only for holds that are still open (`status` `pending` or `authorized`). A released hold raises no event.

        **Reading it incrementally.** Both card activity routes return rows newest first and take only `limit` (up to 100) and `starting_after` - there is no date filter. Rows are ordered by `created_at`, and a row's `status` and `settled_at` can change after it first appears without moving it back to the top. Re-read a recent window (for example the last seven days) on each pass rather than stopping at the first id you already stored, and key your copy by `id`. Poll only cards that still have an open hold (`status` `pending` or `authorized`). A card with no open hold needs no more reads until its next `card.authorized`, `card.settled` or `card.refunded`. Do not treat `reversed` or `declined` as immutable: a late capture turns the same row `settled`, and the row keeps its original `created_at`, which can be weeks old. Make each pass reach back at least to the oldest row you still hold as open (`pending` or `authorized`): a released hold raises no event, so a window shorter than that never sees it close. When a `card.settled` matches nothing in that window, page back until you find it, or re-read back to the oldest `reversed` or `declined` row you still track. Keep all polling well inside your organization's 90 requests per minute; on `429` or `503`, wait `Retry-After` when present and back off otherwise. A purchase OZAV declines is not listed on either route - it is on `GET /cards/{card_id}/declines`; a hold the card processor refuses after approval stays on `/authorizations` as `declined`.

        To 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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/transactions", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/cards/{card_id}/transactions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /cards/{card_id}/authorizations:
    get:
      operationId: listCardAuthorizations
      tags:
        - Cards
      summary: List card authorizations
      description: |-
        Lists the authorization behind each card purchase, newest first - including authorizations that have since settled. `status` says where each one is: the hold is open while it is `pending` or `authorized`. `settled_at` is null on every row that has not settled, open or released.

        A purchase that settled appears here and in `GET /cards/{card_id}/transactions`, so do not add the two lists together - read `item_object` on the page. In the sandbox a simulated purchase settles at once, so every row here is already `settled`. Refunds are not authorizations and are listed only on `/transactions`. A purchase OZAV declines is not listed on either route - it is on `GET /cards/{card_id}/declines`; an authorization OZAV approved and the card processor later declined, reversed or let expire stays here with `status` `declined` or `reversed` and `settled_at` null, unless a late capture settles it. Polling guidance is on `GET /cards/{card_id}/transactions`.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/authorizations", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/cards/{card_id}/authorizations",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /cards/{card_id}/declines:
    get:
      operationId: listCardDeclines
      tags:
        - Cards
      summary: List card declines
      description: |-
        Lists the purchases on this card that OZAV refused, newest first. Such a purchase leaves no hold and appears in neither `/authorizations` nor `/transactions`, so this is where to look when a cardholder asks why the card did not work, including when your endpoint missed the `card.declined` webhook. A hold the card processor refuses after OZAV approved it is not here: it stays on `/authorizations` as `declined`.

        Each row is the same refusal the `card.declined` event announced, and its `id` is that event's id. `fraud_category` is set only when `decline_code` is `fraud_rule`, and is `null` on refusals recorded before the field existed. A card that has never been seen through the v2 API has no declines here.
      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 declines.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CardDeclinePage"
        "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}/declines \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/cards/{card_id}/declines", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/cards/{card_id}/declines",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /card-surcharge-rules:
    get:
      operationId: getCardSurchargeRules
      tags:
        - Cards
      summary: Get your card surcharge rules (sandbox only)
      description: |-
        Returns the active version of your surcharge rules for card purchases: an amount you charge your cardholder on top of each purchase, debited from the same balance at authorization. The surcharge is yours; in the sandbox nothing is paid out, and how it is settled to you in production is defined with the card programme's go-live. `version: null` means you never published rules, and no surcharge is charged.

        **Scope: one set per organization, per environment.** The key selects only the environment (`test` or `live`), so two test keys of the same organization read and write the same set, and `environment` is returned with it. A rule cannot today target a specific customer or card, so two products under one organization cannot be priced differently; separate organizations have separate sets.

        A test key always works. A live key works once your organization is enabled in the live card programme, and live purchases are then charged by the live rules; before that, a live key receives `403 CARD_SURCHARGE_NOT_AVAILABLE`.
      x-ozav-entitlement: cards
      x-ozav-scopes:
        - cards:read
      x-ozav-availability: live
      responses:
        "200":
          description: "The active rules, or `version: null` when none were published."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CardSurchargeRules"
        "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/card-surcharge-rules \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/card-surcharge-rules", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/card-surcharge-rules",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
    put:
      operationId: putCardSurchargeRules
      tags:
        - Cards
      summary: Publish your card surcharge rules (sandbox only)
      description: |
        Publishes a new version of your surcharge rules. The body replaces the
        whole set - send every rule you want to keep. Earlier versions are kept,
        and every purchase records the version that decided its rate
        (`client_surcharge.rule_version`), so your accounting can trace it.

        **Scope: one set per organization, per environment** - publishing
        replaces the set both of your test keys read. A rule cannot today target
        a specific customer or card.


        **Republishing the same set changes nothing.** If the body matches the
        active version field for field, that version is returned and no new one
        is created, so a retry does not add noise to the version trail your
        accounting follows.


        **How a purchase is rated.** Rules are read in order and the first one
        whose `when` matches wins. Inside `when`, every condition must match;
        inside each list, any value matches. If no rule matches,
        `default_rate_bps` applies - unless the issuer did not report the
        purchase currency (`presentment` absent), in which case
        `presentment_missing_rate_bps` applies. A currency condition never
        matches an unknown currency.

        **Merchant names match as literal text**, case-insensitive and with
        repeated spaces collapsed; `*` is a literal character, as in card
        descriptors. `merchant_name_contains_any` is broad and
        catches more than you may expect - `SHOP` also matches `WORKSHOP`. Use
        `merchant_name_starts_with_any` when the descriptor prefix is enough.

        **What the cardholder pays.** The surcharge is `rate_bps` of the
        purchase amount, rounded half-up to the cent, debited together with the
        purchase from the same balance: a balance that covers the purchase but
        not the surcharge declines the purchase. Spending limits count only
        the purchase. The rate is decided once, at authorization; when the
        purchase settles for a different amount, the surcharge is adjusted at
        that same rate, and a reversed or expired authorization returns it.
        A refund the card processor links to its purchase returns the
        surcharge proportionally, at the purchase's rate and never more than
        the purchase charged; the refund row shows it as `client_surcharge`
        and it is taken back from what you are credited. A simulated refund
        (`simulate/purchase` with `is_refund`) is not linked to a purchase and
        returns no surcharge.

        A test key always works. A live key works once your organization is enabled
        in the live card programme, and live purchases are then charged by the live
        rules; before that, a live key receives `403 CARD_SURCHARGE_NOT_AVAILABLE`.
      x-ozav-entitlement: cards
      x-ozav-scopes:
        - cards:write
      x-ozav-availability: live
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CardSurchargeRulesPut"
            examples:
              by_currency_and_merchant:
                summary: A rate for BRL purchases, another for a merchant list, a default for the rest
                value:
                  enabled: true
                  rules:
                    - description: Purchases billed in BRL
                      when:
                        presentment_currency_in:
                          - BRL
                      rate_bps: 150
                    - description: Selected merchants outside BRL
                      when:
                        presentment_currency_not_in:
                          - BRL
                        merchant_name_starts_with_any:
                          - EXAMPLE*ADS
                          - EXAMPLE CLOUD
                      rate_bps: 900
                  default_rate_bps: 400
                  presentment_missing_rate_bps: 400
      responses:
        "200":
          description: The version just published.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CardSurchargeRules"
        "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/card-surcharge-rules \
              -X PUT \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "enabled": true,
                "rules": [
                  {
                    "description": "Purchases billed in BRL",
                    "when": {
                      "presentment_currency_in": [
                        "BRL"
                      ]
                    },
                    "rate_bps": 150
                  },
                  {
                    "description": "Selected merchants outside BRL",
                    "when": {
                      "presentment_currency_not_in": [
                        "BRL"
                      ],
                      "merchant_name_starts_with_any": [
                        "EXAMPLE*ADS",
                        "EXAMPLE CLOUD"
                      ]
                    },
                    "rate_bps": 900
                  }
                ],
                "default_rate_bps": 400,
                "presentment_missing_rate_bps": 400
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/card-surcharge-rules", {
              method: "PUT",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "enabled": true,
                "rules": [
                  {
                    "description": "Purchases billed in BRL",
                    "when": {
                      "presentment_currency_in": [
                        "BRL"
                      ]
                    },
                    "rate_bps": 150
                  },
                  {
                    "description": "Selected merchants outside BRL",
                    "when": {
                      "presentment_currency_not_in": [
                        "BRL"
                      ],
                      "merchant_name_starts_with_any": [
                        "EXAMPLE*ADS",
                        "EXAMPLE CLOUD"
                      ]
                    },
                    "rate_bps": 900
                  }
                ],
                "default_rate_bps": 400,
                "presentment_missing_rate_bps": 400
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.put(
                "https://developers.ozav.io/v2/card-surcharge-rules",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "enabled": True,
                    "rules": [
                        {
                            "description": "Purchases billed in BRL",
                            "when": {
                                "presentment_currency_in": [
                                    "BRL",
                                ],
                            },
                            "rate_bps": 150,
                        },
                        {
                            "description": "Selected merchants outside BRL",
                            "when": {
                                "presentment_currency_not_in": [
                                    "BRL",
                                ],
                                "merchant_name_starts_with_any": [
                                    "EXAMPLE*ADS",
                                    "EXAMPLE CLOUD",
                                ],
                            },
                            "rate_bps": 900,
                        },
                    ],
                    "default_rate_bps": 400,
                    "presentment_missing_rate_bps": 400,
                },
            )
            data = response.json()
  /wallets:
    post:
      operationId: registerSelfCustodyWallet
      tags:
        - Self-custody Wallets
      summary: Register a self-custody wallet the customer already controls
      description: |
        Records an address your user already controls, so OZAV can read what it holds and build unsigned
        transactions for it. Nothing is created: your user holds the key, and the address already exists.

        Send `owner_ref`, your own reference for the wallet's owner (your user id, for example), or
        `customer_id` for an OZAV customer, never both. With `owner_ref`, OZAV stores the address under that
        reference and no personal data.

        - **The address is checked against its network.** An address without the shape of an address on the
          declared network is refused, which catches the common mistake of pasting one from another chain.
          Recording an address does not prove your user holds its key.
        - **One address per owner and environment.** Registering again replaces the previous address, and
          `replaced_previous_address` in the response says so.
        - `custody_model` accepts only `self_custody`. Wallets that OZAV holds come with the customer, not
          from this route.
        - A wallet registered with `owner_ref` serves `POST /wallets/{wallet_id}/transaction-intents`. The
          managed-custody routes answer `404` for its id.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:write
      x-ozav-availability: live
      x-ozav-services: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - custody_model
                - chain
                - address
              properties:
                customer_id:
                  type: string
                  description: The customer this wallet belongs to (`ozav_cus_...`). Send this or `owner_ref`.
                owner_ref:
                  type: string
                  pattern: ^[A-Za-z0-9_.:@-]{1,128}$
                  description: Your own reference for the wallet's owner (no OZAV customer). Send this or `customer_id`.
                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.

                    **`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 it is refused - use a managed-custody wallet on those networks instead.

                    `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
                      - "null"
                    description: The customer (`ozav_cus_...`), or `null` for a wallet registered with `owner_ref`.
                  owner_ref:
                    type: string
                    description: Present only for a wallet registered with `owner_ref`.
                  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.
          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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "custody_model": "self_custody",
                "chain": "base",
                "address": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "custody_model": "self_custody",
                "chain": "base",
                "address": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/wallets",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "customer_id": "ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "custody_model": "self_custody",
                    "chain": "base",
                    "address": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                },
            )
            data = response.json()
    get:
      operationId: listWallets
      tags:
        - Custodial Wallets
      summary: List wallets
      description: |
        Lists wallets with the standard cursor pagination; filter with the
        `customer_id` query parameter. Each wallet reports its `custody_model`.
        Reads are tenant-scoped. Returns a `Page` of `Wallet`.
        Sandbox verification simulation creates a simulated managed wallet
        projection for approved customers when no wallet or self-custody address
        already exists. Live managed wallet provisioning, on-chain addresses,
        and managed sends remain gated. Use `GET /accounts?customer_id=...`
        for customer BRL/Pix account instructions.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/wallets",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /wallets/{wallet_id}:
    get:
      operationId: getWallet
      tags:
        - Self-custody Wallets
      summary: Retrieve a wallet
      description: |
        Returns one wallet by its `ozav_wal_` id, with its `custody_model` and `status`. A wallet registered
        with `owner_ref` comes back with `owner_ref`, `network` and `address` instead of `customer_id`.

        A wallet outside your organization answers `404`. A customer's wallet needs the `custody` product; a
        wallet registered with `owner_ref` needs `self_custody`.
      x-ozav-entitlement: any
      x-ozav-scopes:
        - wallets:read
      x-ozav-availability: live
      x-ozav-services: true
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /wallets/{wallet_id}/deposit-addresses:
    get:
      operationId: listWalletDepositAddresses
      tags:
        - Custodial 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.

        Read `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.

        A 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.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deposit-addresses", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/deposit-addresses",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /wallets/{wallet_id}/balances:
    get:
      operationId: listWalletBalances
      tags:
        - Custodial Wallets
      summary: List balances of a wallet
      description: |
        The wallet's available balance per currency, from OZAV's ledger: posted credits minus posted debits,
        pending debits and card spend, as a decimal string at the currency's precision. It is never more than
        the wallet holds on chain. One balance per currency across networks, so `network` is omitted. This is the balance to reconcile against; what sits at each address on chain is
        `GET /wallets/{wallet_id}/onchain-balances`.

        A self-custody wallet returns an empty `data` array: OZAV holds none of its funds. A wallet outside your
        organization answers `404`. With a `test` key the balance is the sandbox one.
      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: One balance per currency, across networks; `network` is omitted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Balance"
              examples:
                balances:
                  summary: One balance per currency, across networks
                  value:
                    data:
                      - amount: "1500.000000"
                        currency: USDC
                      - amount: "250.500000"
                        currency: USDT
        "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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/balances", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/balances",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /wallets/{wallet_id}/onchain-balances:
    get:
      operationId: listWalletOnchainBalances
      tags:
        - Self-custody Wallets
      summary: List on-chain balances of a wallet, per network
      description: |
        What is at the wallet's address on chain right now, per network and currency.

        - **Registered with `owner_ref`:** OZAV reads the address your user registered, on the network it was
          registered on, for each currency OZAV supports there.
        - **Held by OZAV:** OZAV reads each deposit address of the wallet. This is not the ledger balance of
          `GET /wallets/{wallet_id}/balances`, which is the available balance per currency across networks
          (posted credits minus posted debits minus pending debits): this read does not subtract transfers in
          flight, does not wait for finality, and includes a deposit that arrived and is not credited yet, so
          the two do not sum to the same number. Use it to see what can leave on a given network; use
          `/balances` to reconcile.
        - **Self-custody wallet of an OZAV customer:** OZAV holds no address for it, and `data` is empty.

        For a wallet held by OZAV, only the networks offered to your organization appear, the same set
        `/deposit-addresses` serves (see `x-ozav-network-availability` on `Network`), and a network where
        customers share one deposit address with a memo is left out, because that balance is not
        attributable to one customer. A deposit on a network no longer offered is still credited and shows in
        `/balances`.

        `amount` is `null` when the balance could not be read, never `0`, and `unreadable_reason` says why.
        The other networks in the response are unaffected. A wallet outside your organization answers `404`.
      x-ozav-entitlement: any
      x-ozav-scopes:
        - wallets:read
      x-ozav-availability: live
      x-ozav-services: true
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/onchain-balances", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/onchain-balances",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /wallets/{wallet_id}/transaction-intents:
    post:
      operationId: createTransactionIntent
      tags:
        - Self-custody Wallets
      summary: Build an unsigned transaction for a self-custody wallet
      description: |
        Builds an unsigned transaction for a `self_custody` wallet. Your user signs and broadcasts it; OZAV
        never holds the key and never signs.

        - The token contract comes from OZAV's verified registry, and the amount is converted to the token's
          smallest unit without floating point.
        - What cannot be built exactly is refused with `422` instead of approximated: a network whose
          transaction is not `{to, data, value, chain_id}`, an asset not verified on that network, or an
          amount smaller than one unit of the token.
        - A wallet OZAV holds answers `422 WALLET_NOT_SELF_CUSTODY`. Wallets registered with `owner_ref` are
          accepted.

        Send an `Idempotency-Key` to retry safely: the same key and body return the same transaction intent
        with `Idempotent-Replayed: true`, and on OZAV Services the retry is not charged again.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:write
      x-ozav-availability: live
      x-ozav-services: true
      parameters:
        - name: wallet_id
          in: path
          required: true
          description: The opaque wallet id (`ozav_wal_...`).
          schema:
            type: string
        - $ref: "#/components/parameters/IdempotencyKeyOptional"
      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:
                    - `withdraw` - send funds out to an external destination.
                    - `transfer` - move funds to another address.
                    - `deposit` - rejected: inbound deposits are signed by the external payer, not built here.
                  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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "intent": "transfer",
                "currency": "USDC",
                "amount": "250.000000",
                "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transaction-intents", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "intent": "transfer",
                "currency": "USDC",
                "amount": "250.000000",
                "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transaction-intents",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "intent": "transfer",
                    "currency": "USDC",
                    "amount": "250.000000",
                    "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                },
            )
            data = response.json()
  /approvals:
    get:
      operationId: listApprovals
      tags:
        - Deprecated
      summary: List approvals
      deprecated: true
      description: |
        **Deprecated.** OZAV sets the rules of managed-custody wallets, and no
        send waits on approver keys: this list has no approvals to show. It stays
        available to the integrations that read it until the date in OZAV's
        notice to them.

        The approvals of your organization, newest first. `?status=` filters by
        status.
      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. It has none to show.
          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:
                empty:
                  summary: No approvals
                  value:
                    data: []
                    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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/approvals", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/approvals",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /wallets/{wallet_id}/addresses:
    get:
      operationId: listWalletAddresses
      tags:
        - Deprecated
      summary: List the address book of a wallet
      deprecated: true
      description: |
        **Deprecated.** OZAV sets the destinations of managed-custody wallets.
        This read stays available to the integrations that use it until the date
        in OZAV's notice to them.

        The destinations of this managed wallet with their labels: what is
        `allowed` (leaves without human review), what is still `pending_review`,
        and what was `rejected`. It is the same list as `destinations.allowed` on
        the policy, plus the pending requests.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/addresses",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /wallets/{wallet_id}/policy:
    get:
      operationId: getWalletPolicy
      tags:
        - Deprecated
      summary: Read the policy of a managed wallet
      deprecated: true
      description: |
        **Deprecated.** OZAV sets the rules of managed-custody wallets. This route
        stays available to the integrations that read it until the date in OZAV's
        notice to them.

        What OZAV applies before signing a send from this `managed` wallet, as one
        object: the destinations that leave without human review, the limits in
        force and where they come from, and the limit requests still being decided.
        `approvals.mode` is always `ozav_review`, and `delay` and `cosigner` are
        always off.

        **Read `limits.source` before you read the numbers.** `wallet` means a
        signed per-wallet record is in force; these numbers come from the newest
        one. If more than one is active, OZAV applies the smallest of each
        limit, and the automatic amount never goes above the per-send limit. `organization` and `unconfigured` mean no per-wallet record
        exists yet: the numbers shown are your organization's defaults (or none),
        and until a record is signed every send from this wallet is checked
        against OZAV's house ceiling of USD 100,000 per send, per rolling 24
        hours, and for destinations outside `destinations.allowed`.

        Up to the automatic amount (`limits.auto_approve_below_usd` when `source`
        is `wallet`), a clean send to any destination leaves on its own. Above it,
        only a destination in `destinations.allowed` or an OZAV address receives;
        any other is refused with `422 DESTINATION_NOT_ALLOWLISTED` (the message
        says the amount) and nothing moves. Tenant-scoped: a `wallet_id`
        outside the caller's tenant answers 404 and never leaks existence. A
        `self_custody` wallet has no policy here (its owner holds the key) and
        answers `422 WALLET_NOT_MANAGED`.
      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 limit requests still being decided.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WalletPolicy"
              examples:
                policy:
                  summary: A wallet with a signed limit record and one limit request 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: limits.update
                        status: pending_review
                        requested_at: 2026-09-09T12:00:00Z
                        decided_at: null
                        per_transaction_usd: 50000
                        daily_usd: 500000
                        auto_approve_below_usd: 1000
                    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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/policy", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/policy",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
    patch:
      operationId: proposeWalletPolicyChange
      tags:
        - Deprecated
      summary: Request a limit change for a managed wallet
      deprecated: true
      description: |
        **Deprecated.** OZAV sets the rules of managed-custody wallets. This route
        stays available, for limit requests only, to the integrations that use it
        until the date in OZAV's notice to them.

        `limits` is the only field, and it is a request: it enters review by OZAV
        and nothing changes on this call. It appears at once in `pending_changes`,
        and when decided you receive `wallet_policy.updated`. An approved change
        comes into force minutes later, when its signed record is issued - confirm
        it by reading `limits.valid_until`.

        Missing limit fields inherit the values in force; when nothing is in force
        yet, both `per_transaction_usd` and `daily_usd` are required. A limit
        change needs a provisioned custody vault (`409 POLICY_LIMITS_REQUIRE_VAULT`
        before that). A request carrying `destinations`, `approvals`, `delay` or
        `cosigner` answers `422 WALLET_RULE_SET_BY_OZAV`, with `param` naming the
        field, and nothing in it is applied.

        `Idempotency-Key` is **required**: a request must not be enqueued twice by
        a retry. In the sandbox limits are read-only
        (`422 POLICY_SANDBOX_READ_ONLY`); use a live key.
      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 limit request must be safely retryable.
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WalletPolicyChangeRequest"
            examples:
              raise_daily_limit:
                summary: Raise the daily limit, keeping the other numbers in force
                value:
                  limits:
                    daily_usd: 500000
      responses:
        "202":
          description: |
            The limit request 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.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WalletPolicy"
              examples:
                queued:
                  summary: The limit request is awaiting review; the limits in force are 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: limits.update
                        status: pending_review
                        requested_at: 2026-09-09T12:00:00Z
                        decided_at: null
                        per_transaction_usd: 50000
                        daily_usd: 500000
                        auto_approve_below_usd: 1000
                    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; `POLICY_LIMITS_CHANGE_PENDING` - a limits change for this wallet that you cannot replace is already open (one OZAV opened, or one already approved). Your own pending limits request is replaced by the new one, and no longer shows in `pending_changes`; or `IDEMPOTENCY_KEY_REUSED`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "422":
          description: |
            `WALLET_RULE_SET_BY_OZAV`, `POLICY_LIMITS_INCOMPLETE`, `POLICY_LIMITS_INVALID`, `WALLET_NOT_MANAGED` or `POLICY_SANDBOX_READ_ONLY`. `param` names the field.
          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 \
              -X PATCH \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "limits": {
                  "daily_usd": 500000
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/policy", {
              method: "PATCH",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "limits": {
                  "daily_usd": 500000
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.patch(
                "https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/policy",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "limits": {
                        "daily_usd": 500000,
                    },
                },
            )
            data = response.json()
  /wallets/{wallet_id}/transfers:
    post:
      operationId: createManagedTransfer
      tags:
        - Custodial Wallets
      summary: Send from a managed-custody wallet (OZAV signs)
      description: |
        For `managed` wallets only, where OZAV custodies the key. **OZAV signs and
        broadcasts this send.** It is the inverse of `transaction-intents`, which
        is for `self_custody` wallets, where the client signs.

        This is a real, irreversible money movement, so `Idempotency-Key` is
        **required** - a request without one is rejected rather than risking a
        second broadcast. Treat a timeout as unknown, never as failed: retry with
        the same key, which replays the stored result instead of sending again.

        The send is refused before anything is signed when the wallet's asset does
        not match `currency`, when the destination is not valid for the wallet's
        network, or when the corridor is not enabled for your organization.
        Nothing is broadcast on a refusal.

        **The same move is also available on `POST /transfers`**, with a `wallet`
        source and a `wallet` or `crypto_address` destination. Both routes run
        the same send and apply the same two controls (managed wallets only;
        Travel Rule on any send that leaves custody), so the choice is about which
        resource you want back, not about what happens on-chain:

        - Use **this route** when the send needs a destination `memo`. Networks
          that carry one (Stellar) need it for exchange deposits - an exchange
          credits by memo, and a deposit without one arrives unattributed.
          `POST /transfers` has no `memo` field.
        - Use **`POST /transfers`** when you want a `Transfer` resource, so the
          send appears in `GET /transfers` alongside your on-ramp, off-ramp and
          FX movements and is reconciled through the same status model.
      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.
          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,}$
                  deprecated: true
                  description: "Retired. Client token registration is no longer offered, so no token id exists to send: a request carrying `token_id` answers `404 CLIENT_TOKEN_NOT_FOUND`. Send the assets OZAV supports in this wallet with `currency`."
                  example: ozav_tok_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                nft_collection_id:
                  type: string
                  pattern: ^ozav_nft_[0-9A-Za-z]{16,}$
                  deprecated: true
                  description: "Retired. NFT collection registration is no longer offered, so no collection id exists to send from: a request carrying `nft_collection_id` answers `404 NFT_COLLECTION_NOT_FOUND`."
                  example: ozav_nft_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                nft_token_id:
                  type: string
                  pattern: ^(0|[1-9][0-9]{0,77})$
                  deprecated: true
                  description: Retired with `nft_collection_id`.
                  example: "1234567890123456789012345"
                nft_quantity:
                  type: string
                  pattern: ^[1-9][0-9]{0,29}$
                  deprecated: true
                  description: Retired with `nft_collection_id`.
                  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 OZAV 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
                      - processing
                  amount:
                    $ref: "#/components/schemas/DecimalAmount"
                  currency:
                    $ref: "#/components/schemas/Currency"
                  destination:
                    type: string
                  transaction_hash:
                    type: string
                  settled_at:
                    type:
                      - string
                      - "null"
                    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
        "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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "currency": "USDT",
                "amount": "250.000000",
                "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfers", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "currency": "USDT",
                "amount": "250.000000",
                "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/wallets/ozav_wal_01J8Z9K2M3N4P5Q6R7S8T9U0V1/transfers",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "currency": "USDT",
                    "amount": "250.000000",
                    "destination": "0x1d4c9ae2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
                },
            )
            data = response.json()
  /statements:
    get:
      operationId: listStatements
      tags:
        - Statements
      summary: List service statements
      description: |-
        Your organization's monthly service statements, newest first, up to the last 36 months. Each one is what OZAV charges you, or credits you, for that month: volume, transactions, active users, your share of spread, and the amount due or credited. A statement covers live activity, so a test key gets an empty list.

        The scope `billing:read` is granted to every key created from now on; a key created before it needs to be replaced to read statements.
      x-ozav-entitlement: any
      x-ozav-scopes:
        - billing:read
      x-ozav-availability: live
      responses:
        "200":
          description: The statements.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatementList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/statements \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/statements", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/statements",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /statements/{statement_id}:
    get:
      operationId: getStatement
      tags:
        - Statements
      summary: Get a service statement
      description: One monthly statement with its itemized lines. `statement_id` is the statement's `id` from the list (`ozav_stmt_` followed by the year and month, e.g. `ozav_stmt_202609`).
      x-ozav-entitlement: any
      x-ozav-scopes:
        - billing:read
      x-ozav-availability: live
      parameters:
        - name: statement_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The statement.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatementDetail"
        "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/statements/{statement_id} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/statements/{statement_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/statements/{statement_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        `secret` is returned exactly once, in this response - store it securely,
        as it cannot be retrieved again.

        **Give it either `role` or `scopes` - exactly one.**

        - **`role`** is the shortcut: `admin`, `operator` or `viewer`. Use it to
          separate powers between the people and systems on your side without
          picking 32 scopes by hand. A role is a **subset of what the minting key
          already holds** - never a way to get more - so a key minted with
          `role: admin` by a narrow key is still narrow.
        - **`scopes`** is the explicit list, intersected with the organization's
          entitlements at request time; an empty set is rejected.

        Sending both, or neither, is a `400`: a permission object should not have
        an implicit default, and there is no sensible rule for which one wins.
      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
                    already holds. Mutually exclusive with `scopes`.

                    - `admin` - everything the minting key holds.
                    - `operator` - everything except `keys:write` and
                      `agent_grants:write`, so the key cannot mint or revoke other
                      keys, nor grant an agent mandate. It keeps `keys:read`, so
                      it still sees which keys exist.
                    - `viewer` - read scopes only.

                    A role is a **subset** of the minting key's scopes: it never
                    grants more than the caller already had.

                    The role is stored and echoed back on the key, and a rotated
                    key keeps it. It is a label and a mint-time template - it is
                    **not** part of the permission decision, which stays
                    `entitlements ∩ scopes`.
                environment:
                  type: string
                  description: |
                    Which environment the new key is for - `test` (sandbox) or
                    `live` (production). The prefix of the returned key reflects this.
                  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
                      - lists:read
                      - lists:write
                      - decisions:read
                      - decisions: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
                      - agents:read
                      - agents:write
                      - agent_payments:write
                      - agent_grants:write
                      - billing:read
                  description: |
                    Scopes are intersected with the organization's entitlements at request time; an
                    empty scope set is invalid. This is a **closed set**: a value outside the enum
                    above is **rejected** with `400 INVALID_SCOPE`, and the error lists every valid
                    scope - a typo fails loudly at mint time rather than quietly minting a key with
                    a narrower scope set than you asked for. You can only grant scopes the minting
                    key itself holds (`403 SCOPE_NOT_HELD` otherwise).
            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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "name": "payments operator",
                "environment": "live",
                "role": "operator"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/api-keys", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "name": "payments operator",
                "environment": "live",
                "role": "operator"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/api-keys",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "name": "payments operator",
                    "environment": "live",
                    "role": "operator",
                },
            )
            data = 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
        environment. Secrets are never returned here - a secret is shown only at
        creation and rotation. Live.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/api-keys", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/api-keys",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        fail authentication. Returns `204 No Content`. Irreversible - mint a new
        key to replace a revoked one. Live.
      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 \
              -X DELETE \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/api-keys/ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "DELETE",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.delete(
                "https://developers.ozav.io/v2/api-keys/ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        remains valid for the overlap window (default 24 hours, maximum 7
        days), then is revoked automatically. Live.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "overlap_hours": 48
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/api-keys/ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1/rotate", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "overlap_hours": 48
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/api-keys/ozav_key_01J8Z9K2M3N4P5Q6R7S8T9U0V1/rotate",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "overlap_hours": 48,
                },
            )
            data = 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
        subscribed `enabled_events`. The signing `secret` (`ozav_whsec_`) is
        returned exactly once, in this response - store it securely; it is used to
        verify delivery signatures and cannot be retrieved again.

        The hostname is checked at registration. A host that does not resolve is
        refused with `422 WEBHOOK_URL_HOST_NOT_FOUND`, because a typo there fails
        silently forever: deliveries never reach anything and the only trace is a
        null `response_status` in `GET /webhook-endpoints/{id}/deliveries`. Only a
        non-existent hostname is refused - an endpoint behind a firewall, not yet
        deployed, or slow to answer registers normally.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "url": "https://api.acme.com/ozav/webhooks",
                "enabled_events": [
                  "customer.created",
                  "screening.completed"
                ],
                "description": "production"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/webhook-endpoints", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "url": "https://api.acme.com/ozav/webhooks",
                "enabled_events": [
                  "customer.created",
                  "screening.completed"
                ],
                "description": "production"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/webhook-endpoints",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "url": "https://api.acme.com/ozav/webhooks",
                    "enabled_events": [
                        "customer.created",
                        "screening.completed",
                    ],
                    "description": "production",
                },
            )
            data = 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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/webhook-endpoints", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/webhook-endpoints",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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:
              change_events:
                summary: Subscribe to a different set of events
                value:
                  enabled_events:
                    - transfer.completed
                    - transfer.failed
                    - deposit.credited
              disable:
                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} \
              -X PATCH \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "enabled_events": [
                  "transfer.completed",
                  "transfer.failed",
                  "deposit.credited"
                ]
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}", {
              method: "PATCH",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "enabled_events": [
                  "transfer.completed",
                  "transfer.failed",
                  "deposit.credited"
                ]
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.patch(
                "https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "enabled_events": [
                        "transfer.completed",
                        "transfer.failed",
                        "deposit.credited",
                    ],
                },
            )
            data = 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} \
              -X DELETE \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}", {
              method: "DELETE",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.delete(
                "https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        stops signing immediately, so update your verifier before rotating.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/rotate-secret", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/rotate-secret",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        `webhook.test` is not in `enabled_events`. The response is the queued
        delivery row; delivery itself is asynchronous and retried by OZAV.
      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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/test", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/test",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        are truncated previews; endpoint secrets, secret hashes, Vault refs, and
        internal ids 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: 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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/deliveries", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/webhook-endpoints/{webhook_endpoint_id}/deliveries",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        log. Each item is the same canonical envelope a webhook delivers, so you
        can reconcile or backfill a missed delivery without a webhook endpoint.
        Client-scoped; keyset-paginated.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/events", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/events",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        delivers. A key finds only the events of its own environment, like
        `GET /events`: an event of the other environment answers `404`.
      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} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/events/{event_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/events/{event_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /yield/realized-gains:
    get:
      operationId: listRealizedGains
      tags:
        - Yield
      summary: Realized gain per month
      description: |
        The gain realized by redemptions, per month and per position, as input for a tax computation. It is
        not a tax filing: there is no fiat conversion (`currency` is always `asset_minor_units`) and no filing
        format.

        - Every amount is a decimal integer string in the asset's smallest unit, so it keeps its precision
          above 2^53.
        - The month is the month of the block, not of when OZAV recorded the movement.
        - Each position is computed on its own, so a redemption in one never consumes the cost basis of
          another.
      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:
                two_months:
                  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:
                unavailable:
                  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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/realized-gains", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/yield/realized-gains",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /yield/strategies:
    get:
      operationId: listYieldStrategies
      tags:
        - Yield
      summary: List yield strategies
      description: |
        The yield strategies open to your organization: asset, network and rate. The catalogue is the same
        for every organization with access.

        - `apy_bps` is the indicative rate, refreshed off chain. It is `null` when there is no recent figure,
          which is not the same as zero.
        - `realized_apy_7d` and `realized_apy_30d` are measured from the strategy's share price, recorded
          hourly. When a realized figure cannot be stated, `known` is `false` and `reason` says why:
          `no_data`, `window_not_covered` (the series started inside the window), `window_too_short` (under
          12 hours between readings), `price_went_backwards` (the share price fell) or `unusable_price`. When
          `known` is `true`, `observed_hours` is the period actually measured.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/strategies", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/yield/strategies",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /yield/wallets/{address}/positions:
    get:
      operationId: listYieldPositionsByAddress
      tags:
        - Yield
      summary: List yield positions for a wallet you control
      description: |
        The yield positions held by a wallet address you supply, for a holder who keeps their own key. The
        address comes from the path, because a balance on chain is public; the organization making the call
        always comes from your API key.

        The address is read only on the networks of its own family (an EVM address is not queried on Solana),
        so `unreadable_chains` and `complete` only reflect networks that apply.

        `cost_basis` is `null` here: the deposit did not pass through OZAV, so there is no record of what it
        cost. These positions report `accounting: "untracked"` with the reason. What a position is worth today
        is read from the chain.
      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
            address matching neither is refused with `WALLET_ADDRESS_INVALID`
            rather than searched for, because "not found" and "mistyped" read
            the same and mean opposite things.
      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
                            through OZAV, so there is no book. `null` is the
                            absence of the number, never zero, which would be a
                            claim that nothing was paid.
                        unrealized:
                          type:
                            - string
                            - "null"
                        realized:
                          type:
                            - string
                            - "null"
                        accounting:
                          type: string
                          description: |
                            `untracked` on this route, with the reason attached.
                        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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/wallets/{address}/positions", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/yield/wallets/{address}/positions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /yield/applications:
    post:
      operationId: createYieldApplication
      tags:
        - Yield
      summary: Apply custodied balance to a yield strategy
      description: |
        Instructs OZAV to apply your organization's custodied balance to a strategy. OZAV signs with its own
        key, under a two-party quorum. This is the custody counterpart of `POST /yield/deposits/build`, which
        returns unsigned transactions for a wallet you hold; the two take different scopes (`yield:apply`
        here, `yield:write` there).

        **Closed pilot.** OZAV grants `yield:apply` per organization; asking for it when minting a key answers
        `403 SCOPE_NOT_HELD` unless the key you mint with already holds it. With the scope, an application is still refused with
        `MANAGED_YIELD_NOT_ENABLED` while the product is off for the environment, and with
        `MANAGED_YIELD_CLIENT_NOT_ENABLED` while it is off for your organization. Both arrive as the
        application's `refused` state, with the cause in `detail`.

        **Answers `202`.** Signing waits on a second approval, so the response carries an `id`, and the state
        is read from `GET /yield/applications/{application_id}`.

        **One application in flight per strategy.** A second call while one is `pending` or `running` answers
        `202` with the existing application and `duplicate: true`. The lock is shared with redemptions: while a
        redemption from the strategy is in flight, an application answers `409 YIELD_REDEMPTION_IN_FLIGHT`.

        This route checks that the strategy exists and that the amount is positive. Authorisation and
        exposure limits are checked when the application runs; a refusal there shows up as
        `status: refused`, with the cause in `detail`.
      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
                    string. A JSON number would lose precision above 2^53.
            examples:
              apply:
                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
                      strategy and `data` is that one, not a new one. `data.amount`
                      is that order's amount and may differ from the one just sent:
                      the request was accepted as a no-op, not as a new instruction.
        "400":
          $ref: "#/components/responses/BadRequest"
        "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/yield/applications \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "strategy_id": "base-usdc-prime",
                "amount": "100000000"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/applications", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "strategy_id": "base-usdc-prime",
                "amount": "100000000"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/yield/applications",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "strategy_id": "base-usdc-prime",
                    "amount": "100000000",
                },
            )
            data = 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`:

        - `pending`: recorded, waiting to run;
        - `running`: being executed;
        - `applied`: confirmed on chain;
        - `refused`: declined by a check, and no money moved;
        - `failed`: attempted and not completed.

        `detail` carries the cause for `refused` and `failed`, and `tx_hash` is present only when a
        transaction existed. An application of another organization answers `404`.
      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} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/applications/{application_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/yield/applications/{application_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /yield/redemptions:
    post:
      operationId: createYieldRedemption
      tags:
        - Yield
      summary: Redeem custodied balance from a yield strategy
      description: |
        Instructs OZAV to take your organization's custodied balance back out of a strategy, onto your
        custody address. OZAV signs with its own key, under the same two-party quorum as
        `POST /yield/applications`, and the same scope (`yield:apply`) authorizes both directions.

        **Behind a switch that starts off.** While redemptions are not enabled for the environment, this
        route answers `503 MANAGED_YIELD_REDEEM_NOT_ENABLED` and does nothing.

        **A redemption is always allowed.** Turning managed yield off, for the environment or for your
        organization, stops new applications only. It never blocks taking the balance out, and it never
        refuses a redemption already queued.

        **Send `amount` or `all: true`, never both.** `amount` is in the asset's smallest unit, as a decimal
        integer string, and redeems that value. `all: true` redeems every share you hold in the strategy
        when the order is accepted, so value earned up to the moment it runs comes out with it.

        **Checked against your position before it is recorded.** An `amount` above the position answers
        `409 YIELD_REDEMPTION_EXCEEDS_POSITION`. Above what the strategy releases right now (see
        `redemption` on `GET /yield/positions`) it answers `409 WITHDRAW_EXCEEDS_AVAILABLE`; `all: true`
        needs the whole position to be releasable. To leave a strategy completely, send `all: true`: an
        `amount` equal to the displayed balance can be refused by the network, which rounds the shares a
        withdrawal burns up and the displayed balance down.

        **`idempotency_key` is required.** The same key with the same body returns the same redemption
        (`202`), whatever its state; with a different body it answers `409 IDEMPOTENCY_KEY_REUSED`. Use a new
        key for a new redemption.

        **One order in flight per strategy, in either direction.** A second redemption while one is
        `pending`, `running` or `indeterminate` answers `202` with that one and `duplicate: true`. While an
        application to the strategy is in flight, a redemption answers `409 YIELD_APPLICATION_IN_FLIGHT`.

        **Answers `202`.** Signing waits on a second approval; read the state from
        `GET /yield/redemptions/{redemption_id}`. The realized gain of the redemption appears in
        `GET /yield/realized-gains` once the transaction is recorded.
      x-ozav-entitlement: custody
      x-ozav-scopes:
        - yield:apply
      x-ozav-availability: live
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - idempotency_key
                - strategy_id
              properties:
                idempotency_key:
                  type: string
                  minLength: 1
                  maxLength: 200
                  pattern: ^[A-Za-z0-9._:-]+$
                  description: A new key for each new redemption; the same key to repeat one that did not answer.
                strategy_id:
                  type: string
                  description: The strategy to redeem from, as listed by `GET /yield/strategies`.
                amount:
                  type: string
                  description: |
                    Amount in the asset's smallest unit, as a decimal integer
                    string. A JSON number would lose precision above 2^53. Omit when `all` is true.
                all:
                  type: boolean
                  description: Redeem every share held in the strategy. Omit `amount` when true.
            examples:
              redeem:
                summary: Redeem 500 USDC (smallest unit) from a strategy
                value:
                  idempotency_key: redeem-2026-10-16-001
                  strategy_id: base-usdc-prime
                  amount: "500000000"
              redeemAll:
                summary: Redeem the whole position
                value:
                  idempotency_key: redeem-2026-10-16-002
                  strategy_id: base-usdc-prime
                  all: true
      responses:
        "202":
          description: Accepted. The redemption is recorded; read its state by id.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/YieldRedemptionOrder"
                  duplicate:
                    type: boolean
                    description: |
                      True when a redemption was already in flight for this
                      strategy and `data` is that one, not a new one. The request
                      was accepted as a no-op, not as a new instruction.
        "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"
        "503":
          description: |
            `MANAGED_YIELD_REDEEM_NOT_ENABLED` - redemptions are not enabled here; nothing was done.
            `YIELD_POSITION_UNREADABLE` or `WITHDRAW_LIMIT_UNREADABLE` - transient; retry with the same
            `idempotency_key`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/yield/redemptions \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "redeem-2026-10-16-001",
                "strategy_id": "base-usdc-prime",
                "amount": "500000000"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/redemptions", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "redeem-2026-10-16-001",
                "strategy_id": "base-usdc-prime",
                "amount": "500000000"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/yield/redemptions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "redeem-2026-10-16-001",
                    "strategy_id": "base-usdc-prime",
                    "amount": "500000000",
                },
            )
            data = response.json()
  /yield/redemptions/{redemption_id}:
    get:
      operationId: getYieldRedemption
      tags:
        - Yield
      summary: Read the state of a yield redemption
      description: |
        The state of a redemption created by `POST /yield/redemptions`:

        - `pending`: recorded, waiting to run;
        - `running`: being executed;
        - `redeemed`: confirmed on chain, and the balance is back on your custody address;
        - `refused`: declined by a check, and no money moved;
        - `failed`: attempted and nothing moved;
        - `indeterminate`: stopped without knowing whether the money moved. Do not order it again.

        `detail` carries the cause, and `tx_hash` is present whenever a transaction is known. A redemption
        of another organization answers `404`. Readable while redemptions are switched off.
      x-ozav-entitlement: custody
      x-ozav-scopes:
        - yield:read
      x-ozav-availability: live
      parameters:
        - name: redemption_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The redemption.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/YieldRedemptionOrder"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/yield/redemptions/{redemption_id} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/redemptions/{redemption_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/yield/redemptions/{redemption_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /yield/positions:
    get:
      operationId: listYieldPositions
      tags:
        - Yield
      summary: List yield positions
      description: |
        Your organization's yield positions, in the wallet OZAV holds for you or in the self-custody wallet
        you registered, with the amount applied, the acquisition cost and the accrued gain. The owner comes
        from your API key.

        `unreadable_chains` lists networks that could not be read. An empty list with entries in
        `unreadable_chains` is not "holding nothing": check `complete` before treating the result as the full
        picture.

        `redemption` says what can leave a position right now, which can differ from `balance` (what it is
        worth) when the market is fully used:

        - `available`: `available_to_withdraw` can be redeemed;
        - `held`: the balance is there and still earning, and nothing can be redeemed right now (queue, pause
          or illiquidity);
        - `unreadable`: the limit could not be read, and `available_to_withdraw` is `null`, never `"0"`.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/positions", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/yield/positions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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 strategy's cap and the network cost of each on-chain
        step. Nothing is written and nothing leaves any wallet; it is a `POST` because it carries a body. The
        owner comes from your API key.

        There is no fixed minimum. Send an amount and read the refusal:

        - `AMOUNT_IN_TOO_HIGH`: above the cap the strategy accepts right now, read from the contract.
        - `AMOUNT_IN_TOO_LOW`: the network cost is not less than the amount, so applying would lose value.
          This floor rises when the network gets more expensive.

        `network_cost` sums every step: applying is usually two transactions, an allowance and then the
        deposit. If any step cannot be measured, the total is withheld and `network_cost_unknown_reason`
        names the cause. `network_cost` is what the network charges, not a charge by OZAV.
      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
                    float: floats lose precision in money silently.
                  pattern: ^[0-9]{1,32}$
                  example: "100000000"
            examples:
              preview:
                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
                      is. A null cost with no reason would not distinguish "the
                      network charges nothing" from "it could not be measured".
              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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "strategy_id": "base-usdc-prime",
                "amount": "100000000"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/preview", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "strategy_id": "base-usdc-prime",
                "amount": "100000000"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/yield/preview",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "strategy_id": "base-usdc-prime",
                    "amount": "100000000",
                },
            )
            data = response.json()
  /yield/deposits/build:
    post:
      operationId: buildYieldDeposit
      tags:
        - Yield
      summary: Build an unsigned yield application
      description: |
        Returns the ordered, unsigned transactions that apply funds to a strategy, for your organization's own
        wallet to sign and broadcast in the order returned (the deposit needs the allowance from the step before it).
        OZAV neither signs nor broadcasts: `signed_by` says so, and `signer` names the wallet the transactions
        were built for, to check before signing.

        - **Self-custody only.** A client under managed custody holds no key, and the request is refused with
          `CUSTODY_MODE_NOT_SELF_CUSTODY`.
        - **The registered wallet only.** If `address` is sent, it must be the wallet already registered for
          your organization on that network; any other address answers `403 WALLET_ADDRESS_NOT_REGISTERED`,
          even when it is valid. Omit `address` to use the registered wallet. Register it first with
          `POST /wallets`. There is one registered address per organization, on any network: registering
          again replaces it, and a build on the old network then answers `409 NO_WALLET_ON_CHAIN`. OZAV tracks
          positions by the addresses it knows, so an unregistered address would hold a position outside its
          records. `GET /yield/wallets/{address}/positions` still reads any address.
        - The strategy's contract address appears in `to`, because your user signs a transaction to it. The
          protocol's name is not returned.

        **Closed pilot, two checks in series.** While building is off for the environment the route answers
        `503 YIELD_BUILD_NOT_ENABLED`. Once it is on, an organization not enrolled answers
        `403 YIELD_BUILD_NOT_IN_PILOT`. If enrolment cannot be read, it answers
        `503 YIELD_PILOT_STATE_UNREADABLE`: retry, rather than ask for access.
      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:
              build_deposit:
                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
                      the authenticated client. Check it against the wallet about
                      to sign.
                  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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "strategy_id": "base-usdc-prime",
                "amount": "100000000"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/deposits/build", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "strategy_id": "base-usdc-prime",
                "amount": "100000000"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/yield/deposits/build",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "strategy_id": "base-usdc-prime",
                    "amount": "100000000",
                },
            )
            data = 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 your organization's own wallet to
        sign and broadcast. One transaction: burning your own shares needs no allowance. OZAV neither signs nor
        broadcasts: `signed_by` says so, and `signer` names the wallet the transaction was built for.

        - **Denominated in the asset, not in shares**, so no conversion rounds against the holder.
        - **Built only if the strategy will release it.** OZAV reads how much can leave right now before
          building. `WITHDRAW_EXCEEDS_AVAILABLE`: the balance is there and still earning, and less can leave
          right now. `WITHDRAW_LIMIT_UNREADABLE`: the limit could not be read.
        - **Self-custody only.** A client under managed custody is refused with
          `CUSTODY_MODE_NOT_SELF_CUSTODY`.
        - **The registered wallet only.** If `address` is sent, it must be the wallet already registered for
          your organization on that network; any other answers `403 WALLET_ADDRESS_NOT_REGISTERED`, because
          the shares are held by the registered wallet. There is one registered address per organization, on
          any network: registering again with `POST /wallets` replaces it.
        - The strategy's contract address appears in `to`; the protocol's name is not returned.

        **Closed pilot, two checks in series:** `503 YIELD_BUILD_NOT_ENABLED` while building is off for the
        environment, `403 YIELD_BUILD_NOT_IN_PILOT` for an organization not enrolled, and
        `503 YIELD_PILOT_STATE_UNREADABLE` when enrolment cannot be read.
      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:
              build_withdrawal:
                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
                      the authenticated client. Check it against the wallet about
                      to sign.
                  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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "strategy_id": "base-usdc-prime",
                "amount": "100000000"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/yield/withdrawals/build", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "strategy_id": "base-usdc-prime",
                "amount": "100000000"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/yield/withdrawals/build",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "strategy_id": "base-usdc-prime",
                    "amount": "100000000",
                },
            )
            data = response.json()
  /network-transfers:
    post:
      operationId: createNetworkTransfer
      tags:
        - Network Transfers
      summary: Move your own USDC to another network
      description: |
        **Not callable** (`coming_soon`). No API key can be issued with the `network_transfers:write` scope
        this route requires, so it answers `403 INSUFFICIENT_SCOPE` for every key. The contract below shows
        the request and the response.

        Moves USDC you already hold from one network to another, in the same wallet. It is not a payment:
        origin and destination are the same holder.

        - **Answers `202`.** The transfer burns on the origin network under a two-party signing quorum, waits
          for attestation and then mints on the destination, which takes minutes. The response carries an
          `id`; read the state from `GET /network-transfers/{transfer_id}`.
        - **One transfer in flight per source network.** A second request while one is in flight answers
          `202` with the transfer already running and `duplicate: true`.
        - **The holder comes from your key**, never from the body.
        - **The amount** is a decimal integer string in the asset's smallest unit.
      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_to_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}`.
            `duplicate` is true when a transfer was already in flight for that
            source network; the transfer returned is that one.
          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
            gate and it is not self-service: the scope is outside the bootstrap,
            so `POST /api-keys` cannot grant it. OZAV grants it per account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "409":
          description: |
            `NETWORK_TRANSFER_PRICE_NOT_CONFIGURED` : no active pricing row applies
            to this account, so the transfer is refused rather than priced at zero.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: |
            `NETWORK_TRANSFER_PRICE_UNREADABLE`: pricing could not be read, so the
            transfer cannot be accepted. Retry shortly.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/network-transfers \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "source_chain": "ethereum",
                "destination_chain": "base",
                "asset": "USDC",
                "amount": "5000000"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/network-transfers", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "source_chain": "ethereum",
                "destination_chain": "base",
                "asset": "USDC",
                "amount": "5000000"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/network-transfers",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "source_chain": "ethereum",
                    "destination_chain": "base",
                    "asset": "USDC",
                    "amount": "5000000",
                },
            )
            data = response.json()
  /network-transfers/{transfer_id}:
    get:
      operationId: getNetworkTransfer
      tags:
        - Network Transfers
      summary: Read the state of a network transfer
      description: |
        **Reads a transfer; creating one is not callable** (`coming_soon`). This route returns the state of a
        network transfer that exists; `POST /network-transfers` answers `403 INSUFFICIENT_SCOPE` for every key.

        A transfer of another holder answers `404`. `in_progress` covers everything between leaving the origin
        network and arriving at the destination. While it is in progress, do not retry: a second request would
        move the amount again.
      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 \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/network-transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/network-transfers/ozav_tr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = 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
        sources available and returning the best net rate.

        Read-only. It is a POST because it carries a body (the pair, the amount,
        the tolerance), not because it writes. Nothing leaves any wallet.

        The quote does not execute, and `executable` says so rather than leaving
        it to be assumed. There is no route that accepts a quote: moving value
        needs an execution gate that no source currently passes. The absence of
        that route is the gate, not an omission.

        The wallet that receives the outgoing asset is derived from the
        authenticated client. The body carries the pair and the amount, never a
        destination: accepting one would let a caller price, and later execute, a
        swap whose proceeds land somewhere else.

        `slippage_bps` is required and has no silent default. A hidden default
        would decide, on the client's behalf, how much they accept losing.

        `min_amount_out` travels beside `amount_out`, never instead of it. Showing
        only the expected output would make the tolerance disappear from the
        answer, and the client would compare an optimistic figure against an
        already-discounted one elsewhere.

        Not every network carries a pair. A swap needs both assets settled on
        the same network, and some networks carry only one of them. A pair no
        source serves answers a named error, never a zero or an empty quote, so a
        pair that cannot be served stays distinguishable from a bad price.

        `quotes_compared` and `sources_unavailable` are counts, never lists. That a
        comparison happened is worth knowing (the best of three is a different
        answer from the only one that replied), and the number says it without
        naming who.
      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_to_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
                      chosen from one of three is a different answer from one
                      chosen from three of three.
              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 \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "chain": "ethereum",
                "asset_in": "USDT",
                "asset_out": "USDC",
                "amount_in": "100000000",
                "slippage_bps": 50
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/swap/quote", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "chain": "ethereum",
                "asset_in": "USDT",
                "asset_out": "USDC",
                "amount_in": "100000000",
                "slippage_bps": 50
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/swap/quote",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "chain": "ethereum",
                    "asset_in": "USDT",
                    "asset_out": "USDC",
                    "amount_in": "100000000",
                    "slippage_bps": 50,
                },
            )
            data = response.json()
  /vault-roots:
    post:
      operationId: createVaultRoot
      tags:
        - Vaults
      summary: Register the administrator keys of your vaults
      description: |
        Closed (`coming_soon`): this call answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the platform.

        Registers the Ed25519 public keys that administer your vaults and the number of them that must sign
        a grant or a vault. The same keys and quorum, in any order, return the existing root with `200`.
        Fields the API does not know are ignored and not stored.
        Weak Ed25519 keys (small order or a torsion component) are refused.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:write
      x-ozav-availability: coming_soon
      x-ozav-services: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VaultRootRequest"
            examples:
              default:
                summary: Request body
                value:
                  administrators:
                    - 2012cb90ca60e8e5d8daf66e2272d2233e0486d557e8c66141ed8920177d7eb7
                    - c050c5637a44fa8629fff3cccce2300cb362a63d99d95fc54145266f4332445a
                  threshold: 2
      responses:
        "200":
          description: The same root already existed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultRoot"
        "201":
          description: The root was created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultRoot"
        "400":
          $ref: "#/components/responses/BadRequest"
        "503":
          $ref: "#/components/responses/VaultsNotEnabled"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/vault-roots \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "administrators": [
                  "2012cb90ca60e8e5d8daf66e2272d2233e0486d557e8c66141ed8920177d7eb7",
                  "c050c5637a44fa8629fff3cccce2300cb362a63d99d95fc54145266f4332445a"
                ],
                "threshold": 2
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/vault-roots", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "administrators": [
                  "2012cb90ca60e8e5d8daf66e2272d2233e0486d557e8c66141ed8920177d7eb7",
                  "c050c5637a44fa8629fff3cccce2300cb362a63d99d95fc54145266f4332445a"
                ],
                "threshold": 2
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/vault-roots",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "administrators": [
                        "2012cb90ca60e8e5d8daf66e2272d2233e0486d557e8c66141ed8920177d7eb7",
                        "c050c5637a44fa8629fff3cccce2300cb362a63d99d95fc54145266f4332445a",
                    ],
                    "threshold": 2,
                },
            )
            data = response.json()
  /vault-roots/{root_id}/grants:
    post:
      operationId: createVaultGrant
      tags:
        - Vaults
      summary: Store a grant signed by your administrators
      description: |
        Closed (`coming_soon`): this call answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the platform.

        A grant says who may approve, how many must, the cap of an operator and when it expires. Your
        administrators sign it. The API verifies the signatures against the root before it stores the grant. A
        signature from a key that is not an administrator, or one that repeats, is refused with the index of the
        signature in `signatures`; a signature that does not verify counts as missing and ends as
        `GRANT_BELOW_THRESHOLD`. The enclave repeats every check.

        - `threshold` approvals are needed to release a transaction under this grant.
        - A grant is valid for **every** vault of its root: a vault is as strong as the weakest grant of its root.
        - Fields the API does not know are ignored and not stored.
        - An expired grant is refused with `422 GRANT_ALREADY_EXPIRED`. Fewer valid administrator signatures than
          the root's quorum is `422 GRANT_BELOW_THRESHOLD`.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:write
      x-ozav-availability: coming_soon
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/RootId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VaultGrantRequest"
            examples:
              default:
                summary: A 2-of-2 grant with an Ed25519 and a P-256 approver
                value:
                  approvers:
                    - type: 2
                      public: af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d
                      role: 2
                    - type: 3
                      public: 045b36890dacbd7c9a96bb74a1ee28b3d2d75b72e09a20ef25cf8e6fd8a9f0350d0e14bed8d4682a34d83538bdff5b96e89a6666ec0db5745d02fa1210072df75a
                      role: 2
                  threshold: 2
                  operator_cap: "1000000"
                  cap_asset:
                    family: evm
                    network: 8453
                    id: 833589fcd6edb6e08f4c7c32d4f71b54bda02913
                  valid_until: 2026-12-31T23:59:59.000Z
                  signatures:
                    - public: a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1
                      signature: e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5
                    - public: b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2
                      signature: e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5
      responses:
        "200":
          description: The same grant already existed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultGrant"
        "201":
          description: The grant was stored.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultGrant"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/VaultsNotEnabled"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/vault-roots/{root_id}/grants \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "approvers": [
                  {
                    "type": 2,
                    "public": "af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d",
                    "role": 2
                  },
                  {
                    "type": 3,
                    "public": "045b36890dacbd7c9a96bb74a1ee28b3d2d75b72e09a20ef25cf8e6fd8a9f0350d0e14bed8d4682a34d83538bdff5b96e89a6666ec0db5745d02fa1210072df75a",
                    "role": 2
                  }
                ],
                "threshold": 2,
                "operator_cap": "1000000",
                "cap_asset": {
                  "family": "evm",
                  "network": 8453,
                  "id": "833589fcd6edb6e08f4c7c32d4f71b54bda02913"
                },
                "valid_until": "2026-12-31T23:59:59.000Z",
                "signatures": [
                  {
                    "public": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
                    "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                  },
                  {
                    "public": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
                    "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                  }
                ]
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/vault-roots/{root_id}/grants", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "approvers": [
                  {
                    "type": 2,
                    "public": "af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d",
                    "role": 2
                  },
                  {
                    "type": 3,
                    "public": "045b36890dacbd7c9a96bb74a1ee28b3d2d75b72e09a20ef25cf8e6fd8a9f0350d0e14bed8d4682a34d83538bdff5b96e89a6666ec0db5745d02fa1210072df75a",
                    "role": 2
                  }
                ],
                "threshold": 2,
                "operator_cap": "1000000",
                "cap_asset": {
                  "family": "evm",
                  "network": 8453,
                  "id": "833589fcd6edb6e08f4c7c32d4f71b54bda02913"
                },
                "valid_until": "2026-12-31T23:59:59.000Z",
                "signatures": [
                  {
                    "public": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
                    "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                  },
                  {
                    "public": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
                    "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                  }
                ]
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/vault-roots/{root_id}/grants",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "approvers": [
                        {
                            "type": 2,
                            "public": "af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d",
                            "role": 2,
                        },
                        {
                            "type": 3,
                            "public": "045b36890dacbd7c9a96bb74a1ee28b3d2d75b72e09a20ef25cf8e6fd8a9f0350d0e14bed8d4682a34d83538bdff5b96e89a6666ec0db5745d02fa1210072df75a",
                            "role": 2,
                        },
                    ],
                    "threshold": 2,
                    "operator_cap": "1000000",
                    "cap_asset": {
                        "family": "evm",
                        "network": 8453,
                        "id": "833589fcd6edb6e08f4c7c32d4f71b54bda02913",
                    },
                    "valid_until": "2026-12-31T23:59:59.000Z",
                    "signatures": [
                        {
                            "public": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
                            "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
                        },
                        {
                            "public": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
                            "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
                        },
                    ],
                },
            )
            data = response.json()
  /vault-roots/{root_id}/grants/{grant_id}/revoke:
    post:
      operationId: revokeVaultGrant
      tags:
        - Vaults
      summary: Revoke a grant
      description: |
        Closed (`coming_soon`): this call answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the platform.

        A revoked grant authorizes no new transaction and no new approval (`409 GRANT_REVOKED`). Transactions of the
        grant that still wait for approval, and have no signature under way, are rejected with `GRANT_REVOKED`; a
        signature that was already queued is cancelled. Repeating the call returns the original `revoked_at`.

        - `enforcement: "api"`: the API stops relaying. The enclave is not told, and an approval you already signed
          is still valid there until its own deadline. Revoking inside the vault, on the spot, needs durable memory
          in the enclave and is not built yet.
        - A signature the host already claimed is not stopped; it expires with the 15-minute limit of a stuck job.
        - It only removes power, so your API key is enough. To authorize again, sign a new grant with different content (for example another `valid_until`): the same content returns the revoked grant. A revocation is
          not undone.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:write
      x-ozav-availability: coming_soon
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/RootId"
        - name: grant_id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: The grant is revoked.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  object:
                    type: string
                    enum:
                      - vault_grant
                  root_id:
                    type: string
                  revoked_at:
                    type: string
                    format: date-time
                  rejected_transactions:
                    type: integer
                  enforcement:
                    type: string
                    enum:
                      - api
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/VaultsNotEnabled"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/vault-roots/{root_id}/grants/{grant_id}/revoke \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/vault-roots/{root_id}/grants/{grant_id}/revoke", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/vault-roots/{root_id}/grants/{grant_id}/revoke",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /vaults:
    post:
      operationId: createVault
      tags:
        - Vaults
      summary: Create a vault from a root and its rules
      description: |
        Closed (`coming_soon`): this call answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the platform.

        Creates a vault. The call answers `202`: the enclave creates the wallet asynchronously, so the vault starts
        `pending`. Poll `GET /vaults/{vault_id}` until it is `active` (or `failed`, with a `failure_code`).

        - `rules` is required and lists at least one destination: the API never creates an open vault.
        - `share_2_destination` and `share_3_destination` are the DER public keys that receive key shares 2 and 3,
          sealed. They must be different keys, both yours.
        - An unknown field in the body is `400`, never ignored.
        - Send an `Idempotency-Key` to retry safely (see the header description).
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:write
      x-ozav-availability: coming_soon
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/IdempotencyKeyOptional"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VaultRequest"
            examples:
              default:
                summary: A vault that can pay one address, capped per send
                value:
                  root_id: ozav_vrt_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  rules:
                    destinations:
                      - family: evm
                        address: 3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b
                    per_send_cap: "5000000000"
                    cap_asset:
                      family: evm
                      id: 833589fcd6edb6e08f4c7c32d4f71b54bda02913
                  share_2_destination:
                    spki: 3059301306072a8648ce3d020106082a8648ce3d03010703420004f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6
                  share_3_destination:
                    spki: 3059301306072a8648ce3d020106082a8648ce3d0301070342000407070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707
                  root_authorization:
                    valid_until: 2026-12-31T23:59:59.000Z
                    signatures:
                      - public: a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1
                        signature: e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5
                      - public: b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2
                        signature: e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5
      responses:
        "202":
          description: The vault is `pending` and the enclave job is queued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Vault"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/VaultsNotEnabled"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/vaults \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "root_id": "ozav_vrt_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "rules": {
                  "destinations": [
                    {
                      "family": "evm",
                      "address": "3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b"
                    }
                  ],
                  "per_send_cap": "5000000000",
                  "cap_asset": {
                    "family": "evm",
                    "id": "833589fcd6edb6e08f4c7c32d4f71b54bda02913"
                  }
                },
                "share_2_destination": {
                  "spki": "3059301306072a8648ce3d020106082a8648ce3d03010703420004f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6"
                },
                "share_3_destination": {
                  "spki": "3059301306072a8648ce3d020106082a8648ce3d0301070342000407070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707"
                },
                "root_authorization": {
                  "valid_until": "2026-12-31T23:59:59.000Z",
                  "signatures": [
                    {
                      "public": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
                      "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                    },
                    {
                      "public": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
                      "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                    }
                  ]
                }
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/vaults", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "root_id": "ozav_vrt_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "rules": {
                  "destinations": [
                    {
                      "family": "evm",
                      "address": "3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b"
                    }
                  ],
                  "per_send_cap": "5000000000",
                  "cap_asset": {
                    "family": "evm",
                    "id": "833589fcd6edb6e08f4c7c32d4f71b54bda02913"
                  }
                },
                "share_2_destination": {
                  "spki": "3059301306072a8648ce3d020106082a8648ce3d03010703420004f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6"
                },
                "share_3_destination": {
                  "spki": "3059301306072a8648ce3d020106082a8648ce3d0301070342000407070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707"
                },
                "root_authorization": {
                  "valid_until": "2026-12-31T23:59:59.000Z",
                  "signatures": [
                    {
                      "public": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
                      "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                    },
                    {
                      "public": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
                      "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                    }
                  ]
                }
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/vaults",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "root_id": "ozav_vrt_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "rules": {
                        "destinations": [
                            {
                                "family": "evm",
                                "address": "3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b",
                            },
                        ],
                        "per_send_cap": "5000000000",
                        "cap_asset": {
                            "family": "evm",
                            "id": "833589fcd6edb6e08f4c7c32d4f71b54bda02913",
                        },
                    },
                    "share_2_destination": {
                        "spki": "3059301306072a8648ce3d020106082a8648ce3d03010703420004f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6",
                    },
                    "share_3_destination": {
                        "spki": "3059301306072a8648ce3d020106082a8648ce3d0301070342000407070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070707",
                    },
                    "root_authorization": {
                        "valid_until": "2026-12-31T23:59:59.000Z",
                        "signatures": [
                            {
                                "public": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
                                "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
                            },
                            {
                                "public": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
                                "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
                            },
                        ],
                    },
                },
            )
            data = response.json()
  /vaults/{vault_id}:
    get:
      operationId: getVault
      tags:
        - Vaults
      summary: Retrieve a vault
      description: |
        Closed (`coming_soon`): this call answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the platform.

        `lineage`, `addresses`, `receipt` and `activated_at` appear only when `state` is `active`;
        `failure_code` only when it is `failed`. The receipt is the enclave's signed answer: verify it before you
        deposit.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:read
      x-ozav-availability: coming_soon
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/VaultId"
      responses:
        "200":
          description: The vault.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Vault"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/VaultsNotEnabled"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/vaults/{vault_id} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/vaults/{vault_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/vaults/{vault_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /vaults/{vault_id}/transactions:
    post:
      operationId: createVaultTransaction
      tags:
        - Vaults
      summary: Describe a transfer and get the challenge to approve
      description: |
        Closed (`coming_soon`): this call answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the platform.

        Describes one token transfer from an `active` vault: the exact transaction bytes, what they do (`intent`),
        the derivation path, the grant it is approved under and how long approvers have. The answer carries the
        `challenge` each approver signs.

        - Families: `evm`, `solana` and `tron` (token transfers). Stellar transactions are not available in vaults.
        - `approve_until` is one deadline for every approver, up to 7 days ahead and not after the grant expires.
        - The `challenge` binds the vault, the grant, the sealed rules, the path and the transaction bytes. An
          approval of one transaction is useless for any other.
        - `destination_grant_id` (optional) is a destination grant of this vault; with it the vault also accepts the
          destinations that grant permits, until it expires.
        - `user_envelope` is your key share 3, sealed to your KMS key. OZAV passes it to the enclave and never opens it.
        - Send an `Idempotency-Key` to retry safely. It is scoped to the vault.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:write
      x-ozav-availability: coming_soon
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/VaultId"
        - $ref: "#/components/parameters/IdempotencyKeyOptional"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VaultTransactionRequest"
            examples:
              default:
                summary: An ERC-20 transfer on Base
                value:
                  grant_id: ozav_vgr_01J8Z9K2M3N4P5Q6R7S8T9U0V1
                  family: evm
                  derivation_path: m/44'/60'/0'/0/0
                  raw_transaction: 02f811111111111111111111111111111111111111111111111111111111111111111111111111111111
                  intent:
                    chain_id: 8453
                    token: 833589fcd6edb6e08f4c7c32d4f71b54bda02913
                    destination: 3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b
                    amount: "1000000"
                    fee_cap_wei: "5000000000000000"
                  user_envelope:
                    dominio: 3
                    forma: kms
                    ciphertextBlob: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
                    keyId: 1234abcd-12ab-34cd-56ef-1234567890ab
                    algoritmo: RSAES_OAEP_SHA_256
                  approve_until: 2026-11-02T18:00:00.000Z
      responses:
        "201":
          description: The transaction awaits approvals.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultTransaction"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/VaultsNotEnabled"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/vaults/{vault_id}/transactions \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Idempotency-Key: idem-6f9c24e5b1a74d38" \
              -H "Content-Type: application/json" \
              -d '{
                "grant_id": "ozav_vgr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "family": "evm",
                "derivation_path": "m/44'/60'/0'/0/0",
                "raw_transaction": "02f811111111111111111111111111111111111111111111111111111111111111111111111111111111",
                "intent": {
                  "chain_id": 8453,
                  "token": "833589fcd6edb6e08f4c7c32d4f71b54bda02913",
                  "destination": "3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b",
                  "amount": "1000000",
                  "fee_cap_wei": "5000000000000000"
                },
                "user_envelope": {
                  "dominio": 3,
                  "forma": "kms",
                  "ciphertextBlob": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                  "keyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
                  "algoritmo": "RSAES_OAEP_SHA_256"
                },
                "approve_until": "2026-11-02T18:00:00.000Z"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/vaults/{vault_id}/transactions", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "grant_id": "ozav_vgr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                "family": "evm",
                "derivation_path": "m/44'/60'/0'/0/0",
                "raw_transaction": "02f811111111111111111111111111111111111111111111111111111111111111111111111111111111",
                "intent": {
                  "chain_id": 8453,
                  "token": "833589fcd6edb6e08f4c7c32d4f71b54bda02913",
                  "destination": "3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b",
                  "amount": "1000000",
                  "fee_cap_wei": "5000000000000000"
                },
                "user_envelope": {
                  "dominio": 3,
                  "forma": "kms",
                  "ciphertextBlob": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                  "keyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
                  "algoritmo": "RSAES_OAEP_SHA_256"
                },
                "approve_until": "2026-11-02T18:00:00.000Z"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/vaults/{vault_id}/transactions",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Idempotency-Key": "idem-6f9c24e5b1a74d38",
                    "Content-Type": "application/json",
                },
                json={
                    "grant_id": "ozav_vgr_01J8Z9K2M3N4P5Q6R7S8T9U0V1",
                    "family": "evm",
                    "derivation_path": "m/44'/60'/0'/0/0",
                    "raw_transaction": "02f811111111111111111111111111111111111111111111111111111111111111111111111111111111",
                    "intent": {
                        "chain_id": 8453,
                        "token": "833589fcd6edb6e08f4c7c32d4f71b54bda02913",
                        "destination": "3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b",
                        "amount": "1000000",
                        "fee_cap_wei": "5000000000000000",
                    },
                    "user_envelope": {
                        "dominio": 3,
                        "forma": "kms",
                        "ciphertextBlob": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
                        "keyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
                        "algoritmo": "RSAES_OAEP_SHA_256",
                    },
                    "approve_until": "2026-11-02T18:00:00.000Z",
                },
            )
            data = response.json()
  /vaults/{vault_id}/transactions/{transaction_id}:
    get:
      operationId: getVaultTransaction
      tags:
        - Vaults
      summary: Retrieve a vault transaction
      description: |
        Closed (`coming_soon`): this call answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the platform.

        `status` is `awaiting_approvals`, `signed` or `rejected` (`expired` when the approval deadline passes with no signing job; `submitted` is reserved and never returned today). A `rejected` transaction carries a `failure_code`: `GRANT_REVOKED` when its grant was revoked, `HOST_TIMEOUT` when the signature was claimed and not completed within 15 minutes, or the code the vault refused with. Create the transaction again to retry. `signing` is true while the quorum is met
        and the enclave has not answered yet. `signed_transaction` appears only when `status` is `signed`, and
        `failure_code` only when it is `rejected`. OZAV does not broadcast: you do.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:read
      x-ozav-availability: coming_soon
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/VaultId"
        - $ref: "#/components/parameters/TransactionId"
      responses:
        "200":
          description: The transaction.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultTransaction"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/VaultsNotEnabled"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/vaults/{vault_id}/transactions/{transaction_id} \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/vaults/{vault_id}/transactions/{transaction_id}", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/vaults/{vault_id}/transactions/{transaction_id}",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /vaults/{vault_id}/transactions/{transaction_id}/approvals:
    post:
      operationId: approveVaultTransaction
      tags:
        - Vaults
      summary: Approve a transaction with one of your approver keys
      description: |
        Closed (`coming_soon`): this call answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the platform.

        One call per approver. Sign the transaction's `challenge` with the approver key and send the signature.

        - `ed25519`: sign the 32 challenge bytes. `p256`: ECDSA over SHA-256 of the challenge, DER signature (a cloud
          KMS signs the raw bytes without hashing them again). The API checks both signatures against the challenge.
        - `passkey`: send `auth_data`, `client_data_json` and the DER `signature`. The API stores the assertion and
          the enclave verifies it.
        - `valid_until` must be the transaction's `approve_until`.
        - The key must be an approver of the grant the transaction was created under (`422 APPROVER_NOT_IN_GRANT`).
        - The same approver twice counts once and answers `200`.
        - When the grant's quorum is met the vault signs. The enclave repeats every check, including the cap of an
          operator role, and may still reject the transaction.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:write
      x-ozav-availability: coming_soon
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/VaultId"
        - $ref: "#/components/parameters/TransactionId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VaultApprovalRequest"
            examples:
              default:
                summary: An Ed25519 approval of the transaction challenge
                value:
                  type: ed25519
                  public: af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d
                  signature: e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5
                  valid_until: 2026-11-02T18:00:00.000Z
      responses:
        "200":
          description: This approver had already approved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultTransaction"
        "201":
          description: The approval was recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultTransaction"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/VaultsNotEnabled"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/vaults/{vault_id}/transactions/{transaction_id}/approvals \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "type": "ed25519",
                "public": "af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d",
                "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
                "valid_until": "2026-11-02T18:00:00.000Z"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/vaults/{vault_id}/transactions/{transaction_id}/approvals", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "type": "ed25519",
                "public": "af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d",
                "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
                "valid_until": "2026-11-02T18:00:00.000Z"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/vaults/{vault_id}/transactions/{transaction_id}/approvals",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "type": "ed25519",
                    "public": "af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d",
                    "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
                    "valid_until": "2026-11-02T18:00:00.000Z",
                },
            )
            data = response.json()
  /vaults/{vault_id}/destination-grants:
    post:
      operationId: createVaultDestinationGrant
      tags:
        - Vaults
      summary: Permit new destinations on a vault, signed by your administrators
      description: |
        Closed (`coming_soon`): this call answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the platform.

        The vault's allowed destinations are sealed when it is created. To pay a new address without recreating the
        vault, your administrators sign a list of destinations with an expiry. The signed message binds the vault, its
        sealed rules, the destinations and the expiry, so a grant for one vault is useless on another. The API checks the
        administrator quorum of the vault's root before it stores anything; the enclave repeats every check.

        - A transaction uses the grant by sending its id as `destination_grant_id`. The grant must outlast the
          transaction's `approve_until`.
        - **Revoking is letting it expire**, or issuing a new grant without the destination. The worst delay is the
          time left on the grant that is still valid, so prefer short expiries.
        - The API does not check that an address is the right one. Confirm it through a second channel before your
          administrators sign.
        - The same destinations and expiry on the same vault return the existing grant with `200`.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:write
      x-ozav-availability: coming_soon
      x-ozav-services: true
      parameters:
        - $ref: "#/components/parameters/VaultId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VaultDestinationGrantRequest"
            examples:
              default:
                summary: Permit one more address for 30 days
                value:
                  destinations:
                    - family: evm
                      address: 4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c
                  valid_until: 2026-12-01T00:00:00.000Z
                  signatures:
                    - public: a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1
                      signature: e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5
                    - public: b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2
                      signature: e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5
      responses:
        "200":
          description: The same destination grant already existed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultDestinationGrant"
        "201":
          description: The destination grant was stored.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultDestinationGrant"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/VaultsNotEnabled"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/vaults/{vault_id}/destination-grants \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "destinations": [
                  {
                    "family": "evm",
                    "address": "4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c"
                  }
                ],
                "valid_until": "2026-12-01T00:00:00.000Z",
                "signatures": [
                  {
                    "public": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
                    "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                  },
                  {
                    "public": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
                    "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                  }
                ]
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/vaults/{vault_id}/destination-grants", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "destinations": [
                  {
                    "family": "evm",
                    "address": "4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c"
                  }
                ],
                "valid_until": "2026-12-01T00:00:00.000Z",
                "signatures": [
                  {
                    "public": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
                    "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                  },
                  {
                    "public": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
                    "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5"
                  }
                ]
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/vaults/{vault_id}/destination-grants",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "destinations": [
                        {
                            "family": "evm",
                            "address": "4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c",
                        },
                    ],
                    "valid_until": "2026-12-01T00:00:00.000Z",
                    "signatures": [
                        {
                            "public": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
                            "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
                        },
                        {
                            "public": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
                            "signature": "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5",
                        },
                    ],
                },
            )
            data = response.json()
  /vaults/attestation:
    get:
      operationId: getVaultAttestation
      tags:
        - Vaults
      summary: Retrieve the enclave attestation
      description: |
        Closed (`coming_soon`): this call answers `503 WAAS_VAULTS_NOT_ENABLED` until vaults are enabled for the platform.

        The attestation document of the enclave that runs your vaults: a COSE_Sign1 signed by the certificate chain of the enclave platform, with the
        PCR0 of the running image and a timestamp inside. Verify it yourself before you trust a vault: the chain up to the
        platform root certificate, the PCR0 you trust, and that the timestamp is recent.

        - OZAV **relays** the document, it does not attest anything: the host publishes it every few minutes and this
          route returns the latest. Whoever relays it can delay it but cannot forge it.
        - `observed_at` is when OZAV received it, not proof of freshness. Check the timestamp **inside** the document.
        - The same document for every customer: it describes the enclave, which all vaults share.
        - `404 VAULT_ATTESTATION_NOT_AVAILABLE` until the host has published one.
      x-ozav-entitlement: self_custody
      x-ozav-scopes:
        - wallets:read
      x-ozav-availability: coming_soon
      x-ozav-services: true
      responses:
        "200":
          description: The latest attestation document.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VaultAttestation"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/VaultsNotEnabled"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/vaults/attestation \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/vaults/attestation", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/vaults/attestation",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /treasury/obligations:
    get:
      operationId: getTreasuryObligations
      tags:
        - Treasury
      summary: Read the declared obligations, reserve floors and buffer
      description: |
        Returns the plan your organization declared with `PUT /treasury/obligations`. Before the first `PUT`
        it returns an empty plan with the defaults: `buffer_bps: 1000` (10%), `horizon_days: 30`,
        `outflow_history_buffer: false`.

        **Behind a switch that starts off.** While treasury is not enabled for the environment, every
        `/treasury` route answers `503 TREASURY_NOT_ENABLED` and does nothing.
      x-ozav-entitlement: custody
      x-ozav-scopes:
        - yield:read
      x-ozav-availability: beta
      responses:
        "200":
          description: The current plan.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/TreasuryPlan"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          description: "`TREASURY_NOT_ENABLED` - treasury is not enabled here; nothing was done."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/treasury/obligations \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/treasury/obligations", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/treasury/obligations",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
    put:
      operationId: putTreasuryObligations
      tags:
        - Treasury
      summary: Declare upcoming payments, reserve floors and the buffer
      description: |
        Replaces your organization's whole plan: the upcoming payments the forecast must keep covered, an
        optional reserve floor per currency, the buffer added on top of the payments, and the horizon and
        history-buffer choice that every allocation is checked with. What you do not send is cleared
        (`reserve_floors`) or set to its default (`buffer_bps` 1000, `horizon_days` 30,
        `outflow_history_buffer` false).

        **Amounts are integers in the currency's smallest unit**, as decimal strings (`"2500000000"` is 2,500
        USDC). A currency must be one your organization can hold in custody, the same currencies
        `GET /wallets/{wallet_id}/balances` reports; any other answers `400 TREASURY_CURRENCY_UNSUPPORTED`.

        **Recurrence.** `weekly` repeats every 7 days from `due_date`. `monthly` repeats on the day of month of
        `due_date`, moved to the last day in shorter months and back to it afterwards: a payment due on 31
        January falls on 28 (or 29) February and on 31 March. `recurrence_end` (inclusive) stops it.

        **Who may change it.** Writing the plan needs `yield:apply`, the scope that allocates, because the plan
        decides how much an allocation may take. The plan is the one live allocations are checked against, so
        it is only written with a live key; a test key answers `422 TREASURY_PLAN_LIVE_ONLY`.
      x-ozav-entitlement: custody
      x-ozav-scopes:
        - yield:apply
      x-ozav-availability: beta
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - obligations
              properties:
                obligations:
                  type: array
                  maxItems: 200
                  description: Every upcoming payment. Send `[]` to clear them.
                  items:
                    $ref: "#/components/schemas/TreasuryObligationInput"
                reserve_floors:
                  type: array
                  description: At most one floor per currency. An allocation never leaves the available balance below it.
                  items:
                    $ref: "#/components/schemas/TreasuryReserveFloor"
                buffer_bps:
                  type: integer
                  minimum: 0
                  maximum: 10000
                  default: 1000
                  description: Buffer on top of the payments in the horizon, in basis points of their total (1000 = 10%).
                horizon_days:
                  type: integer
                  minimum: 1
                  maximum: 365
                  default: 30
                  description: Days ahead whose payments the reserve keeps covered. Allocations are always checked with this horizon.
                outflow_history_buffer:
                  type: boolean
                  default: false
                  description: Add the outflow-history buffer (see `GET /treasury/forecast`) to the reserve allocations are checked against.
            examples:
              payroll:
                summary: Monthly payroll, a one-off invoice and a reserve floor
                value:
                  obligations:
                    - id: payroll
                      label: Payroll
                      currency: USDC
                      amount: "40000000000"
                      due_date: 2026-10-31
                      recurrence: monthly
                    - label: Supplier invoice 1182
                      currency: USDC
                      amount: "12500000000"
                      due_date: 2026-11-10
                  reserve_floors:
                    - currency: USDC
                      amount: "10000000000"
                  buffer_bps: 1000
      responses:
        "200":
          description: The plan as stored. Obligations sent without an `id` come back with one.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/TreasuryPlan"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          description: "`TREASURY_NOT_ENABLED` - treasury is not enabled here; nothing was done."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/treasury/obligations \
              -X PUT \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "obligations": [
                  {
                    "id": "payroll",
                    "label": "Payroll",
                    "currency": "USDC",
                    "amount": "40000000000",
                    "due_date": "2026-10-31",
                    "recurrence": "monthly"
                  },
                  {
                    "label": "Supplier invoice 1182",
                    "currency": "USDC",
                    "amount": "12500000000",
                    "due_date": "2026-11-10"
                  }
                ],
                "reserve_floors": [
                  {
                    "currency": "USDC",
                    "amount": "10000000000"
                  }
                ],
                "buffer_bps": 1000
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/treasury/obligations", {
              method: "PUT",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "obligations": [
                  {
                    "id": "payroll",
                    "label": "Payroll",
                    "currency": "USDC",
                    "amount": "40000000000",
                    "due_date": "2026-10-31",
                    "recurrence": "monthly"
                  },
                  {
                    "label": "Supplier invoice 1182",
                    "currency": "USDC",
                    "amount": "12500000000",
                    "due_date": "2026-11-10"
                  }
                ],
                "reserve_floors": [
                  {
                    "currency": "USDC",
                    "amount": "10000000000"
                  }
                ],
                "buffer_bps": 1000
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.put(
                "https://developers.ozav.io/v2/treasury/obligations",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "obligations": [
                        {
                            "id": "payroll",
                            "label": "Payroll",
                            "currency": "USDC",
                            "amount": "40000000000",
                            "due_date": "2026-10-31",
                            "recurrence": "monthly",
                        },
                        {
                            "label": "Supplier invoice 1182",
                            "currency": "USDC",
                            "amount": "12500000000",
                            "due_date": "2026-11-10",
                        },
                    ],
                    "reserve_floors": [
                        {
                            "currency": "USDC",
                            "amount": "10000000000",
                        },
                    ],
                    "buffer_bps": 1000,
                },
            )
            data = response.json()
  /treasury/forecast:
    get:
      operationId: getTreasuryForecast
      tags:
        - Treasury
      summary: Reserve and surplus per currency
      description: |
        How much of each currency can go to yield while the declared payments stay covered. Deterministic:
        the same balances and plan give the same answer, and every number comes with its parts and the formula
        that joins them, so the result can be checked by hand.

        Per currency:

        - `available` = the available custody balance, minus the balance already in yield positions, minus
          applications to yield still in flight. The custody balance still counts money you applied to a strategy,
          so it is taken off here, at position value: the same money is never offered twice;
        - `obligations` = every payment due from today (UTC) to `horizon_end`, recurrences expanded;
        - `buffer` = `ceil(obligations × buffer_bps / 10000)`, plus the optional outflow-history buffer;
        - `reserve` = `max(obligations + buffer, reserve_floor)`;
        - `surplus` = `max(0, available − reserve)`; it is never negative. `shortfall` = `max(0, reserve − available)`.

        `yield_positions` shows what is already in yield, split into redeemable now and redeemable with a delay
        (what the strategy does not release right now): how a shortfall could be covered.

        `horizon_days` and `outflow_history_buffer` default to the plan's. Passing them here is a what-if read;
        allocations are always checked with the plan's values.

        **Outflow-history buffer (optional, off by default).** With `outflow_history_buffer=true`, OZAV adds
        the 90th-percentile day of your organization's own completed outflows in that currency over the last 90
        days, times the horizon. It is observed history, labelled as such, not a prediction.

        A payment due before today is not counted: OZAV cannot tell whether it was paid.
      x-ozav-entitlement: custody
      x-ozav-scopes:
        - yield:read
      x-ozav-availability: beta
      parameters:
        - name: horizon_days
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 365
          description: Days ahead to count payments, from today (UTC). Defaults to the plan's `horizon_days`.
        - name: outflow_history_buffer
          in: query
          required: false
          schema:
            type: boolean
          description: Add the buffer from observed outflows (see above). Defaults to the plan's `outflow_history_buffer`.
      responses:
        "200":
          description: The forecast.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/TreasuryForecast"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          description: |
            `TREASURY_NOT_ENABLED` - treasury is not enabled here; nothing was done.
            `TREASURY_BALANCE_UNAVAILABLE` - the available balance could not be read; retry shortly.
            `TREASURY_HISTORY_UNAVAILABLE` - the outflow history of the window is larger than one read.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/treasury/forecast \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef"
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/treasury/forecast", {
              method: "GET",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
              },
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.get(
                "https://developers.ozav.io/v2/treasury/forecast",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                },
            )
            data = response.json()
  /treasury/allocations:
    post:
      operationId: createTreasuryAllocation
      tags:
        - Treasury
      summary: Allocate surplus to managed yield
      description: |
        Applies `amount` of your organization's custodied balance to a yield strategy, through the same order
        as `POST /yield/applications` (same queue, same two-party quorum, same compliance checks). Calling it is
        your approval.

        **Live keys only.** There is no simulator for yield, so a test key answers
        `422 TREASURY_ALLOCATION_LIVE_ONLY` and nothing is read or recorded.

        **Checked against a forecast computed at request time**, in the strategy's currency, with the plan's
        `horizon_days` and `outflow_history_buffer` (the request cannot change them):

        - if the available balance after the allocation would fall below your `reserve_floor`, it answers
          `409 TREASURY_ALLOCATION_BELOW_RESERVE_FLOOR`;
        - if `amount` is above the surplus, it answers `409 TREASURY_ALLOCATION_ABOVE_SURPLUS`, with the
          surplus and the forecast in `error.details`.

        Nothing is recorded on a refusal. If a yield position cannot be read, the balance already in yield is
        unknown and it answers `503 TREASURY_POSITIONS_UNAVAILABLE`. On success the forecast the checks used is
        stored with the allocation and returned in `forecast`.

        **One allocation of your organization at a time.** While another one is being recorded it answers
        `409 TREASURY_ALLOCATION_IN_PROGRESS`; retry shortly.

        **Recorded only if the checks still hold.** The application is written in the same database transaction
        that confirms the plan and the applications in flight are what the checks saw, under a lock that every
        application and redemption of your organization takes (including `POST /yield/applications` and agents'
        applications) and every plan write. If the plan or the set of applications in flight changed in between,
        it answers `409 TREASURY_FORECAST_CHANGED` and nothing is recorded; retry with the same `idempotency_key`.

        **What the reserve covers.** It is checked at request time and serializes yield applications and
        redemptions. It does not serialize other outflows: a transfer, a payout or card spend that leaves between
        the check and the order is not seen by it.

        **`idempotency_key` is required.** The same key with the same `strategy_id` and `amount` returns the
        same allocation (`202`), whatever the balance did since; with a different one it answers
        `409 IDEMPOTENCY_KEY_REUSED`. While the first call with the key is still being recorded, a repeat answers
        `409 TREASURY_ALLOCATION_IN_PROGRESS`.

        **One order in flight per strategy.** While an application or redemption for the strategy is in
        flight, it answers `409 YIELD_APPLICATION_IN_FLIGHT` (or `409 YIELD_REDEMPTION_IN_FLIGHT`) and the key
        stays free: repeat the same request after it settles. An organization held for compliance review
        answers `403 COMPLIANCE_HOLD`, as `POST /yield/applications` does.

        **The reserve binds this route only.** `POST /yield/applications` and an agent's
        `POST /agents/{agent_id}/yield-applications` apply without the treasury checks; use this route when the
        reserve must hold. An agent acting under a mandate allocates through its own route, under the
        mandate's rules.

        **Answers `202`.** Read the application's state on `GET /yield/applications/{application_id}` with
        `yield_application_id`.
      x-ozav-entitlement: custody
      x-ozav-scopes:
        - yield:apply
      x-ozav-availability: beta
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - idempotency_key
                - strategy_id
                - amount
              properties:
                idempotency_key:
                  type: string
                  minLength: 1
                  maxLength: 200
                  pattern: ^[A-Za-z0-9._:-]+$
                  description: A new key for each new allocation; the same key to repeat one that did not answer.
                strategy_id:
                  type: string
                  description: The strategy to apply to, as listed by `GET /yield/strategies`.
                amount:
                  type: string
                  pattern: ^[0-9]{1,30}$
                  description: Amount in the strategy asset's smallest unit, as a decimal integer string.
            examples:
              allocate:
                summary: Allocate 5,000 USDC (smallest unit)
                value:
                  idempotency_key: treasury-2026-10-16-001
                  strategy_id: base-usdc-prime
                  amount: "5000000000"
      responses:
        "202":
          description: Accepted. The application to yield is recorded.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/TreasuryAllocation"
        "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":
          description: "`TREASURY_ALLOCATION_LIVE_ONLY` - a test key; nothing was done."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          description: |
            `TREASURY_NOT_ENABLED` - treasury is not enabled here; nothing was done.
            `TREASURY_BALANCE_UNAVAILABLE`, `TREASURY_POSITIONS_UNAVAILABLE` or `TREASURY_HISTORY_UNAVAILABLE` -
            an input could not be read; retry with the same `idempotency_key`.
            `TREASURY_ALLOCATION_BUSY` - another order of your organization is being recorded; retry with the same
            `idempotency_key`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl https://developers.ozav.io/v2/treasury/allocations \
              -X POST \
              -H "X-API-Key: ozav_sk_test_0123456789abcdef" \
              -H "Content-Type: application/json" \
              -d '{
                "idempotency_key": "treasury-2026-10-16-001",
                "strategy_id": "base-usdc-prime",
                "amount": "5000000000"
              }'
        - lang: JavaScript
          label: JavaScript (fetch)
          source: |-
            const response = await fetch("https://developers.ozav.io/v2/treasury/allocations", {
              method: "POST",
              headers: {
                "X-API-Key": "ozav_sk_test_0123456789abcdef",
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                "idempotency_key": "treasury-2026-10-16-001",
                "strategy_id": "base-usdc-prime",
                "amount": "5000000000"
              }),
            });
            const data = await response.json();
        - lang: Python
          label: Python (requests)
          source: |-
            import requests

            response = requests.post(
                "https://developers.ozav.io/v2/treasury/allocations",
                headers={
                    "X-API-Key": "ozav_sk_test_0123456789abcdef",
                    "Content-Type": "application/json",
                },
                json={
                    "idempotency_key": "treasury-2026-10-16-001",
                    "strategy_id": "base-usdc-prime",
                    "amount": "5000000000",
                },
            )
            data = 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_-]`.
        Same key + same body replays the original
        response (`Idempotent-Replayed: true`); same key + different body
        returns 409 IDEMPOTENCY_KEY_REUSED; a concurrent duplicate returns 202.
      schema:
        type: string
        minLength: 16
        maxLength: 256
        pattern: ^[A-Za-z0-9_-]{16,256}$
    X402PaymentSignature:
      name: PAYMENT-SIGNATURE
      in: header
      required: false
      description: |
        Pay this call in USDC with x402 (v2, scheme `exact`) instead of prepaid credit: the base64 `PaymentPayload` answering the `PAYMENT-REQUIRED` challenge of a previous `402`. OZAV checks the authorization itself, settles it on-chain itself, credits the amount to your prepaid balance and runs the call, which is then charged as usual; the settlement comes back in the `PAYMENT-RESPONSE` header. A call that fails after payment is refunded to your credit, not on-chain. To pay with MPP instead, send `Authorization: Payment <credential>` answering the `WWW-Authenticate` challenge, with your API key in `X-API-Key`; the receipt returns in `Payment-Receipt`, and one authorization is credited once whichever protocol carries it. With a test key, see `X-Ozav-Simulate-Payment`.
      schema:
        type: string
        maxLength: 16384
    XOzavSimulatePayment:
      name: X-Ozav-Simulate-Payment
      in: header
      required: false
      description: |
        Sandbox only. A sandbox call is never charged, so it never answers `402` by itself. Send `required` to rehearse the payment: the call answers the same `402` (`PAYMENT-REQUIRED` and `details.x402`, with `details.simulated: true`) at the real price of the call, payable to the sandbox address `0x0000000000000000000000000000000000000402`. Retry with a well-formed `PAYMENT-SIGNATURE` (for example the one an OZAV agent signs in the sandbox with `POST /agents/{agent_id}/payments`): the call runs and `PAYMENT-RESPONSE` comes back with `simulated: true`. Nothing is checked on-chain and nothing moves. Ignored with a live key.
      schema:
        type: string
        enum:
          - required
    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.
      schema:
        type: string
        minLength: 16
        maxLength: 256
        pattern: ^[A-Za-z0-9_-]{16,256}$
    RootId:
      name: root_id
      in: path
      required: true
      description: The vault root id (`ozav_vrt_...`).
      schema:
        type: string
    VaultId:
      name: vault_id
      in: path
      required: true
      description: The vault id (`ozav_vlt_...`).
      schema:
        type: string
    TransactionId:
      name: transaction_id
      in: path
      required: true
      description: The vault transaction id (`ozav_vtx_...`).
      schema:
        type: string
  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 is not served (see `Rail.x-ozav-rail-availability`).
      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
        the key lacks the scope (code INSUFFICIENT_SCOPE). On money-moving
        requests (POST /transfers, POST /wallets/{wallet_id}/transfers) a
        compliance hold on the customer answers with code COMPLIANCE_HOLD: the
        hold is reviewed and lifted on OZAV's side; retrying does not clear it.
        When the only hold is a pause until pending onboarding documents are
        received, a withdrawal answers CUSTOMER_DOCUMENTS_PENDING instead: send
        what `requirements` on GET /customers/{id} lists; OZAV lifts the pause
        after reviewing it.
        On OZAV Services (live without the platform's KYB), any API outside decisions,
        screenings, lists and self-custody wallets answers with code PRODUCTION_NOT_ACTIVATED.
      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
        malformed for this position (see "Resource ids" under Errors). These share
        one status; `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.

        If this `202` repeats instead of resolving, the original request's outcome is unknown and no stored result will arrive - 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.
      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.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    PaymentRequired:
      description: |
        OZAV Services only (prepaid): your prepaid credit does not cover this call (code
        INSUFFICIENT_CREDIT). The call did not run and nothing was charged. `details` carries
        `balance_usd` and `price_usd`; top up in the portal (Billing) and retry. Organizations on
        the OZAV platform are invoiced monthly and never receive this.

        On `POST /decisions` and `POST /screenings`, where OZAV accepts x402 payment, the response also
        carries an x402 v2 challenge: the `PAYMENT-REQUIRED` header (base64 `PaymentRequired`) and the
        same object in `details.x402` - the price of this one call in native USDC, paid to OZAV. `accepts`
        lists one entry per network OZAV receives on, Polygon (`eip155:137`) first; where enabled, a
        second entry offers Base (`eip155:8453`). Pay on any one of them, signing for that network's USDC
        (the entry's `asset`); a payment whose `accepted.asset` is not the USDC of its `network` is refused
        with `network_or_asset_not_accepted`. Retry with the `PAYMENT-SIGNATURE` header to pay it. A payment OZAV does not accept answers
        `402 X402_PAYMENT_REJECTED` with `details.reason` (`amount_below_price`, `wrong_pay_to`,
        `network_or_asset_not_accepted`, `authorization_not_yet_valid`, `authorization_expires_too_soon`,
        `authorization_window_too_long`, `signature_invalid`, `authorization_already_used`,
        `insufficient_balance`, `settlement_refused`, `payment_malformed` or `payment_not_accepted`) and a
        new challenge; nothing was charged. If OZAV could not confirm the settlement, `503
        X402_PAYMENT_UNAVAILABLE`: retry with the same header, it is never charged twice. A retried payment is
        credited only once OZAV reads, on-chain, the transaction that used the authorization moving the USDC to OZAV; when that cannot be
        established automatically, the `503` says the payment needs a manual check (contact support with the request id).

        Where OZAV also accepts MPP, the response carries an MPP challenge in `WWW-Authenticate: Payment`
        (method `evm`, intent `charge`, credential type `authorization`, valid for five minutes) and in
        `details.mpp.www_authenticate`. A credential OZAV does not accept answers `402
        MPP_PAYMENT_REJECTED` with `details.reason` (the reasons above, plus
        `challenge_not_issued_by_ozav`, `challenge_expired` and `nonce_not_bound_to_challenge`); `503
        MPP_PAYMENT_UNAVAILABLE` means retry with the same credential.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestId"
        PAYMENT-REQUIRED:
          description: The x402 v2 challenge (base64 JSON), only where OZAV accepts x402 for the call.
          schema:
            type: string
        WWW-Authenticate:
          description: The MPP challenge (`Payment id=..., realm=..., method="evm", intent="charge", request=..., expires=...`), only where OZAV accepts MPP for the call.
          schema:
            type: string
      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.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    RateLimited:
      description: |
        Too many requests. Read `code` first. `RATE_LIMIT_EXCEEDED` is the per-minute limit:
        wait the number of seconds in the `Retry-After` response header before retrying;
        retrying sooner just consumes budget without being served. A cap with its own code
        (for example `CREDENTIAL_ISSUANCE_LIMIT_REACHED`, which resets the next day) carries
        no `Retry-After`, and retrying sooner than its stated reset cannot succeed.
      headers:
        Retry-After:
          description: Seconds to wait before retrying. Among `429` responses, sent with `RATE_LIMIT_EXCEEDED` only.
          schema:
            type: integer
            minimum: 1
        X-Request-ID:
          $ref: "#/components/headers/XRequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    VaultsNotEnabled:
      description: |
        `WAAS_VAULTS_NOT_ENABLED` - vaults are off for this platform - or `VAULT_STORE_UNAVAILABLE`, a transient
        storage failure. Retry the second shortly.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
  schemas:
    TreasuryObligationInput:
      type: object
      description: An upcoming payment the forecast must keep covered.
      required:
        - label
        - currency
        - amount
        - due_date
      properties:
        id:
          type: string
          pattern: ^[A-Za-z0-9._:-]{1,64}$
          description: Your id for it. Assigned when omitted; unique within the plan.
        label:
          type: string
          minLength: 1
          maxLength: 140
        currency:
          type: string
          description: A currency your organization can hold in custody
          such as USDC.: null
        amount:
          type: string
          pattern: ^[0-9]{1,30}$
          description: Smallest unit of the currency
          as a decimal integer string greater than zero.: null
        due_date:
          type: string
          format: date
          description: The first (or only) due date.
        recurrence:
          type: string
          enum:
            - none
            - weekly
            - monthly
          default: none
        recurrence_end:
          type:
            - string
            - "null"
          format: date
          description: Last date (inclusive) a recurring payment can fall on. Only with `weekly` or `monthly`.
    TreasuryObligation:
      type: object
      properties:
        id:
          type: string
        label:
          type: string
        currency:
          type: string
        amount:
          type: string
          description: Smallest unit
          as a decimal integer string.: null
        due_date:
          type: string
          format: date
        recurrence:
          type: string
          enum:
            - none
            - weekly
            - monthly
        recurrence_end:
          type:
            - string
            - "null"
          format: date
    TreasuryReserveFloor:
      type: object
      required:
        - currency
        - amount
      properties:
        currency:
          type: string
        amount:
          type: string
          pattern: ^[0-9]{1,30}$
          description: Smallest unit
          as a decimal integer string (zero allowed).: null
    TreasuryPlan:
      type: object
      properties:
        obligations:
          type: array
          items:
            $ref: "#/components/schemas/TreasuryObligation"
        reserve_floors:
          type: array
          items:
            $ref: "#/components/schemas/TreasuryReserveFloor"
        buffer_bps:
          type: integer
        horizon_days:
          type: integer
        outflow_history_buffer:
          type: boolean
        updated_at:
          type:
            - string
            - "null"
          format: date-time
    TreasuryCurrencyForecast:
      type: object
      description: |
        One currency. Every amount is a decimal integer string in the currency's smallest unit, and each
        block carries the `formula` that joins its parts.
      properties:
        currency:
          type: string
        available:
          type: object
          properties:
            amount:
              type: string
            formula:
              type: string
            ledger_available:
              type: string
              description: The available custody balance.
            in_yield_positions:
              type: string
              description: Balance already in yield positions in this currency
              still counted by the custody balance.: null
            pending_yield_applications:
              type: string
              description: Applications to yield still in flight.
        yield_positions:
          type: object
          properties:
            total:
              type: string
            redeemable_now:
              type: string
            redeemable_with_delay:
              type: string
            release_unreadable:
              type: string
              description: Balance in positions whose release could not be read now.
            note:
              type: string
        obligations:
          type: object
          properties:
            total:
              type: string
            occurrences:
              type: array
              items:
                type: object
                properties:
                  obligation_id:
                    type: string
                  label:
                    type: string
                  due_date:
                    type: string
                    format: date
                  amount:
                    type: string
        buffer:
          type: object
          properties:
            total:
              type: string
            formula:
              type: string
            obligations_buffer:
              type: object
              properties:
                amount:
                  type: string
                buffer_bps:
                  type: integer
                formula:
                  type: string
            outflow_history_buffer:
              type: object
              properties:
                enabled:
                  type: boolean
                amount:
                  type: string
                p90_daily_outflow:
                  type: string
                days:
                  type: integer
                window_days:
                  type: integer
                formula:
                  type: string
                basis:
                  type: string
        reserve:
          type: object
          properties:
            amount:
              type: string
            formula:
              type: string
            required_by_obligations:
              type: string
            reserve_floor:
              type: string
        surplus:
          type: object
          properties:
            amount:
              type: string
              description: Never negative.
            formula:
              type: string
        shortfall:
          type: object
          properties:
            amount:
              type: string
            formula:
              type: string
    TreasuryForecast:
      type: object
      properties:
        as_of:
          type: string
          format: date
        horizon_days:
          type: integer
        horizon_end:
          type: string
          format: date
        amount_unit:
          type: string
          enum:
            - currency_smallest_unit
        complete:
          type: boolean
          description: False when some yield positions could not be read; `unreadable_chains` names them.
        unreadable_chains:
          type: array
          items:
            type: string
        currencies:
          type: array
          items:
            $ref: "#/components/schemas/TreasuryCurrencyForecast"
    TreasuryAllocation:
      type: object
      properties:
        id:
          type: string
          format: uuid
        idempotency_key:
          type: string
        strategy_id:
          type: string
        currency:
          type: string
        amount:
          type: string
          description: Smallest unit
          as a decimal integer string.: null
        status:
          type: string
          enum:
            - submitted
          description: "`submitted` - the application to yield is recorded; follow it by `yield_application_id`."
        yield_application_id:
          type: string
          format: uuid
        forecast:
          description: The forecast of this currency the checks used, as `TreasuryCurrencyForecast` plus `as_of` and `horizon_days`.
          allOf:
            - $ref: "#/components/schemas/TreasuryCurrencyForecast"
        created_at:
          type: string
          format: date-time
    NetworkTransfer:
      type: object
      description: |
        One network transfer of your own balance.

        `status` is smaller than the internal state machine. There is
        no "expired": internally that means the attestation window passed and will
        be requested again, and showing the word to a client would say the money
        was lost about a transfer that is going fine.
      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
            stated one by one. This mirrors `YieldApplication.status`.

            `pending` - accepted and queued. Nothing has been attempted yet.

            `running` - being attempted right now. Wait; do not order again.

            `submitted` - it left the source network and is on its way. This is
            not a failure and not a completion: the arrival is confirmed on the
            destination network, and this field does not yet report it.

            `refused` - a gate declined it and no money left your balance.
            `detail` says which gate. Fixing what it names and ordering again is
            safe.

            `failed` - it was attempted and nothing left your balance, and that
            is known. Safe to order again.

            `indeterminate` - it stopped without knowing whether the amount left
            your balance. Do not order it again: a second attempt may move the
            amount twice, and moving between networks cannot be undone. Support
            has been notified and a person confirms it on the network.
        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
            points, frozen when it was accepted. It is the rate, not an amount
            deducted: the transfer moves the full `amount` you requested. Null until then. The network cost is a separate line and is
            never folded into this.
        delivery:
          type:
            - string
            - "null"
          enum:
            - in_progress
            - completed
            - failed
            - null
          description: |
            Whether the amount arrived on the destination network. This is a
            different question from `status`, and both matter.

            `status` is about the order: accepted, running, submitted, refused,
            failed, indeterminate. It stops at `submitted` once the amount
            leaves the source network, and stays there.

            `delivery` is about the money: `in_progress` while it is crossing,
            `completed` once it is credited on the destination network, and
            `failed` if the crossing ended without crediting.

            `null` means there is no crossing yet: the order has not left.
            It does not mean the amount failed to arrive.
        detail:
          type:
            - string
            - "null"
          description: |
            Why, for `refused`, `failed` and `indeterminate`. Null otherwise.
            For `indeterminate` it carries the do-not-retry text.
        created_at:
          type: string
          format: date-time
    YieldApplication:
      type: object
      description: |
        An instruction to apply custodied balance to a yield strategy. It is
        what the client asked for, not what the client holds. The position
        actually held is read from `GET /yield/positions`, which is built from
        what happened on-chain. Confusing the two would make a refused
        instruction look like a position.
      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
            difference is money, so it is stated field by field.

            `applied` - confirmed on-chain. `tx_hash` carries the transaction.

            `refused` - a gate declined it and no money moved. `detail` says
            which gate. Fixing what it names and ordering again is safe.

            `failed` - it was attempted and nothing moved, and that is known:
            either the network rejected the transaction, or it never left. Safe
            to order again.

            `indeterminate` - it stopped without knowing whether the money
            moved. Either an internal approval is still pending, or a
            transaction went out and was not yet recorded. Do not order it
            again: a second order can move the balance twice. While an
            application sits here, further orders for the same strategy answer
            202 with `duplicate: true` and this same application. It clears
            only after a person checks the chain. `tx_hash` is populated
            whenever a transaction is known, and it is the pointer to use.

            `pending` / `running` - queued, and being worked on.
        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
    YieldRedemptionOrder:
      type: object
      description: |
        An instruction to take custodied balance back out of a yield strategy.
        Like `YieldApplication`, it is what was asked for, not what is held: the
        position is read from `GET /yield/positions`, and the realized gain from
        `GET /yield/realized-gains`, both built from what happened on-chain.
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - running
            - redeemed
            - refused
            - failed
            - indeterminate
          description: |
            The same states as `YieldApplication.status`, with `redeemed` in
            place of `applied`.

            `redeemed` - confirmed on-chain; the balance is back on your custody
            address. `tx_hash` carries the transaction.

            `refused` - a gate declined it and no money moved. `detail` says
            which gate. Fixing what it names and ordering again is safe.

            `failed` - it was attempted and nothing moved, and that is known.
            Safe to order again.

            `indeterminate` - it stopped without knowing whether the money
            moved. Do not order it again. Further orders for the same strategy,
            in either direction, wait on it. It clears only after a person
            checks the chain; `tx_hash` is the pointer when a transaction is
            known.

            `pending` / `running` - queued, and being worked on.
        strategy_id:
          type: string
        amount:
          type:
            - string
            - "null"
          description: Smallest unit, as a decimal integer string. Null when `all` is true.
        all:
          type: boolean
          description: True when every share held was redeemed.
        idempotency_key:
          type: string
        detail:
          type:
            - string
            - "null"
          description: The cause, for `refused`, `failed` and `indeterminate`.
        tx_hash:
          type:
            - string
            - "null"
          description: Present whenever a transaction is known; 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
        through cost, not a charge by OZAV, and it is quoted on its own line.

        Reported at six decimal places when two would round a real cost to
        "0.00": a fraction of a cent on a cheap network is a real number, and
        rounding it to zero makes the same false claim that stating no cost
        would.
      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.

        `balance` says what the position is worth; this says what the venue will
        release. The two diverge when the underlying market is fully utilised,
        and the holder needs to know before requesting a withdrawal rather than
        at the point of refusal.

        The three causes call for opposite actions, so they are never collapsed:
        `held` means the funds are there and still earning (wait), `unreadable`
        means the limit could not be read (a defect to fix), and only
        `available` states a redeemable amount.
      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
            this before reading `available_to_withdraw`.

            `available` - a redeemable amount is stated. `held` - the funds are there and still
            earning, but the venue will not release them now; wait and re-read. `unreadable` - the
            limit could not be read, which is a defect on our side, not a statement about your
            funds; do not present it as zero.
        available_to_withdraw:
          type:
            - string
            - "null"
          description: |
            Smallest-unit amount, as a string. `"0"` when `kind` is `held`, which
            is a measured zero. `null` when `kind` is `unreadable`, which is not
            a zero at all: it means the limit was never read.
      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.

        OZAV never names the protocol, the curator, the issuer, or the contract
        address on a client surface: you contract with OZAV, not with the venue.
        That rule left a real gap, because without any of those you also could
        not tell two strategies apart on risk. This object closes it: it answers
        the questions that change a decision, and none that identify a party.

        `measured_at` is when these facts were read on-chain. A risk profile with
        no date ages without anyone noticing.
      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
            sits between the holder and the market (`direct`).
        curator_ref:
          type:
            - string
            - "null"
          description: |
            An opaque, stable label for the curator; `null` when `management` is
            `direct`. It is not a name and never becomes one. It answers exactly
            one question: do two strategies share the team that decides? That is
            the concentration question, and you can answer it without knowing who
            the team is. Two strategies with the same value concentrate the same
            decision, however different their networks look.
        issuer_freeze:
          type: string
          enum:
            - none
            - contract_rule
            - unilateral_key
          description: |
            Whether the instrument's issuer can freeze the position, and how.
            `none`: no such authority exists. `contract_rule`: it exists and runs
            through public, auditable logic on-chain. `unilateral_key`: it exists
            as a key, and the issuer can freeze any account on its own.

            The same instrument can carry a different form on different networks.
            When it does, the two are listed as separate strategies and this field
            is what tells them apart.
        redemption_form:
          type: string
          enum:
            - immediate
            - by_request
          description: |
            The form of redemption the instrument supports.
            `immediate` redeems against the contract, up to what it releases.
            `by_request` is a request to the issuer, with its own turnaround, and
            never settles on the spot.

            This is not today's liquidity. How much can leave right now is on the
            position, read live from the chain; a calm day never turns
            `by_request` into `immediate`.
        principal_guaranteed:
          type: boolean
          enum:
            - false
          description: |
            Always `false`. OZAV does not promise, guarantee, or indicate a
            return, and does not repurchase or restore principal. The result is
            whatever the protocol produces, positive or negative.
        smart_contract_risk:
          type: boolean
          enum:
            - true
          description: |
            Always `true`. The balance sits in a contract, and a contract can
            fail or be exploited. Admission checks reduce this risk and do not
            remove it.
        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.
        Distinct from the indicative rate, which is refreshed off-chain and
        states what the venue pays now. Both are published; neither replaces the
        other.

        When the rate cannot be stated, `known` is `false` and `reason` names the
        cause instead of the field being null or zero.
      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`
            is true. The `7d`/`30d` in the field name is what was requested; this
            is what was measured.
        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.
        Never a float. The value must be greater than zero, with no leading
        zeros in the integer part (a bare `0` integer part is allowed for
        sub-unit values such as "0.50") and at most 15 integer digits.
      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
        digits. Never a float.
      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.
      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, shared by named accounts and payout
        destinations. The table below answers one question: can you open a named
        account on this rail (machine-readable in `x-ozav-rail-availability`)?
        Paying out is a different question, answered by the corridor table in
        `POST /payout-destinations`. The two differ: `fedwire`, for example, is
        live for payouts, while opening a USD account on it is sandbox only.

        | Rail | Corridor | Availability |
        |---|---|---|
        | `pix` | BRL - Brazil instant payments | available |
        | `fedwire` | USD - US wire | sandbox only |
        | `sepa` | EUR - SEPA credit transfer | live (behind enablement) |
        | `spei` | MXN - Mexico SPEI | sandbox only |
        | `swift` | Cross-border SWIFT | live (behind enablement) |
        | `faster_payments` | GBP - UK Faster Payments | live (behind enablement) |

        **`ach` and `co_transfer` are not offered.** Requests for accounts, payout
        destinations and quotes use `RequestRail`, which leaves both out, and a
        `fiat_payment` source on `ach` is refused with `422 RAIL_NOT_AVAILABLE`.
        They stay in this enum only because older records carry them.

        **`live (behind enablement)`** means the corridor is served in production,
        but not to everyone: opening an account still requires that OZAV has
        enabled the specific customer for foreign-currency named accounts. Until
        then a `live` request is refused with a named error that says which step
        is missing - not with `ACCOUNT_RAIL_NOT_AVAILABLE`, which would send you
        looking at the corridor instead of the customer.

        **`sandbox only` is a real, usable state, and not a promise of later.**
        The corridor is fully exercisable with a `test` key: you can open the
        account, read its receiving instructions and build against the exact
        response shape production will return. What it does not have is a
        settlement partner behind it, so a `live` key is refused with
        `ACCOUNT_RAIL_NOT_AVAILABLE` rather than opening something that cannot
        receive. Build against it in sandbox; the request you write does not
        change when the corridor goes live.
      enum:
        - pix
        - ach
        - fedwire
        - sepa
        - spei
        - swift
        - faster_payments
        - co_transfer
        - uaefts
        - eft
        - cnaps
        - zengin
        - nip
        - cce
        - rtc
      x-ozav-rail-availability:
        pix: sandbox_active
        fedwire: sandbox_only
        sepa: live_gated
        spei: sandbox_only
        swift: live_gated
        faster_payments: live_gated
    RequestRail:
      type: string
      description: |
        The rails a request can name: every value of `Rail` except `ach` and
        `co_transfer`, which are not offered and appear only on older records.
      enum:
        - pix
        - fedwire
        - sepa
        - spei
        - swift
        - faster_payments
        - uaefts
        - eft
        - cnaps
        - zengin
        - nip
        - cce
        - rtc
    Network:
      type: string
      description: |
        Public v2 blockchain network vocabulary for wallet balances and crypto
        transfer legs. This enum is the wire contract today and
        lists only networks accepted by public v2 fields now. The custody stack
        can have additional active networks before those networks are accepted
        by every public API flow; see `x-ozav-network-matrix` for that explicit
        split.
      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
          maxLength: 32
          description: National phone number. At most 20 digits.
    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.

        The `type` enum is the union of the personal (`cpf`, `ssn`, `nif`, `passport`) and company
        (`cnpj`, `ein`, `company_number`) identifiers, because a payout destination can be held by
        either. Use `other` only when none applies - it is the one value that carries no meaning and
        gets no validation.

        `cpf` and `cnpj` are checked against their own check digits and rejected with `400` when the
        number does not add up.
      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`:
            - `cpf` - Brazilian individual taxpayer registry. Check digits are verified.
            - `cnpj` - Brazilian company registry. Check digits are verified.
            - `ssn` - US Social Security Number.
            - `ein` - US Employer Identification Number.
            - `nif` - Iberian/European tax identification number.
            - `company_number` - company registry number outside Brazil/US.
            - `passport` - passport number.
            - `other` - any identifier not covered above. Carries no meaning and gets no validation.
          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`:
            - `cpf` - Brazilian individual taxpayer registry (Cadastro de Pessoas Físicas).
            - `ssn` - US Social Security Number.
            - `nif` - Iberian/European tax identification number (Número de Identificação Fiscal).
            - `passport` - passport number.
            - `other` - any identifier not covered above.
          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`:
            - `cnpj` - Brazilian company registry (Cadastro Nacional da Pessoa Jurídica).
            - `ein` - US Employer Identification Number.
            - `nif` - Iberian tax/registry number (Número de Identificação Fiscal).
            - `company_number` - a national company/registry number (e.g. Companies House).
            - `other` - any registration identifier not covered above.
          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
        you can request for a customer (`POST /verifications`) and the keys of a
        customer's `verification.checks` map.

        Identity / KYC (individuals):
        - `kyc_document` - government ID document capture and authentication.
        - `liveness` - confirms the person is physically present (anti-spoofing).
        - `face_match` - the selfie matches the ID document portrait.
        - `proof_of_address` - a document evidencing residential address.
        - `age_estimation` - estimate that the person is above an age threshold.

        Business / KYB (companies):
        - `kyb` - business registration and standing verification.
        - `ubo` - ultimate-beneficial-owner identification and checks.

        AML (name-based):
        - `aml_name_screening` - point-in-time designated-list name screening.
        - `aml_ongoing_monitoring` - continuous re-screening for new matches.

        Transaction / on-chain:
        - `wallet_screening` - blockchain-address risk screening.
        - `travel_rule` - originator/beneficiary information exchange for crypto sends.

        Contactability:
        - `email_verify` - the email address is reachable and controlled by the customer.
        - `phone_verify` - the phone number is reachable and controlled by the customer.

        Financial:
        - `economic_capacity` - assessment of the customer's economic capacity (declared/evidenced financial standing) used to justify transaction limits.
      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.
        - `not_started` - the check has not begun.
        - `pending` - the check is in progress; no outcome yet.
        - `pass` - the check succeeded.
        - `fail` - the check did not succeed.
        - `review` - inconclusive; queued for manual review (never silently passed).
      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.

        | Value | Meaning |
        |---|---|
        | `pending` | Created; verification has not completed. The customer cannot transact yet. |
        | `in_review` | Checks were submitted and are under (automated or manual) review. |
        | `approved` | Verification passed. The customer may transact once OZAV has everything a payout will need; see the note below. |
        | `rejected` | Verification failed; the customer cannot be onboarded. |
        | `suspended` | A previously active customer has been blocked by a compliance or operational action. |

        **`approved` is about the checks, not about being able to operate.**
        OZAV activates a customer only when it also holds everything a payout
        will need: the holder's name, a valid tax document for the country
        declared on it, and an address. A Brazilian company with a valid CNPJ
        is exempt from the address, because the public registry supplies it.

        A customer can therefore sit at `approved` and still be refused, with
        `422` on `POST /accounts` and on the money paths. The gap shows up there,
        before any funds are committed, and not at payout time.

        The request body cannot resolve it. Send the missing field through the
        customer update path, or ask OZAV: the blocked queue names each
        customer and the exact field it is waiting on.
      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 `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. Accepted and stored; not required for onboarding.
        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. When omitted, OZAV's own screening records the answer: not a PEP when its recent screening read the PEP lists without a match. `true`, or a PEP match, goes to OZAV compliance review."
        gender:
          type: string
          enum:
            - male
            - female
            - other
          description: Gender as the person declares it. Optional; never defaulted. Required on the authorized person of a business before USD, EUR or GBP accounts can open. A `PATCH` that leaves it out keeps the value already given.
        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.
    AssociatedPersonIndividualUpdate:
      type: object
      description: 'The `individual` block of an associated-person update. Same fields as `CustomerIndividual`, all optional: only the fields you include change, and omitted fields keep their stored value. To declare a gender, send `{ "gender": "female" }` alone.'
      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. Accepted and stored; not required for onboarding.
        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.
        gender:
          type: string
          enum:
            - male
            - female
            - other
          description: Gender as the person declares it. Optional; never defaulted. Required on the authorized person of a business before USD, EUR or GBP accounts can open. A `PATCH` that leaves it out keeps the value already given.
        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.
        cnae_codes:
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            pattern: ^([0-9]{7}|[0-9]{4}-[0-9]/[0-9]{2})$
            example: 6201-5/01
          description: "Brazilian CNAE activity codes the company declares, either as 7 digits (`6201501`) or in the notation of the CNPJ card (`6201-5/01`). They are stored as digits only; `6201-5/01` and `6201501` are the same code, and the stored list never repeats one (`uniqueItems` describes the stored list: a request that repeats a code is accepted and stored once, and the 20-code limit counts codes after repeats are removed). The first one is the primary activity. Send an empty array to clear the list. Used only when the public registry (Receita Federal) does not return an activity code for the company's CNPJ; the registry always prevails, and a declared code is recorded as declared by the customer, never as official."
        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."
        expected_daily_volume_usd:
          type: number
          minimum: 0
          description: "Expected daily transaction movement in USD. A separate declaration, not the monthly figure divided by anything: account opening asks for both, and a derived number would be a value the customer never declared. 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.
        bc_protege_disabled:
          type: boolean
          description: "Brazilian business only: the company declares it has turned off BC Protege+ (the Central Bank of Brazil service that blocks institutions from opening accounts in the company's name; see https://www.gov.br/pt-br/servicos/proteger-se-com-o-bc-protege), or never turned it on. Required before a BRL account is requested for a business: without `true`, `POST /accounts` refuses with `422 ACCOUNT_PROFILE_INCOMPLETE` and `missing` lists `business.bc_protege_disabled` (sandbox too). Only the company can turn the service off, with its own gov.br login; send `true` once it has. Absent = not declared."
    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:
            - `full` - OZAV performs the verification checks directly.
            - `reliance` - you attest, under a reliance arrangement, that verification
              was performed on your side; available only to organizations holding the
              `kyc_reliance` entitlement (contractual attestation). Reliance mode does
              not auto-approve a customer in the public API response; the customer's
              `status` reflects the current onboarding state.

            Designated-list screening is always performed by OZAV regardless of mode.

            **When `mode` is absent.** This field is optional, and its absence is meaningful:
            OZAV cannot state a verification posture for this record. Treat it as
            "posture unknown". It is not equivalent to `full`, and must never be read as one.

            Two different internal situations produce it, and the API does not distinguish them:
            either no verification posture was ever recorded (typical for records created outside
            the API v2 onboarding path, which carry no canonical verification block), or a posture
            exists internally but is not expressible in this API version. Both mean the same thing
            to you: do not treat this record as verified on the strength of this object.

            **Precedence against `status`.** `status` is required and is always present. When
            `mode` is absent, `status` describes the internal onboarding state only and is not a
            verification attestation - a record can read `status: approved` with no `mode`, and
            that combination must not be used as evidence that verification was performed.

            **`requirements.missing_fields` is a lower bound here.** It is still emitted and still
            useful, but when the canonical block is missing OZAV cannot resolve the record's
            jurisdiction, so jurisdiction-specific requirements are not listed. Satisfying every
            field it names may still leave the record incomplete.

            The record is still returned in full, and other records are unaffected: one
            incomplete record never fails a call.
        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
        "was it approved, and if not, why, and whose move is it".

        `status` is finer than the customer's `status`: a customer reads `pending`
        while OZAV is reviewing and while OZAV is waiting on documents from you,
        and only `decision.status` tells those apart (`pending_review` vs
        `information_requested`). Read `requirements.owner` for whose move it is.

        `reason_code` is a closed vocabulary (below) and `reason` is its English
        label. Free-text analyst notes never travel: a rejection recorded before
        this vocabulary existed reads `policy`.

        Present on `GET /customers/{customer_id}`, on every record of
        `GET /customers`, and in the `customer.updated` webhook body - which is
        emitted on approval, on rejection, and when information is requested.
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - pending_review
            - information_requested
            - approved
            - rejected
            - expired
          description: |
            - `pending_review` - OZAV is reviewing; nothing is owed by you right now.
            - `information_requested` - OZAV asked for documents or corrections;
              `requirements.documents_due` lists them with the deadline.
            - `approved` / `rejected` - final.
            - `expired` - nothing was sent for this customer for 30 days while it
              was your move. Not final and not a rejection: it reopens on its own
              as soon as you send anything (a document, a profile update, an
              identity session), and `requirements.owner` stays `integrator`.
              Treat any status you don't recognise as not final.
        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
        it is, until when, and whether the deadline has passed.

        - OZAV decides within **48 business hours** (Mon-Fri, Brazilian national
          holidays excluded) once the record is complete - profile fields filled
          and the required documents accepted. While that clock runs `owner` is
          `ozav`.
        - When OZAV asks you for a document or a correction the clock is yours:
          **30 days** for a document, **15 days** for a correction (`owner`
          `integrator`; `due_at` mirrors `requirements.documents_due[].due_at`).
        - `owner` is `none` when the decision is final, when no onboarding case is
          open, or while OZAV is waiting on a third party (that wait counts
          against no one).

        Present on `GET /customers/{customer_id}`, on every record of
        `GET /customers`, and in the `customer.updated` webhook body.
      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
        vocabulary; each code has a fixed English label in `decision.reason`.

        Rejection codes: `sanctions_match`, `pep_unmitigated`, `identity_failed`,
        `document_invalid`, `capacity_unproven`, `inconsistent_data`,
        `restricted_jurisdiction`, `no_response`, `fraud_suspected`, `policy`.

        Request codes (also on `requirements.documents_due[].reason_code` and on
        the `requirement.due` webhook `reason`): `document_missing`,
        `document_invalid`, `data_inconsistent`, `capacity_unproven`,
        `identity_session_needed`, `half_of_limit_reached`, `registration_incomplete`,
        `destination_unidentified`.

        `destination_unidentified` asks who holds a wallet the customer sent crypto to:
        the customer's own self-hosted wallet, or the exchange where the customer has an
        account. Answer it with `POST /customers/{customer_id}/destination-identifications`.
      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
        - registration_incomplete
        - destination_unidentified
    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`; and `tax_registration` and `pep_declaration`, the two a Brazilian payment institution requires for a BRL account).
        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
            the clock; OZAV still has to accept it. `accepted` - done. `rejected` - the
            file was refused; a new `due` line follows with the reason.
        due_at:
          type:
            - string
            - "null"
          format: date-time
          description: Deadline for `due` items; `null` when none applies.
        reason_code:
          $ref: "#/components/schemas/DecisionReasonCode"
        purpose:
          type: string
          enum:
            - limit_increase
            - account_opening
            - registration_data
            - travel_rule
          description: |
            `limit_increase` - this item does not hold the approval back. It is what OZAV
            needs to raise the customer's limit above the entry level (economic capacity
            not yet proven: bank statements, financial statements, tax return).
            `account_opening` - this item does not hold the approval back either. It is
            needed to open the customer's named account.
            `registration_data` - a registration field (declared income or revenue,
            expected volume) that completes the profile. It does not hold the approval
            back: until it arrives the risk score uses a conservative provisional value.
            `travel_rule` - who holds a destination wallet the customer sent crypto to
            (see `destination`). It does not hold the approval back and blocks no send.
            Answer it with `POST /customers/{customer_id}/destination-identifications`.
            A `due` line with any of these purposes does not turn `owner` to `integrator` nor
            `decision.status` to `information_requested`. Absent - the item is needed for
            approval.
        destination:
          type: object
          description: Only on a `travel_rule` item - the wallet the item asks about.
          required:
            - chain
            - address
          properties:
            chain:
              type: string
              description: The network, lowercase (e.g. `tron`, `ethereum`).
            address:
              type: string
              description: The destination address.
    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,
            information requested, an approved customer with a non-empty `due`
            (see the next field), or a `pending_review` customer whose analysis
            cannot start yet because the record is incomplete (`missing_fields`)
            or the hosted verification session of a `full` customer has not
            completed. A missing `identity_document` - the individual's own, or
            an associated person's - also puts `owner` at `integrator`, under
            `reliance` too. `ozav` - OZAV is reviewing; nothing to do. `none` - the
            decision is final and nothing is owed: a rejection, or an approval
            whose `due` is empty.

            Read `documents_due`, `due`, `missing_fields` and `decision.status`
            together for what is actually owed. In a list read, `owners` and
            `representative` are reported in `missing_fields` for every business
            (associated persons are not counted there) and do not flip `owner` on
            their own; read the single customer for those two.

            An approved customer with a non-empty `due` returns `integrator`, not
            `none`: there is still something for you to send. A rejected customer keeps
            `none` even with a non-empty `due`: the list survives the verdict, and
            no field reverses a rejection.
        documents_due:
          type: array
          description: |
            Every document OZAV's compliance has asked for on this customer - the ask,
            its current `status` and its deadline - and why (`reason_code`).

            It does not shrink. A line stays after the file lands (`submitted`), after it
            is accepted (`accepted`) and after it is refused (`rejected`): this is the
            history of the asks, with where each one stands. To know whether anything is
            owed, read `status` per line, or read `owner` - `owner: none` means nobody
            owes anything right now.

            The `requirement.due` webhook announces a new ask; this field is the pull side
            of the same fact.
          items:
            $ref: "#/components/schemas/DocumentDue"
        associated_persons_due:
          type: array
          description: |
            Which associated persons are holding the business back, and which field is
            missing on each. Present only when there is at least one; absent means either
            nothing is missing or the people could not be read, and those are already
            covered elsewhere (`missing_fields` carries `owners` when none is registered).
            An empty array would claim "we checked and all of them are complete", which is
            more than is known when the read did not happen.

            OZAV screens every person who owns 25% or more, carries the role `ubo`, or signs
            for the company, and a screening needs a name, a tax id and a date of birth. While
            any of those is missing on any such person the business stays in review. The field names match what you send to
            `POST /customers/{customer_id}/associated-persons`, so the fix is a direct write.
          items:
            type: object
            properties:
              id:
                type: string
                description: The `associated_person_id` to correct.
              missing_fields:
                type: array
                description: Field names still needed on that person.
                items:
                  type: string
                  enum:
                    - legal_name
                    - tax_id
                    - date_of_birth
        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`.
            The 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.
            One more entry exists only on the organization's own account (the customer that represents your organization itself): `org_self_case_review`. It means OZAV compliance has not yet finished reviewing that account; there is nothing for you to send, and `owner` reads `ozav` while it is the only entry.
            Capability kinds and document requests can also appear here from the onboarding path; `documents_due` is the structured view of those, with deadline and reason.
            To 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.
            The vocabulary is 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`.
            `org_self_case_review` (see `due`) appears here unchanged - it is already a token.
            Only 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.
            For a Brazilian individual: `date_of_birth`, `phone`, `email`, `residential_address` (a full structured address), and `tax_id`. `mother_name` is not in this set - it is still accepted and stored, it just does not hold a customer at `pending`. 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).
            Independently 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.
            The 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.
            An individual also owes `occupation`, `nationality` and `account_purpose`. `is_pep` is not reported: the politically-exposed-person declaration is optional, and without it OZAV's own screening records the answer.
            `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.
            `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.
            A 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.
            An 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:
              - 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
              - regulator
              - regulated_activity_details
    CustomerCreateRequest:
      type: object
      required:
        - type
        - email
      description: |
        `email` is required. An `individual` customer additionally requires `individual.tax_id`;
        a `business` customer requires `business.registration` (enforced server-side).
      properties:
        type:
          type: string
          description: |
            Whether the customer is a natural person or a company:
            - `individual` - a person; supply the `individual` block (including `individual.tax_id`).
            - `business` - a company; supply the `business` block (including `business.registration`).
          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
          maxLength: 254
          description: Contact email for the customer. Required. At most 254 characters.
        phone:
          type: string
          maxLength: 32
          description: Contact phone number in E.164 form (e.g. +5511999998888). Optional. At most 20 digits.
        individual:
          $ref: "#/components/schemas/CustomerIndividual"
        business:
          $ref: "#/components/schemas/CustomerBusiness"
        verification_mode:
          type: string
          description: |
            The verification posture for this customer (see `CustomerVerification.mode`).
            Optional: omitted, the organization's posture applies - `reliance` when
            your organization holds the `kyc_reliance` product (the default for API
            integrators), otherwise `full`.
            - `reliance` - you verified the person and attest it; the end-customer does
              nothing. OZAV still screens against designated lists and assesses
              economic capacity, and approves automatically once the profile is
              complete, the partner document is `received` and screening is clear.
              OZAV terms (`terms_session_id`) are not required. Requires the
              `kyc_reliance` entitlement (`403 ENTITLEMENT_REQUIRED` otherwise).
            - `full` - OZAV verifies the end-customer in a hosted session
              (`POST /verifications`: document, liveness, face match).
          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
            for a `reliance` customer (the default posture for API integrators):
            the contract with the end-customer is yours. For `full` customers, mint
            it with `POST /customers/terms-sessions`, present `terms_url` to your
            end user, record the acceptance with `POST /customers/terms-sessions/accept`,
            then pass the same id here. Consumed at provisioning for personas that
            settle or hold funds: provisioning a settlement customer fails closed
            with `422 TERMS_ACCEPTANCE_REQUIRED` when there is no accepted session,
            and the customer stays unprovisioned (`status: pending`). Not required
            for compliance-only (verification-only) personas, which are never
            provisioned at a settlement layer.

            Sandbox: a test key never resolves this field - the attestation is
            simulated and any value (or none) is accepted. The gate is real only on
            `live`. Do not read a sandbox `201` as proof that your terms flow works.
        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
            has both custody products enabled.

            Omit it and the default applies: `self_custody` when your
            organization has it, otherwise `managed`. That default is why this
            field exists - before it, holding both products removed the choice
            instead of offering it, and every customer came out `self_custody`
            (no deposit address, and no custody balance for an off-ramp to
            debit).

            Asking for a model your organization has not contracted returns
            `403 ENTITLEMENT_REQUIRED` naming the missing product - never a
            silent fallback to the other model. When it resolves to
            `self_custody`, `self_custody.address` is required.
        metadata:
          $ref: "#/components/schemas/Metadata"
    CustomerSelfCustody:
      type: object
      required:
        - chain
        - address
      description: |
        Bring-your-own-wallet block for self-custody provisioning. Supply the
        end-user-controlled wallet address on the given chain; OZAV records it and
        never opens a managed wallet for it (the end-user holds the key). Consumed
        only when your organization holds the `self_custody` product; when the
        provisioning plan resolves to self-custody and this block is missing, the
        call fails closed with `422 SELF_CUSTODY_ADDRESS_REQUIRED` and the customer
        stays unprovisioned. The resulting wallet reports
        `custody_model: self_custody`.

        **How the plan resolves, and what to do when you did not ask for
        self-custody.** The create request does not choose a custody model. The
        model comes from the products your organization holds, and **self-custody
        outranks managed custody when both are enabled** - so an organization with
        both gets self-custody on every customer, and a request without this block
        is refused even though it never asked for self-custody.

        If you meant managed custody, that refusal is not something you can fix in
        the request body: ask OZAV to disable `self_custody` for your organization.
        The `422` message says this too, so the response is self-explanatory.

        **This block accepts more chains than `POST /wallets` does.** The two
        chain lists are different: the enum here is the wider one,
        and `POST /wallets` accepts a narrower set. Registering a second address
        later on one of the chains only this block accepts is refused. If a chain
        matters to you and it is not in the `POST /wallets` enum, ask before you
        design around it.
      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
            the active EVM networks listed in `Network`, plus Solana, Tron, and
            Stellar. The address must be valid on the chosen chain; OZAV records it
            as-is and never derives or holds a key for it.
        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
          maxLength: 254
          description: Replacement contact email. At most 254 characters.
        phone:
          type: string
          maxLength: 32
          description: Replacement contact phone number (E.164). At most 20 digits.
        individual:
          $ref: "#/components/schemas/CustomerIndividual"
        business:
          $ref: "#/components/schemas/CustomerBusinessUpdate"
        metadata:
          $ref: "#/components/schemas/Metadata"
    CustomerBusinessUpdate:
      type: object
      description: The `business` block of `PATCH /customers/{customer_id}`. The same fields as on creation, every one optional (send only what changes), plus the acceptance of the BRL account opening terms, which only exists here because the associated person who accepts is created after the customer.
      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.
        cnae_codes:
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            pattern: ^([0-9]{7}|[0-9]{4}-[0-9]/[0-9]{2})$
            example: 6201-5/01
          description: "Brazilian CNAE activity codes the company declares, either as 7 digits (`6201501`) or in the notation of the CNPJ card (`6201-5/01`). They are stored as digits only; `6201-5/01` and `6201501` are the same code, and the stored list never repeats one (`uniqueItems` describes the stored list: a request that repeats a code is accepted and stored once, and the 20-code limit counts codes after repeats are removed). The first one is the primary activity. Send an empty array to clear the list. Used only when the public registry (Receita Federal) does not return an activity code for the company's CNPJ; the registry always prevails, and a declared code is recorded as declared by the customer, never as official."
        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."
        expected_daily_volume_usd:
          type: number
          minimum: 0
          description: "Expected daily transaction movement in USD. A separate declaration, not the monthly figure divided by anything: account opening asks for both, and a derived number would be a value the customer never declared. 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.
        bc_protege_disabled:
          type: boolean
          description: "Brazilian business only: the company declares it has turned off BC Protege+ (the Central Bank of Brazil service that blocks institutions from opening accounts in the company's name; see https://www.gov.br/pt-br/servicos/proteger-se-com-o-bc-protege), or never turned it on. Required before a BRL account is requested for a business: without `true`, `POST /accounts` refuses with `422 ACCOUNT_PROFILE_INCOMPLETE`, with `business.bc_protege_disabled` as an `error.details[].param` (code `missing_field`; sandbox too). Only the company can turn the service off, with its own gov.br login; send `true` once it has. Absent = not declared."
        account_opening_terms_accepted_by:
          type: string
          writeOnly: true
          description: "Brazilian business only: the id of the associated person of this customer (pending or approved, not suspended) on whose behalf the business accepted the BRL account opening terms. The person needs a legal name and a valid Brazilian CPF in `individual.tax_id`; otherwise `422 REPRESENTATIVE_TAX_ID_REQUIRED` (a person created from the onboarding documents cannot be given a legal name or a CPF through the API: name another person or contact OZAV support). Any other id returns `422 REPRESENTATIVE_NOT_FOUND`. OZAV records the acceptance as declared by your organization through the API (when, and with which API key) and delivers the institution's term on its next account-opening pass. Sending the same person again, with the same name and CPF, records no new acceptance (the update itself still applies and emits `customer.updated`); a different person, or a corrected name or CPF, records a new one. One declared with a `test` key counts in the sandbox only. The field is write-only and never returned. When the institution that opens the account asks for these terms, `POST /accounts` refuses a BRL account without the acceptance with `422 ACCOUNT_PROFILE_INCOMPLETE`, with `business.account_opening_terms_accepted_by` as an `error.details[].param` (code `missing_field`; sandbox too). You can send it before `POST /accounts` for any Brazilian business: when the institution does not ask for the terms, the acceptance is recorded and has no effect."
    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:
        limits:
          description: The limits that apply to this customer, returned only by `GET /customers/{customer_id}`; lists and create responses leave it out. `null` means the limits could not be read right now - retry, and never read it as zero. Amounts are in USD.
          oneOf:
            - type: "null"
            - type: object
              required:
                - organization
                - customer
              properties:
                organization:
                  description: The limits contracted for your organization, shared by all your customers. `null` when no organization limit is set.
                  oneOf:
                    - type: "null"
                    - type: object
                      required:
                        - onramp_request_max_usd
                        - withdraw_tier_usd
                        - withdraw_daily_max_usd
                        - since
                      properties:
                        onramp_request_max_usd:
                          type:
                            - number
                            - "null"
                          description: The largest single purchase request.
                        withdraw_tier_usd:
                          type:
                            - number
                            - "null"
                          description: Withdrawals up to this amount need no manual approval.
                        withdraw_daily_max_usd:
                          type:
                            - number
                            - "null"
                          description: The most that can be withdrawn per day.
                        since:
                          type:
                            - string
                            - "null"
                          format: date-time
                          description: When these limits took effect.
                customer:
                  description: This customer's own limit currently in force. `null` when none is in force.
                  oneOf:
                    - type: "null"
                    - type: object
                      required:
                        - monthly_usd
                        - per_transaction_usd
                        - since
                      properties:
                        monthly_usd:
                          type:
                            - number
                            - "null"
                          description: The most this customer can move per calendar month.
                        per_transaction_usd:
                          type:
                            - number
                            - "null"
                          description: The most per single operation.
                        since:
                          type:
                            - string
                            - "null"
                          format: date-time
                          description: When this limit took effect.
        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`.
            An 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.
            Read 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"
        still_required:
          type: array
          items:
            $ref: "#/components/schemas/CustomerDocumentType"
          description: |
            The documents this customer will need before an account can be opened for them,
            given their type and country of registration. Returned on `POST /customers` so you
            learn the list when the customer is created, not when the account is refused.

            This is not `requirements.documents_due`, and the difference matters. `documents_due`
            is what OZAV has formally asked for, each item with a deadline and a reason; right
            after creation it is empty, because nothing has been asked yet. `still_required` is
            what the corridor will demand regardless - a Brazilian company needs seven documents
            (plus four per associated person, reported by `GET /customers/{customer_id}/documents`
            in `still_required_by_person`) whether or not anyone has asked.

            Absent, never `[]`, for a customer whose requirement set is not modelled: an empty
            list would assert "nothing is missing", and that promise is only safe to make for the
            shapes the gate actually models. The same field, computed the same way, is returned by
            `GET /customers/{customer_id}/documents` once documents start arriving.
    PendingDocumentsOfPerson:
      type: object
      description: One associated person and the documents that person still owes.
      required:
        - associated_person_id
        - still_required
        - pending
      properties:
        associated_person_id:
          type: string
          description: The person's public id (`ozav_per_…`), as returned by `GET /customers/{customer_id}/associated-persons`.
          example: ozav_per_01J8Z9K2M3N4P5Q6R7S8T9U0V1
        name:
          type: string
          description: The person's full name, when recorded.
        still_required:
          type: array
          items:
            $ref: "#/components/schemas/CustomerDocumentType"
        pending:
          type: array
          items:
            $ref: "#/components/schemas/PendingDocument"
    PendingDocument:
      type: object
      description: |
        One document category the customer still owes, and why it is still owed.

        **The whole point of this object is the difference between its two
        states.** `missing` means the bytes never arrived - upload the document.
        `rejected` means they arrived and failed review - fix the document and
        upload it again. `still_required` cannot tell you which, so an
        integrator reading only that field resends the same rejected file.

        The category vocabulary is the same `doc_type` you use to upload, so
        what you read here is what you send back.
      required:
        - doc_type
        - state
      properties:
        doc_type:
          $ref: "#/components/schemas/CustomerDocumentType"
        state:
          type: string
          enum:
            - missing
            - rejected
          description: |-
            `missing` - never uploaded, or declared and never uploaded. A document counts only once its bytes arrive, so one you created but never finished uploading is `missing`, not `rejected`.

            `rejected` - uploaded and failed review. Do not resend the same file; correct it first.
        reason:
          type: string
          description: "Why the document was rejected, in text meant for a person to act on. Present only when `state` is `rejected`, and only when a reason was recorded: some older rejections have none, and OZAV does not invent one. Its absence never means the document was accepted - `state` is what says that."
          example: Issued more than 90 days ago.
      example:
        doc_type: proof_of_address
        state: rejected
        reason: Issued more than 90 days ago.
    CustomerDocumentType:
      type: string
      description: |
        The kind of KYC/KYB document. Jurisdiction-agnostic - generic
        categories, never country-specific artifact names -
        because this API onboards your customers worldwide.

        **What this list is, and what it is not.** It is the vocabulary for
        declaring what you are uploading. It is not a per-country list of which
        artifact satisfies which category: whether a given national ID, driving
        licence or utility bill is accepted for a specific customer is decided
        in review, not by this enum, and OZAV does not publish a per-jurisdiction
        acceptance table. Declare the category that best describes the artifact
        and upload it; if something is missing or unusable you get a named
        outcome on the verification, not a silent rejection.

        The fields a customer must supply are a separate question, answered by
        `requirements.missing_fields` on the customer itself. That set is driven
        by the country on `tax_id` / `registration`, and it does not vary by the
        currency of any account you later open.

        **Required versus optional, and why nothing here blocks.** For an
        international account the receiving institution asks for a defined set of
        company information. Some categories below satisfy part of that set and
        some do not apply to every company - a Brazilian company, for example, has
        no separate memorandum of association, because its Contrato Social is both
        the constitutive act and the articles.

        So this API accepts every category at any time and refuses none of them for
        being absent. Upload what the company has, under the category that
        describes it.

        **What this API does not do yet, said plainly.** `requirements` on
        `GET /customers/{id}` reports missing onboarding *fields*, not missing
        documents - it will not tell you which of the categories above are still
        outstanding. Document review happens at the receiving institution, and
        today its outcome reaches you through your account contact, not through
        this API. Publishing the list here without saying that would suggest a
        completeness signal that does not exist.

        A company that cannot produce one of these is not blocked from being
        created. It is blocked, later and visibly, from having an account opened -
        and the difference matters: the first is a wall at the door, the second is
        a list you can work through.

        **Three categories for the Brazilian named account.** `tax_registration`
        is the company's tax registration certificate (in Brazil, the Cartão
        CNPJ). `pep_declaration` is a politically-exposed-person declaration,
        one per partner or legal representative (`subject_type:
        associated_person`), optional except for a BRL account. `income_tax_return` is the company's income tax
        return, accepted as evidence of economic capacity alongside
        `financial_statement`. A bank statement is **not** accepted as economic
        capacity by the Brazilian receiving institution; send a balance sheet or
        the tax return instead.
      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
        - tax_registration
        - pep_declaration
        - income_tax_return
        - 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"
        label:
          type: string
          minLength: 1
          maxLength: 120
          description: "Free-text label describing what this file is, in your own words. Strongly recommended whenever `doc_type` is `other`, because without it the document is indistinguishable from any other `other` in review. It complements `doc_type` and never replaces it: the type is what drives requirements, the label is what a human reads."
          example: Articles of Association (2025 amendment)
        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.
    LimitRequestCreate:
      type: object
      additionalProperties: false
      required:
        - monthly_usd
      properties:
        monthly_usd:
          type: string
          pattern: ^\d{1,12}(\.\d{1,2})?$
          description: The monthly limit you ask for, in USD, as a decimal string.
          example: "50000.00"
        per_transaction_usd:
          type:
            - string
            - "null"
          pattern: ^\d{1,12}(\.\d{1,2})?$
          description: Optional per-transaction limit, in USD. Cannot exceed `monthly_usd`.
          example: "10000.00"
        reason:
          type:
            - string
            - "null"
          maxLength: 500
          description: Why the customer needs more. Short; no personal data.
    DestinationIdentificationRequest:
      type: object
      description: |
        Who holds a destination wallet the customer sent crypto to. The vocabulary is the
        one of `TravelRule.beneficiary`: `self_custodied` is the customer's own wallet,
        `external` is the customer's account at an exchange.
      required:
        - chain
        - address
        - wallet_type
      properties:
        chain:
          type: string
          description: The network of the address, with the name the send used (e.g. `tron`, `ethereum`, `solana`). Any other spelling is refused with `422`, because it would never match the send.
          example: tron
        address:
          type: string
          description: The destination address. EVM addresses are matched case-insensitively.
          example: TGssfnTouNs2H9AYPRkeZo9zuRxTPB5KCq
        wallet_type:
          type: string
          enum:
            - self_custodied
            - external
          description: "`self_custodied` - the customer's own wallet (holds the keys). `external` - the customer's account at an exchange."
        wallet_attested_ownership_at:
          type: string
          format: date-time
          description: Required with `self_custodied` - when the customer attested owning the wallet. Must not be in the future.
        exchange:
          type: object
          description: Required with `external` - the exchange where the customer has the account.
          required:
            - name
          properties:
            name:
              type: string
              maxLength: 140
              description: Legal or trade name of the exchange.
            lei:
              type: string
              pattern: ^[0-9A-Z]{20}$
              description: The exchange's Legal Entity Identifier, when known.
    DestinationIdentification:
      type: object
      description: |
        A destination wallet identified for a customer. It applies to every send of that
        customer to the same `chain` and `address`, past and future. Posting again for the
        same wallet records a new identification; the latest one applies. It does not
        allow or block any send.
      required:
        - object
        - customer_id
        - chain
        - address
        - wallet_type
        - declared_at
      properties:
        object:
          type: string
          const: destination_identification
          description: Resource type discriminator; always `destination_identification`.
        customer_id:
          type: string
          description: The `ozav_cus_` id of the customer.
        chain:
          type: string
          description: The network, lowercase.
        address:
          type: string
          description: The address as stored (EVM in lowercase).
        wallet_type:
          type: string
          enum:
            - self_custodied
            - external
          description: Who holds the wallet.
        wallet_attested_ownership_at:
          type:
            - string
            - "null"
          format: date-time
          description: Present with `self_custodied`.
        exchange:
          type: object
          description: Present with `external`.
          properties:
            name:
              type:
                - string
                - "null"
              description: Name of the exchange.
            lei:
              type:
                - string
                - "null"
              description: LEI of the exchange, when given.
        declared_at:
          type:
            - string
            - "null"
          format: date-time
          description: When the identification was recorded.
        requirement_satisfied:
          type: boolean
          description: Only on the create response - `true` when it answered an open `destination_unidentified` item of the customer.
    DestinationIdentificationList:
      type: object
      required:
        - object
        - data
      properties:
        object:
          type: string
          const: list
          description: Always `list`.
        data:
          type: array
          description: The customer's identifications, newest first (up to 200).
          items:
            $ref: "#/components/schemas/DestinationIdentification"
    CustomerDeactivation:
      type: object
      description: Whether you deactivated the customer through the API. It reflects only your own deactivation.
      required:
        - object
        - customer_id
        - deactivated
        - deactivated_at
      properties:
        object:
          type: string
          const: customer_deactivation
          description: Resource type discriminator; always `customer_deactivation`.
        customer_id:
          type: string
          description: The `ozav_cus_` id of the customer.
        deactivated:
          type: boolean
          description: "`true` while your deactivation is in force."
        deactivated_at:
          type:
            - string
            - "null"
          format: date-time
          description: When you first deactivated the customer; `null` when active.
    LimitRequest:
      type: object
      required:
        - id
        - object
        - customer_id
        - status
        - requested
        - reason
        - documents_due
        - approved_limit
        - created_at
        - updated_at
        - decided_at
      properties:
        id:
          type: string
          example: ozav_lir_01J8Z9K2M3N4P5Q6R7S8T9U0V1
        object:
          type: string
          enum:
            - limit_request
        customer_id:
          type: string
          example: ozav_cus_01J8Z9K2M3N4P5Q6R7S8T9U0V1
        status:
          type: string
          description: |-
            `awaiting_documents` while a proof in `documents_due` is missing;
            `in_review` once everything arrived and OZAV is deciding;
            `approved` when the new limit is in force (see `approved_limit`);
            `declined` when OZAV did not raise the limit (no reason is given);
            `canceled` when the customer stopped being eligible (rejected, closed
            or suspended) while the request was open.
          enum:
            - awaiting_documents
            - in_review
            - approved
            - declined
            - canceled
        requested:
          type: object
          required:
            - monthly_usd
            - per_transaction_usd
          properties:
            monthly_usd:
              type:
                - string
                - "null"
              example: "50000.00"
            per_transaction_usd:
              type:
                - string
                - "null"
              example: "10000.00"
        reason:
          type:
            - string
            - "null"
        documents_due:
          type: array
          description: |-
            The proofs of economic capacity this request still needs. Send each
            one with `POST /customers/{customer_id}/documents` using its
            `doc_type`. Empty once the request is decided.
          items:
            type: object
            required:
              - key
              - doc_type
              - label
              - status
            properties:
              key:
                type: string
                example: bank_statement
              doc_type:
                type:
                  - string
                  - "null"
                example: bank_statement
              label:
                type: string
              status:
                type: string
                description: "`due` - send it; `submitted` - received, under review; `accepted` - an OZAV review accepted a document sent after the request."
                enum:
                  - due
                  - submitted
                  - accepted
        approved_limit:
          description: The limit now in force. Only when `status` is `approved`; it can be lower than requested.
          oneOf:
            - type: "null"
            - type: object
              required:
                - monthly_usd
                - per_transaction_usd
              properties:
                monthly_usd:
                  type:
                    - string
                    - "null"
                  example: "40000.00"
                per_transaction_usd:
                  type:
                    - string
                    - "null"
                  example: "10000.00"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        decided_at:
          type:
            - string
            - "null"
          format: date-time
    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
        statement, proof of income, company document). Identity documents and
        selfies are verified in the hosted verification session instead and
        read `not_reviewed` here.

        `rejected` comes with a closed `reason_code` and its English label; a
        fresh `requirements.documents_due` line follows with a new deadline. The
        `document.reviewed` webhook carries the same outcome. Omitted (not
        `not_reviewed`) when the review could not be read at response time.
      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).
            - `in_review` - waiting on a compliance officer.
            - `accepted` - the document satisfies the requirement.
            - `rejected` - refused; see `reason_code`. Send a new file.
        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
        - holder_in_other_script
        - expired
        - address_mismatch
        - low_confidence
        - tampering_signals
        - missing_period
        - not_registered
        - registry_mismatch
        - outdated_version
        - ownership_not_evidenced
        - holder_shares_address
        - 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
                endpoint.

                **Send your API key on this PUT as well.** The embedded `upload_token` scopes the
                grant to this one customer/document pair and expires on its own, but it does not
                replace authentication - a `PUT` without `X-API-Key` (or `Authorization: Bearer`)
                is rejected with `MISSING_API_KEY`.

                Because the key is required, **this URL is for your server, not your browser**: do
                not hand it to an end user's device, which would mean shipping your secret key with
                it. Upload the bytes from your backend.
            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:
            - `ubo` - ultimate beneficial owner (an owner above the ownership threshold).
            - `director` - a member of the board or a statutory director.
            - `officer` - an executive officer (e.g. CEO, CFO).
            - `authorized_signer` - authorized to act or sign on the account.
          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
          maxLength: 254
          description: Contact email for this person. Optional. At most 254 characters.
    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/AssociatedPersonIndividualUpdate"
        email:
          type: string
          format: email
          maxLength: 254
          description: Contact e-mail of the person. The institution that opens the account asks for it from every representative of a company. It can be set or replaced here at any time, also on a person created from the onboarding documents (which accepts only `email`, `individual.phone`, `individual.gender` and, when missing, `individual.residential_address` and `individual.date_of_birth`).
    AssociatedPerson:
      type: object
      description: |
        A person linked to a business customer for KYB purposes.

        `verification.mode` is **inherited from the customer**, never set per
        person: an associated person has no verification posture of its own, so
        a customer onboarded under `reliance` reports `reliance` here too.

        When the customer has no recorded posture, `mode` is **omitted**. Absent
        is not `full`: it means OZAV has no posture on record for that customer,
        and `status` must not be read as a verification attestation while `mode`
        is absent.
      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`,
            `authorized_signer` (see `AssociatedPersonCreateRequest.roles`).
          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
            for the customer they belong to.

            A business account is refused while a qualifying owner is incomplete, and
            that refusal names the person. This field is the same information delivered
            when you create the person, instead of when you request the account.

            The set depends on the customer's **jurisdiction**. A
            person attached to a Brazilian company owes the full KYB set:
            `individual.legal_name`, `individual.date_of_birth`, `individual.tax_id`,
            `email`, `individual.phone` and `individual.residential_address`, with the
            address complete: `line1`, `number`, `neighborhood`, `city`, `state` and
            `postal_code`. Outside Brazil only `individual.legal_name` and
            `individual.date_of_birth` are owed.
        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
        client-selected spread for the quote, in integer basis points. OZAV cost,
        floor, margin, split and cost analytics are internal and are not
        serialized in public API responses.
      required:
        - spread_bps
      properties:
        spread_bps:
          type: integer
          minimum: 0
          description: Additional client-selected spread, in integer basis points (100 bps = 1%).
        rate_without_spread:
          type: string
          description: |
            The same quote before the `spread_bps` you selected, in the same
            scale as `rate`. When you did not select a spread it equals `rate`
            exactly.


            Use it to tell the spread you added apart from the OZAV rate. If you are
            comparing our quote to a market reference, compare
            `rate_without_spread`: `rate` already carries whatever spread you
            asked for.


            **To read your spread back exactly, measure both against the
            quote, not against each other.** Both rates are derived from the same
            internal base, so dividing one by the other returns slightly more
            than `spread_bps` (with a 10 bps floor, a 40 bps spread reads as
            40.04). The value that is exact is `spread_bps` itself.


            Absent when the quote does not allow deriving it. OZAV cost, floor
            and margin stay inside both rates and are never serialized.
        amount_deducted:
          type: string
          description: |
            The amount taken off `source.amount` before conversion, in the
            source currency. Present only when an execution cost (the delivery
            network cost, the local payment rail cost and, on purchases, the
            settlement safety margin) is charged as an amount instead of being
            carried by the rate. Today that is limited to
            stablecoin purchases (fiat to USDT/USDC) on accounts configured for
            it, so its presence depends on the account, not on the corridor alone.
            The network cost depends on `target.chain`: send it. Sandbox does
            not emit it yet.


            On a transfer created from such a quote, `amounts.rate` is the all-in
            rate (`target.amount / source.amount`, 8 decimal places), so the
            transfer reconciles on its own, to that precision, without this field.


            With it, `(source.amount - amount_deducted) * rate` equals
            `target.amount`. That is the reconciliation to run when both are
            present. Absent, the plain `source.amount * rate` holds, as before.


            **It is published at the source currency's accounting precision,
            rounded UP.** The reconciliation therefore closes to within one minor
            unit of the source, and never resolves to more than `target.amount`
            promises. Rounding it down would let the arithmetic overstate what
            you receive.


            **It is not an OZAV charge, and it is not our cost.** It is what
            was deducted from this quote. OZAV cost, floor and margin stay
            internal and are never serialized.
    Quote:
      type: object
      description: An FX quote - opaque, indicative (`floating`), 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.

            - `floating` - indicative; the rate re-derives at execution time. **Every quote
              returns this today**, and it comes with `expires_at: null`: a firm price holds
              for only a few seconds, so OZAV re-quotes at execution instead of expiring your
              quote. Treat `rate` as indicative: what
              executes can differ from the quote.
            - `locked` - held fixed until `expires_at`. **Not served on this API yet.** When it
              ships it will require the price-lock facility enabled on your account, and will
              carry a real `expires_at`. Ask OZAV if you need a firm price today.

            Do not branch on `expires_at` being non-null: on `floating` it is always `null`,
            in sandbox and in production alike.
          enum:
            - floating
            - locked
        rate:
          type: string
          description: "The FX rate as a decimal string - target-currency units per one source unit, all-in except for any `amount_deducted` (it already carries any `spread_bps` you added). Indicative on a `floating` quote, which is every quote today: it re-derives at execution. On the quote, `source.amount x rate` gives `target.amount` (with `amount_deducted` present, `(source.amount - amount_deducted) x rate`)."
        source:
          $ref: "#/components/schemas/MonetaryAmount"
        target:
          allOf:
            - $ref: "#/components/schemas/MonetaryAmount"
          description: |
            The amount on the target side at the quoted `rate`: `source.amount x rate`, or `(source.amount - amount_deducted) x rate` when `amount_deducted` is present. When you fix `target.amount` it stays as you sent it and `source.amount` carries the rate instead.

            Like the rate, it is indicative on a `floating` quote: what executes can differ.
        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` on a `floating` quote is the normal answer, not a missing
            value, and it does not mean the quote is valid forever.** A floating
            rate is indicative: it re-derives at execution, so there is no window
            to "accept" it within. Quote as close to execution as you can, and
            read the rate you are given at execution as the one that applies.

            A timestamp appears only when the rate is held: a `locked` quote,
            which expires at this instant and is refused after it.
        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"
        via:
          type: object
          description: "Present only on a quote requested with `via`: the stablecoin and the network the conversion passes through."
          required:
            - currency
            - chain
          properties:
            currency:
              type: string
            chain:
              type: string
        legs:
          type: array
          minItems: 2
          maxItems: 2
          description: "Present only on a quote requested with `via`. The two legs, in order: BRL → stablecoin, then stablecoin → target. The second leg is priced on the stablecoin the first delivers, rounded down to the cent; a remainder below 0.01 stays in your customer's wallet. The top-level `source` is the first leg's source and `target` the second leg's target."
          items:
            $ref: "#/components/schemas/QuoteLeg"
    QuoteLeg:
      type: object
      description: One leg of a quote requested with `via`, in the same shape as a quote, without its own id or expiry.
      required:
        - source
        - target
        - rate
        - exchange_conditions
      properties:
        source:
          $ref: "#/components/schemas/MonetaryAmount"
        target:
          $ref: "#/components/schemas/MonetaryAmount"
        rate:
          type: string
          description: Target units per one source unit, for this leg.
        exchange_conditions:
          $ref: "#/components/schemas/ExchangeConditions"
    FacilityUnavailable:
      type: object
      description: |
        **Why the rate-lock preview is not available for this account.**

        Returned 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.

        **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.

        **This preview runs the offer gates only** - the ones that decide whether a lock can be shown. It 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.

        | `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_too_short` | Today's cutoff is too close to leave a usable delivery window (under ten minutes). | 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. |
      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 in English, safe to show an operator. It includes `(reason: <slug>)`. May change without notice."
    FacilityPreview:
      type: object
      description: |
        **The FX rate lock conditions (`trava de câmbio`), previewed alongside the quote.**

        Returned 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.

        **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`.

        **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.
      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 implied by this quote, at this instant - indicative. The binding lock is priced at acceptance (`POST /transfers` with `facility_terms_accepted: true`) from a firm quote, and what the customer receives comes back as `facility.amount` on the `201`, net of your organization's exchange conditions. Do not present `locked_rate` as the final price. The rate that would be locked, at this instant, always expressed as **BRL per unit of the stablecoin** - the same base the acceptance records. On an off-ramp (stable to BRL) it is `expected_amount / source_amount`; on an on-ramp (BRL to stable) it is `source_amount / expected_amount`. Never a ratio in the other direction."
        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` - the quote's target currency: centavos (2 decimals) when the target is BRL, micro-units (6 decimals) when the target is a stablecoin such as USDC or USDT. 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`: the quote's target. `BRL` on an off-ramp preview, the stablecoin on an on-ramp preview. Read it before interpreting the amount - the minor unit follows it."
        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
        `awaiting_payment → payment_confirmed → converting → payout_initiated →
        completed`; the remaining states are terminal failures or attention states.

        | Value | Meaning |
        |---|---|
        | `awaiting_payment` | Transfer created; awaiting the customer's inbound payment. |
        | `payment_confirmed` | The inbound payment has been confirmed. |
        | `converting` | Funds are in flight and the FX conversion is underway (not yet confirmed). |
        | `payout_initiated` | The payout to the destination has been initiated. |
        | `completed` | Terminal success - funds delivered to the destination. |
        | `failed` | Terminal failure - no funds delivered. |
        | `cancelled` | Terminal - the transfer was cancelled. |
        | `expired` | Terminal - the transfer expired before completing. |
        | `needs_review` | Stuck and needs attention (e.g. underfunded, returned, in review); not terminal. |
        | `awaiting_approval` | Not returned: no transfer waits on approver keys. |
      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
        `source_leg` and `destination_leg`, each with one of these values.

        | Value | Meaning |
        |---|---|
        | `pending` | Accepted; awaiting the payer's funds. Non-terminal. |
        | `processing` | Funds in flight (received, or payout initiated). Non-terminal. |
        | `needs_review` | Stuck / needs attention (underfunded, in review, returned). Non-terminal. |
        | `completed` | Terminal success - the leg settled. |
        | `failed` | Terminal failure - the leg did not settle. |
        | `cancelled` | Terminal - the leg was cancelled. |
      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
        customer already deposited into that account, and the order is funded from its balance.
        No payment instructions are returned, because there is nothing left to pay.

        The account must be active and known to its institution, and the amount must fit the
        balance that is not already committed to the customer's other open orders. In the sandbox,
        a test key can use a sandbox account after `POST /accounts/{account_id}/simulate/credit`.
        Choose `fiat_payment` instead when the customer has not deposited yet and needs payment
        instructions to send the money.
      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: 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 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: "Accepted and ignored: the payment is always received through the standard flow. Omit it."
        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.
    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
        stablecoin is sold and the fiat is credited to that account, where it stays until the
        customer moves it. Available for USD, EUR and GBP accounts that are active and known to
        the account's institution; the quote's target currency must be the account's currency.

        Choose this when the customer wants to hold a fiat balance rather than pay a third
        party; choose `payout_destination` to pay out to a registered external account.
      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. A managed wallet serves several networks, so a BRL Pix on-ramp into one requires it: without it the transfer is refused with `DESTINATION_CHAIN_REQUIRED` before any charge is created.

            **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.

            The refusal can also mean the pair itself has no registered treasury destination (`USDC:tron`, for instance, is not a pair OZAV serves). 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.

        The bytes travel in the request rather than through a signed upload URL
        because this is one small file per payout, and a single call leaves no
        window in which the document is registered and its bytes are absent,
        a state that reads as "sent" and is not.
      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
            invoice, and a file its compliance review cannot accept comes back as
            a request for information days later, by email. Refusing here is an
            immediate message to whoever still has the file in hand.
          enum:
            - application/pdf
        content_base64:
          type: string
          minLength: 1
          description: |
            The file, base64-encoded, without a `data:` prefix. Maximum 10 MB
            decoded. Content that does not decode, or decodes to zero bytes, is
            refused with `400 DOCUMENT_CONTENT_INVALID` before anything is stored.
          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.
    TravelRule:
      type: object
      description: |
        Originator and beneficiary information for a crypto send, required by BCB
        Resolution 520/2025 when value leaves custody to an external address.

        **Read the "Required for an external crypto send" list below before building
        this object.** The fields marked required *there* are not the same as the ones
        marked required *here*: this schema is shared with paths where Travel Rule is
        supplementary, so it stays permissive. The mandatory set is enforced on
        `destination.type = crypto_address`, and a request that misses any of it is
        refused with `422 TRAVEL_RULE_REQUIRED` and a `details.missing_fields` array
        naming each one.

        ### Required for an external crypto send

        | field | rule |
        |---|---|
        | `originator.name` | non-blank |
        | `originator.identity_documents[].type` | at least one entry must be accepted for the customer's country - see the table below |
        | `originator.identity_documents[].number` | non-blank; for `cpf` and `cnpj`, the check digits must be valid (`cnpj` accepts the alphanumeric format) - otherwise `missing_fields` names `originator.identity_documents.number(digito_verificador_invalido)` |
        | `originator.identity_documents[].issuing_country` | required on **every** entry |
        | `originator.address.street_line_1` | non-blank (`street` and `line1` are accepted aliases) |
        | `originator.address.city` | non-blank |
        | `originator.address.country` | non-blank |
        | `beneficiary.name` | non-blank |
        | `beneficiary.wallet_type` | `external` or `self_custodied` (`hosted` is rejected on this path) |
        | `beneficiary.wallet_attested_ownership_at` | ISO-8601 timestamp of when the holder attested ownership; must not be in the future |
        | `beneficiary.is_self` | boolean; `true` when the destination belongs to your own customer |

        #### Accepted originator identity documents, by country

        The requirement follows the customer's country of residence as held on the
        customer record - **not** the country you send inside `travel_rule`, which is
        stored but never selects the regime. A country not listed below uses the
        default set.

        To be precise about where that country comes from: you declare it when you
        create the customer (`individual.tax_id.country`, or the address), and you can
        correct it later with `PATCH /customers/{id}`. Both paths refuse a jurisdiction
        OZAV does not serve. So the regime is not selectable *per request*, but it does
        follow what you declared for that customer - keep it accurate.

        | country | accepted `identity_documents[].type` |
        |---|---|
        | `BR` | `cpf`, `cnpj` |
        | `NG` | `bvn`, `nin`, `passport`, `national_id` |
        | *(default)* | `passport`, `national_id` |

        If your customer's country is not listed and the default set does not fit
        its national identifiers, tell us and it gets modelled - the list is data,
        not code.

        A destination that is an OZAV wallet id (`ozav_wal_...`) is exempt: the value
        stays in custody and both sides are already verified.

        Values inside `originator` and `beneficiary` must be plain strings - do not
        embed internal or third-party references.
      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
        needs; see `TravelRule` for the stricter set an external crypto send enforces.
      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
            must carry at least one entry whose type is accepted for the customer's
            country (see "Accepted originator identity documents, by country").
            Accepted under the legacy name
            `identifying_information` as well; both normalize to the same record.
          items:
            type: object
            required:
              - type
              - number
              - issuing_country
            properties:
              type:
                type: string
                description: |
                  Document kind, lowercase. Which kinds satisfy the originator
                  requirement depends on the customer's country - see "Accepted
                  originator identity documents, by country" above. Other kinds are
                  accepted and stored but do not satisfy it on their own.
                example: cpf
              number:
                type: string
                description: |
                  The document number. Non-blank. For `cpf` and `cnpj` on an external
                  crypto send, the check digits are validated; `cnpj` accepts the Receita
                  Federal alphanumeric format (12 alphanumeric + 2 numeric check digits).
                  Punctuation is ignored.
                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.
            `street_line_1` is the canonical key; `street` and `line1` are accepted aliases.
          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
            must be `external` or `self_custodied`; `hosted` is rejected there.
        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
            wallet) rather than a third party.
    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: Not returned. No transfer waits on approver keys.
        status_detail:
          type: object
          description: Optional 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 this field existed; treat absence as "not stated", never as "no reason". It never contains internal 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 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 and rate of this transfer. They start from the quote when the transfer is created (when the quote carried `exchange_conditions.amount_deducted`, `rate` is the all-in `target.amount / source.amount`, published at 8 decimal places, and differs from the quote's `rate` by the deducted cost), and on some paths they are updated at settlement: on the fiat-to-stablecoin path (`fiat_payment` to `wallet`) `source`, `target` and `rate` are rewritten from the settled amounts as the operation progresses, before the matching `transfer.*` webhook is sent. On the other paths they keep the quoted values, which are indicative on a `floating` quote. Read them again from `GET /transfers/{transfer_id}` or the webhook after the transfer settles; do not cache the values returned at creation.
          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"
        settlement_references:
          $ref: "#/components/schemas/TransferSettlementReferences"
        beneficiary_bank:
          $ref: "#/components/schemas/TransferBeneficiaryBank"
    TransferSettlementReferences:
      type: object
      readOnly: true
      description: |
        **The references the payment rail produced for this transfer**, the ones a bank or an auditor asks for to trace the payment. Returned by `GET /transfers/{transfer_id}` and `GET /transfers` for off-ramp transfers (a payout to a `payout_destination` or to your customer's own account) and for wallet sends (a transfer to a `wallet` or a `crypto_address`).

        **Only the keys that exist are present.** A reference appears once the payout step recorded it, never earlier and never made up; a key that does not apply to the rail is simply absent, and the whole object is absent while nothing has been recorded. Read the absence as "not available (yet)", never as "failed". The `transfer.*` webhook events and the `201` of a create do not carry it: read it again with `GET /transfers/{transfer_id}` after the transfer settles.

        | Key | Rail | What it is | |---|---|---| | `end_to_end_id` | Pix | The Pix end-to-end id (`E...`). | | `imad` | US wire | Input Message Accountability Data. | | `omad` | US wire | Output Message Accountability Data, when the wire reports it. | | `uetr` | SWIFT | The end-to-end transaction reference (UUID). | | `trace_number` | ACH | The ACH trace number. | | `ach_reference` | ACH | The ACH reference. | | `sepa_reference` | SEPA | The SEPA payment reference. | | `spei_reference` | SPEI | The SPEI tracking key. | | `tracking_number` | Other bank rails | The rail's tracking number. | | `tx_hash` | On-chain | The transaction hash, present only once the send is confirmed on-chain. | | `network` | On-chain | The network the transaction settled on (`ethereum`, `polygon`, `solana`, `tron`, `stellar`). Present with `tx_hash`. |
      properties:
        end_to_end_id:
          type: string
          description: Pix end-to-end id.
        imad:
          type: string
          description: US wire Input Message Accountability Data.
        omad:
          type: string
          description: US wire Output Message Accountability Data.
        uetr:
          type: string
          description: SWIFT end-to-end transaction reference.
        trace_number:
          type: string
          description: ACH trace number.
        ach_reference:
          type: string
          description: ACH reference.
        sepa_reference:
          type: string
          description: SEPA payment reference.
        spei_reference:
          type: string
          description: SPEI tracking key.
        tracking_number:
          type: string
          description: Tracking number of the bank rail.
        tx_hash:
          type: string
          description: On-chain transaction hash.
        network:
          type: string
          description: Network the on-chain transaction settled on.
    TransferBeneficiaryBank:
      type: object
      readOnly: true
      description: |
        **The receiving bank of a fiat payout, masked.** Returned by `GET /transfers/{transfer_id}` and `GET /transfers` for off-ramp transfers sent to a bank account. Account numbers are never returned whole: `account_masked` is `****` plus the last four characters, and `iban_masked` keeps the country and check digits and the last four. Only the keys that are known are present; the object is absent when the destination is not a bank account (a Pix key, for instance) or the bank details were not recorded.
      properties:
        bank_name:
          type: string
          description: Name of the receiving bank.
        country:
          type: string
          description: Country of the receiving bank.
        routing_number:
          type: string
          description: US ABA routing number, a public bank identifier.
        account_masked:
          type: string
          description: Account number, last four characters only (`****1234`).
        iban_masked:
          type: string
          description: IBAN with the middle masked (`DE89**************3000`).
        bic:
          type: string
          description: SWIFT/BIC code of the receiving bank.
    FacilityOutcome:
      type: object
      description: |
        **What happened to the FX rate lock on this transfer.**

        Present 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.

        **Two limits worth knowing before you build on it.**

        **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`.

        **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".

        **Read `locked` before you present a price to the payer.** A lock can be requested and still not happen, and those cases return `201`: 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".

        | `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}`. |
      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 in English. May change without notice. 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: "**What the customer receives** at the locked conditions, net of your organization's exchange conditions - the same number the floating flow would credit for this order, fixed at acceptance and delivered exactly. Present only when `locked` is `true`."
        currency:
          type: string
          description: The currency of `amount` and `counterparty_amount`. Present only when `locked` is `true`.
        counterparty_amount:
          type: string
          description: The gross amount OZAV bought to cover this lock (the exposure). Informational, for reconciliation. It is not what the customer receives - `amount` is. 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
        currency, to one of the customer's registered payout destinations.
      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
            idempotency lever: the same `reference` with the same `amount` on the
            same account is the same payout. Also sent to the receiving bank as
            remittance information.
          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
        payment institution: `accepted` means the order was taken, not that the
        money arrived; `settled` means it did; `rejected` is final; `pending`
        means the institution has not classified the outcome yet - read the
        account's `/movements` rather than assuming either way.
      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
        documents are already on file. `approved` is the verification result and
        is not sufficient on its own: this endpoint reads the documents, and on
        `live` it refuses with `422 ACCOUNT_PROFILE_INCOMPLETE` naming each one
        still missing. Send them with `POST /customers/{customer_id}/documents`
        first. The sandbox does not enforce the documents (it does enforce, for
        a Brazilian business, `business.bc_protege_disabled` and, when the
        institution asks for the terms, `business.account_opening_terms_accepted_by`),
        so an account opening there proves the request is well-formed, never
        that the customer is ready.

        The URL stays the same across currencies, countries and rails;
        availability is selected by the account rail matrix below.
      x-ozav-account-rail-availability:
        - currency: BRL
          country: BR
          rail: pix
        - 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: USD
          country: US
          rail: swift
        - currency: USD
          country: GB
          rail: swift
      required:
        - customer_id
        - currency
      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
          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/RequestRail"
          description: Receiving rail for this account.
        metadata:
          $ref: "#/components/schemas/Metadata"
    AccountStatus:
      type: string
      description: |
        Public lifecycle state for a named account:
        - `pending` - opening is in progress or waiting for review.
        - `active` - receiving instructions can be shown to the customer.
        - `suspended` - account is blocked or deactivated.
        - `failed` - opening did not complete.
      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 FedWire.
        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`
            accounts.
        clabe:
          type: string
          description: |
            Mexican CLABE - 18 digits, including its own check digit. Present on `spei` accounts.
        bre_b_key:
          type: string
          description: |
            Colombian Bre-B key (the payer-facing alias). Present on `co_transfer` accounts.
        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 OZAV can pay this destination right now.

            Read this, not `status`. A destination is `active` from the moment you create it, and
            `status` never says anything about readiness.

            - A **local BRL/Pix** destination (a Pix key) is paid directly to the key on the
              domestic rail: `true` as soon as it is `active`, in `live` and in the sandbox.
            - Any other destination is `true` once it is registered with the settlement venue.
              While it is `false`, a route that needs that registration is refused with
              `422 PROVISIONING_REQUIRED`; the stablecoin-to-fiat off-ramp does not need it.

            `true` does not validate the Pix key itself: a key that does not exist, or whose
            holder does not match, still fails at payout time.
    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 entry's identifier. Stable across reads; use it to de-duplicate. It is the institution's own identifier where the statement is read live, and an OZAV id (`ozav_amov_...`) on a BRL account. The same id arrives in `account.credited`.
        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 institution that holds the account. 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
        remains stable.

        `currency`, `country` and `rail` describe the account that was actually
        opened, which may differ from the values in your request. OZAV selects
        the banking route, and the receiving coordinates decide the rail: an
        account reachable by IBAN is not reachable by the same instructions as
        one reachable by a domestic account number. Read these three fields from
        the response rather than assuming your request was echoed back, and
        publish `receiving_instructions` to your payers exactly as returned.
      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 institution that holds the account, or `null` when OZAV has never
            read one for this account. Always present. `null` never means zero: it means no
            reading exists yet (a freshly opened account, or one whose institution does not expose a
            balance). When present, `as_of` says when the reading was taken - the number is a
            mirror, not a live query, and a payment received after `as_of` is not reflected.
          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
            waited on from the holder. `url` is omitted when the verification link cannot be
            verified as belonging to the institution that holds the account, in which case the action is still
            reported so the holder knows verification is outstanding.
          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, on a host of the institution that holds the account.
        failure_reason:
          type: string
          description: |
            Present only while `status` is `failed`, and only when a reason was recorded. Absent
            means we did not record one. It never means "a reason exists and we withheld it".

            A comma-separated list of codes. Never free text, and never a message written by the
            underlying institution. Every value is one of:

            - `billing_address`, `registered_address`, `company_documents`, `representatives`,
              `representative_address`, `representative_documents`, `time_in_business`,
              `tax_document`, `legal_name`: the holder still owes us this. Collect it and retry.
            - `counterparty_block`: the block is not the holder's paperwork. Sending more
              documents will not clear it. Contact us.
            - `other_requirement`: a reason was recorded that we could not map to a code above.
              When it appears alongside other codes, the rest of the list is not exhaustive.

            Branch on exact values, and treat an unrecognised value the way you treat
            `other_requirement`. The list grows. Every value is a member of
            `AccountFailureCode`, which carries the full enum.
        metadata:
          $ref: "#/components/schemas/Metadata"
    AccountFailureCode:
      type: string
      description: |
        One value of `Account.failure_reason`. That field carries a comma-separated list of these,
        so a value here is never the whole field.
      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 / fedwire. Older `ach` destinations carry the same shape.
      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
        bank_name:
          type: string
          description: The beneficiary bank's name.
        bank_address:
          $ref: "#/components/schemas/Address"
    PayoutDestinationDetailsSepa:
      type: object
      description: EUR / sepa.
      required:
        - iban
      properties:
        iban:
          type: string
          description: The beneficiary IBAN.
        bic:
          type: string
          description: Optional. The beneficiary bank's SWIFT/BIC code (8 or 11 characters).
        bank_name:
          type: string
          description: The beneficiary bank's name.
        bank_address:
          $ref: "#/components/schemas/Address"
    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.
        bank_name:
          type: string
          description: The beneficiary bank's name.
        bank_address:
          $ref: "#/components/schemas/Address"
    PayoutDestinationDetailsSpei:
      type: object
      description: MXN / spei.
      required:
        - clabe
      properties:
        clabe:
          type: string
          description: The 18-digit CLABE identifying the beneficiary account.
        bank_name:
          type: string
          description: The beneficiary bank's name. Payouts in MXN carry it, so send it.
        bank_address:
          $ref: "#/components/schemas/Address"
    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.
        bank_name:
          type: string
          description: The beneficiary bank's name.
        bank_address:
          $ref: "#/components/schemas/Address"
    PayoutDestinationDetailsCoTransfer:
      type: object
      description: COP / co_transfer. Not offered and not requestable; this is the shape of older destinations that carry it.
      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).
        bank_name:
          type: string
          description: The beneficiary bank's name.
        bank_address:
          $ref: "#/components/schemas/Address"
    PayoutDestinationBeneficiaryBank:
      type: object
      description: The beneficiary's bank - accepted on every rail that has one, and required in several corridors.
      properties:
        bank_name:
          type: string
          description: The beneficiary bank's name.
        bank_address:
          $ref: "#/components/schemas/Address"
    PayoutDestinationDetailsIbanLocal:
      type: object
      description: AED / uaefts - the beneficiary account is an IBAN, and the bank is identified by its BIC (the corridor carries no clearing-system code).
      required:
        - iban
        - bic
      properties:
        iban:
          type: string
          description: The beneficiary IBAN.
        bic:
          type: string
          description: "The beneficiary bank's SWIFT/BIC. Required: no clearing system identifies the bank in this corridor. Checked when you register the destination (`BIC_REQUIRED` / `BIC_INVALID`, ISO 9362), not at payment time - a destination that is missing it would otherwise fail after the quote is locked."
        bank_name:
          type: string
          description: The beneficiary bank's name.
        bank_address:
          $ref: "#/components/schemas/Address"
    PayoutDestinationDetailsLocalBank:
      type: object
      description: |-
        A domestic account in a corridor with no shape of its own - the account number plus whatever identifies the bank there. Serves `cnaps` (CNY), `zengin` (JPY), `nip` (NGN), `cce` (PEN), `rtc` (ZAR) and `eft` (CAD).

        **Which of the optional fields a corridor actually needs is the corridor's rule, not this schema's.** Registering a destination that serves one corridor must not be invalidated because another asks for more, so the requirement is enforced when the payment is created, with a named refusal (except `bic` in **PEN**, checked when you register the destination):

        - `bank_code` - required in **NGN** and **JPY**, from a closed directory
          (669 and 1,150 values). The refusal lists the
          accepted values. Accepted and unconstrained elsewhere.

        - `branch_code` - required in **JPY**; the payment is refused without
          it.

        - `bic` - required in **PEN**, where no clearing system identifies the
          bank. A missing or malformed one is refused when you register the
          destination (`400 BIC_REQUIRED` / `400 BIC_INVALID`).

        - `account_number` alone is enough in **CNY** and **ZAR**.
      required:
        - account_number
      properties:
        account_number:
          type: string
          description: The beneficiary account number, as the corridor writes it.
        bank_code:
          type: string
          description: The identifier of the beneficiary bank in the corridor's own scheme (CNAPS in China, the NIBSS code in Nigeria, the Zengin bank code in Japan).
        branch_code:
          type: string
          description: The beneficiary bank branch. Required in Japan.
        bic:
          type: string
          description: The beneficiary bank's SWIFT/BIC. Required in Peru, and checked when you register the destination.
        bank_name:
          type: string
          description: The beneficiary bank's name.
        bank_address:
          $ref: "#/components/schemas/Address"
    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"
        - $ref: "#/components/schemas/PayoutDestinationDetailsIbanLocal"
        - $ref: "#/components/schemas/PayoutDestinationDetailsLocalBank"
      x-ozav-payout-rail-details:
        pix: PayoutDestinationDetailsPix
        ach: PayoutDestinationDetailsUsBank
        fedwire: PayoutDestinationDetailsUsBank
        sepa: PayoutDestinationDetailsSepa
        swift: PayoutDestinationDetailsSwift
        spei: PayoutDestinationDetailsSpei
        faster_payments: PayoutDestinationDetailsFasterPayments
        co_transfer: PayoutDestinationDetailsCoTransfer
        uaefts: PayoutDestinationDetailsIbanLocal
        eft: PayoutDestinationDetailsLocalBank
        cnaps: PayoutDestinationDetailsLocalBank
        zengin: PayoutDestinationDetailsLocalBank
        nip: PayoutDestinationDetailsLocalBank
        cce: PayoutDestinationDetailsLocalBank
        rtc: PayoutDestinationDetailsLocalBank
    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/RequestRail"
        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:
            - `active` - not deactivated.
            - `inactive` - deactivated; reactivate it before using it again.

            **`active` does not mean the destination is ready to receive a
            transfer.** A newly created destination is `active` immediately, but
            stays unusable until OZAV finishes registering it with the settlement
            rail. Using one before then is refused with
            `422 PROVISIONING_REQUIRED` ("Complete destination registration
            first"), and no field on this resource distinguishes the two states
            yet. Until one exists, treat `422 PROVISIONING_REQUIRED` as retryable
            rather than as a bad request, and do not gate your UI on `status`
            alone.
          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 spend the USDC in the cardholder's OZAV-managed wallet.
        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. It becomes `active` when the card processor
            confirms the card, with no call of yours, and you receive `card.updated`. A card the
            processor returns already usable is born `active`, and in the sandbox every card is.
            `active` - authorizations are attempted.
            `frozen` - reversible refusal, set by `/freeze` and undone by `/unfreeze`.
            `cancelled` - terminal, set by `/close`; there is no reopen, issue a replacement.
            `expired` - past `exp_month`/`exp_year`.

            `active` is not the same as "will approve". A card with no funded balance is `active`
            and declines every purchase with `Insufficient balance` - see `funding_type`.
        funding_type:
          type: string
          enum:
            - prepaid_balance
          description: |
            How authorizations are funded. Only `prepaid_balance` exists today: the card spends a
            balance that must already be there.

            Note, because it decides whether you can pilot this: in the sandbox a customer's balance
            starts at zero, so their first card is `active` and declines every authorization for
            `Insufficient balance` until you load it with `POST /cards/{card_id}/simulate/funding`.
            In production the card spends the USDC in the
            cardholder's OZAV-managed wallet, so the customer needs one: create them with
            `custody_model: managed` (it needs the `custody` product). Issuing a card for a
            customer without the wallet returns `422 CUSTOMER_WALLET_REQUIRED`. There is no separate card load: a deposit reaches the
            card automatically, usually within minutes, and USDT or other assets do not fund the card.
        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` when the issuer does not return it.

            Do not assume this fills in once the card is provisioned. Some issuers
            never return expiry to a non-PCI-DSS-Level-1 partner at all: the
            cardholder sees it only inside the display session, and the value never
            reaches this API.

            When it is `null`, the display session is where the expiry lives - it
            carries the card number, the CVV and the expiry together. So `null` means
            "open a display session", not "the card is not ready", and a card with a
            `null` expiry is fully usable. Never derive an expiry of your own.
        exp_year:
          type:
            - integer
            - "null"
          description: |
            Four-digit expiry year. `null` when the issuer does not return it - the
            same rule as `exp_month`, and the two are always `null` together.
        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 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.
                `card_authorization` - the issuer's hold at purchase time; it can expire or be
                reversed and never settle. `card_transaction` - the settled movement.
                Summing both together double-counts the same purchase.
    CardDeclinePage:
      allOf:
        - $ref: "#/components/schemas/Page"
        - type: object
          required:
            - object
            - card_id
            - data
            - has_more
            - next_cursor
            - item_object
          properties:
            object:
              type: string
              const: list
              description: Resource type discriminator for the page envelope; always `list`.
            card_id:
              type: string
              description: The `ozav_card_` id this page belongs to.
            data:
              type: array
              description: The refused purchases, newest first.
              items:
                $ref: "#/components/schemas/CardDecline"
            item_object:
              type: string
              const: card_decline
              description: Always `card_decline`.
    CardDecline:
      type: object
      description: A purchase OZAV refused. It moved no money and left no hold.
      required:
        - id
        - object
        - card_id
        - amount_usd
        - decline_code
        - fraud_category
        - merchant
        - created_at
      properties:
        id:
          type: string
          description: The id of the `card.declined` event that announced this refusal, prefixed `ozav_evt_`.
        object:
          type: string
          const: card_decline
          description: Resource type discriminator; always `card_decline`.
        card_id:
          type: string
          description: The `ozav_card_` id of the card.
        amount_usd:
          type: string
          description: The amount the purchase asked for, in USD, as a decimal string.
          example: "30.00"
        decline_code:
          type: string
          enum:
            - insufficient_balance
            - card_frozen
            - card_inactive
            - account_inactive
            - spending_limit
            - mcc_blocked
            - country_blocked
            - presence_blocked
            - fraud_rule
            - temporarily_unavailable
          description: Why the purchase was refused. Same codes as the `card.declined` event.
        fraud_category:
          type:
            - string
            - "null"
          enum:
            - velocity
            - transaction_amount
            - period_spend
            - merchant_category
            - merchant_country
            - time_of_day
            - other
            - null
          description: The kind of fraud rule that refused the purchase. Set only when `decline_code` is `fraud_rule`.
        merchant:
          type: object
          required:
            - name
            - category_code
            - country
          properties:
            name:
              type:
                - string
                - "null"
              description: The merchant name as the card network sent it.
            category_code:
              type:
                - string
                - "null"
              description: The merchant category code (MCC).
            country:
              type:
                - string
                - "null"
              description: The merchant country, ISO 3166-1 alpha-2.
        created_at:
          type: string
          format: date-time
          description: When the refusal was recorded (RFC 3339, UTC).
    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.

        Interchange 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.

            **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.

            **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.

            `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."
        client_surcharge:
          $ref: "#/components/schemas/ClientSurcharge"
        created_at:
          type: string
          format: date-time
        settled_at:
          type:
            - string
            - "null"
          format: date-time
          description: Null while an authorisation has not settled.
    ClientSurcharge:
      type: object
      description: "Your surcharge on this purchase, charged to the cardholder together with the purchase. It is not part of `amount`. Absent when you had no active surcharge rules - absent is not zero: a rule that rated 0 appears with `amount: \"0.00\"`. On a refund (`direction: credit`) it is the surcharge returned to the cardholder with the refund, at the purchase's rate - it reduces what you are credited, so do not add it to your earnings."
      required:
        - amount
        - currency
        - rate_bps
        - basis
        - rule_version
        - rule_index
      properties:
        amount:
          type: string
          description: In the card's currency, two decimals.
          example: "9.00"
        currency:
          type: string
          example: USD
        rate_bps:
          type: integer
          minimum: 0
          maximum: 2000
          example: 900
        basis:
          type: string
          enum:
            - rule
            - default
            - presentment_missing
          description: "What decided the rate: a rule (`rule_index`), `default_rate_bps`, or `presentment_missing_rate_bps` because the purchase currency was not reported."
        rule_version:
          type: integer
          minimum: 1
          description: The published version that decided the rate.
        rule_index:
          type:
            - integer
            - "null"
          minimum: 0
          description: Zero-based position of the matching rule; null unless `basis` is `rule`.
    CardSurchargeCondition:
      type: object
      additionalProperties: false
      minProperties: 1
      description: Every condition present must match. `presentment_currency_in` and `presentment_currency_not_in` are mutually exclusive, and neither matches a purchase whose currency was not reported.
      properties:
        presentment_currency_in:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
            pattern: ^[A-Za-z]{3}$
        presentment_currency_not_in:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
            pattern: ^[A-Za-z]{3}$
        merchant_name_contains_any:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
            minLength: 1
            maxLength: 64
            pattern: \S
            description: Trimmed; blank is refused.
        merchant_name_starts_with_any:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
            minLength: 1
            maxLength: 64
            pattern: \S
            description: Trimmed; blank is refused.
        mcc_in:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
            pattern: ^[0-9]{4}$
    CardSurchargeRule:
      type: object
      additionalProperties: false
      required:
        - when
        - rate_bps
      properties:
        description:
          type:
            - string
            - "null"
          maxLength: 120
        when:
          $ref: "#/components/schemas/CardSurchargeCondition"
        rate_bps:
          type: integer
          minimum: 0
          maximum: 2000
          description: Integer basis points; 2000 is 20%.
    CardSurchargeRulesPut:
      type: object
      additionalProperties: false
      required:
        - enabled
        - rules
        - default_rate_bps
        - presentment_missing_rate_bps
      properties:
        enabled:
          type: boolean
          description: false publishes a version that charges nothing.
        rules:
          type: array
          maxItems: 50
          items:
            $ref: "#/components/schemas/CardSurchargeRule"
        default_rate_bps:
          type: integer
          minimum: 0
          maximum: 2000
        presentment_missing_rate_bps:
          type: integer
          minimum: 0
          maximum: 2000
          description: "Required: the rate when the issuer did not report the purchase currency. OZAV never picks one of your rates for you."
    CardSurchargeRules:
      type: object
      required:
        - object
        - environment
        - version
        - enabled
        - rules
        - default_rate_bps
        - presentment_missing_rate_bps
        - created_at
      properties:
        object:
          type: string
          const: card_surcharge_rules
        environment:
          type: string
          enum:
            - test
            - live
        version:
          type:
            - integer
            - "null"
          description: null = never published.
        enabled:
          type: boolean
        rules:
          type: array
          items:
            $ref: "#/components/schemas/CardSurchargeRule"
        default_rate_bps:
          type: integer
        presentment_missing_rate_bps:
          type: integer
        created_at:
          type:
            - string
            - "null"
          format: date-time
    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. Absent for a wallet registered with `owner_ref`.
        owner_ref:
          type: string
          description: Your own reference for the wallet's owner. Present only for a wallet registered with `owner_ref` (no OZAV customer).
        network:
          type: string
          description: The network of a wallet registered with `owner_ref`.
        address:
          type: string
          description: The address of a wallet registered with `owner_ref`, as your user registered it.
        custody_model:
          type: string
          enum:
            - managed
            - self_custody
          description: |
            `managed` wallets are held under OZAV-side custody arrangements;
            `self_custody` wallets are controlled by the client or their
            end-user, and OZAV only builds unsigned transactions.
        status:
          type: string
          description: |
            Whether the wallet is usable. A wallet is `active` only while its
            owning customer account is active; any other account state
            (pending, suspended, closed) projects to `inactive`.
          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.
      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.
        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.
        unreadable_reason:
          type: string
          description: Present only when `amount` is `null`. Why the read failed.
    WalletAddress:
      type: object
      description: |
        One entry of a managed wallet's address book - the destinations that leave without human review (`allowed`), plus the requests OZAV has not decided (`pending_review`) or refused (`rejected`). The book is the allowlist that the custody plane applies, with its labels.
      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
    Approval:
      type: object
      description: |
        An approval of your organization's quorum, as `GET /approvals` lists it. OZAV sets the rules of managed-custody wallets, and no operation waits on approver keys, so no approval is opened.
      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: What the approval was about.
        status:
          type: string
          enum:
            - pending
            - approved
            - rejected
            - expired
            - executed
            - failed
          description: Where the approval ended.
        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"
    WalletPolicy:
      type: object
      description: |
        What OZAV applies before signing a send from a managed wallet, and the limit requests 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).
      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: |
                Your organization's destination setting, shown as configured. The rule OZAV applies is the same in both modes: up to the automatic amount a clean send to any address leaves on its own; above it only addresses in `allowed` (or OZAV's own) receive, and any other is refused (`422 DESTINATION_NOT_ALLOWLISTED`).
            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: |
                Up to and including this amount a clean send to a destination not in `allowed` leaves on its own; above it the send is refused (`422 DESTINATION_NOT_ALLOWLISTED`). `0` means a send to any unlisted destination is refused. When `source` is `wallet` and more than one record is active, the smallest applies, and never above the per-send limit; otherwise the house ceiling (USD 100,000) applies.
            source:
              type: string
              enum:
                - wallet
                - organization
                - unconfigured
              description: |
                `wallet`: a signed per-wallet record is in force (with more than one active, the smallest of each limit applies). `organization`: no per-wallet record; the numbers shown are your organization's defaults, and sends are checked against the house ceiling (USD 100,000) until a record is signed. `unconfigured`: neither exists; the house ceiling applies.
            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
              description: Always `ozav_review`. OZAV's compliance desk decides what falls outside the limits.
        cosigner:
          type: object
          description: "Always `mode: none`. OZAV sets the rules of managed-custody wallets."
          required:
            - mode
          properties:
            mode:
              type: string
              enum:
                - none
        delay:
          type: object
          description: "Always `mode: none`. No send from a managed-custody wallet waits on a delay."
          required:
            - mode
          properties:
            mode:
              type: string
              enum:
                - none
        pending_changes:
          type: array
          description: |
            Requests not yet in force, newest first: awaiting review, approved (limits only - in force once the signed record is issued), or rejected.
          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
        status:
          type: string
          enum:
            - pending_review
            - approved
            - rejected
          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.
        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
    WalletPolicyChangeRequest:
      type: object
      additionalProperties: false
      required:
        - limits
      description: A limit request. `destinations`, `approvals`, `delay` and `cosigner` answer `422 WALLET_RULE_SET_BY_OZAV`.
      properties:
        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.
          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.
    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.
      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.
        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.
        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.

            **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.
        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:
            - `withdraw` - send funds out of the wallet to an external destination.
            - `transfer` - move funds from the wallet to another address.
            - `deposit` - inbound; not buildable here (a deposit is signed by the external payer).
          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`,
        `business`, or `wallet_address`. Inline subjects (`person` / `business`)
        enable standalone screening with no customer record.
      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}$
                email:
                  type: string
                  minLength: 3
                  maxLength: 254
                  description: Optional contact email. Evaluated for contact signals (disposable mailbox; for businesses also non-existent domain, no mail server, domain age) when contact signals are enabled on the OZAV platform - a platform-wide setting, see `contact_evaluated`. Never stored or echoed back - only the resulting signal is kept, and for a person the email domain is quoted only when it is a public disposable-mail domain.
                phone:
                  type: string
                  minLength: 3
                  maxLength: 40
                  description: Optional contact phone (E.164 preferred; without `+`, send `country` so the number can be read - otherwise it is not evaluated). Evaluated for contact signals (invalid number, virtual line, country mismatch) when contact signals are enabled on the OZAV platform - a platform-wide setting. Never stored or echoed back.
        - 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}$
                email:
                  type: string
                  minLength: 3
                  maxLength: 254
                  description: Optional contact email. Evaluated for contact signals (disposable mailbox; for businesses also non-existent domain, no mail server, domain age) when contact signals are enabled on the OZAV platform - a platform-wide setting, see `contact_evaluated`. Never stored or echoed back - only the resulting signal is kept, and for a person the email domain is quoted only when it is a public disposable-mail domain.
                phone:
                  type: string
                  minLength: 3
                  maxLength: 40
                  description: Optional contact phone (E.164 preferred; without `+`, send `country` so the number can be read - otherwise it is not evaluated). Evaluated for contact signals (invalid number, virtual line, country mismatch) when contact signals are enabled on the OZAV platform - a platform-wide setting. Never stored or echoed back.
        - type: object
          required:
            - wallet_address
          description: |
            Wallet-address screening is not available: this variant returns 422 FEATURE_NOT_AVAILABLE.
          properties:
            wallet_address:
              type: object
              description: The blockchain address to screen. Not available - 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:
            - `un` - United Nations Security Council consolidated list.
            - `ofac` - US OFAC (SDN and related lists).
            - `eu` - European Union consolidated sanctions list.
            - `uk` - UK OFSI consolidated list.
            - `national` - a national designation list.
          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:
            - `exact` - the names match exactly.
            - `alias` - the subject matched a known alias of a listed entity.
            - `fuzzy` - a weaker, token-level match (surfaced for review, never auto-cleared).
          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.
        id:
          type: string
          description: The hit id (`hit_...`). It is the hit's fingerprint - list, entry, strength and listed name - so the same subject hitting the same entry again carries the same id. Use it with `PATCH /screenings/{screening_id}/hits/{hit_id}`.
        status:
          type: string
          description: |
            The decision on this hit. Reused by later screenings of the same subject when it is
            identifiable (`customer_id`, or a person with `date_of_birth`):
            - `open` - not decided.
            - `false_positive` - decided as not being the subject; no longer escalates.
            - `confirmed` - decided as the subject. A confirmed sanctions hit makes
              `suggested_decision` `decline`.
            A changed list entry (other strength or listed name) is a new hit and starts `open`.
          enum:
            - open
            - false_positive
            - confirmed
        decided_by:
          type:
            - string
            - "null"
          description: |
            Who decided: `you` (your organization, via the API) or `ozav` (OZAV compliance, shown
            until you decide; your decision prevails). `null` while `open`.
          enum:
            - you
            - ozav
            - null
    ScreeningHitDecisionRequest:
      type: object
      additionalProperties: false
      required:
        - decision
        - reason
      properties:
        decision:
          type: string
          description: |
            - `false_positive` - the hit is not the subject.
            - `confirmed` - the hit is the subject.
          enum:
            - false_positive
            - confirmed
        reason:
          type: string
          minLength: 10
          maxLength: 500
          description: Why - kept for audit (at least 10 characters, no NUL). Do not put personal data here.
    ScreeningHit:
      type: object
      required:
        - id
        - object
        - screening_id
        - category
        - status
      properties:
        id:
          type: string
          description: The hit id (`hit_...`).
        object:
          type: string
          const: screening_hit
          description: Always the literal `screening_hit`.
        screening_id:
          type: string
          description: The screening the decision was made on (`ozav_scr_...`).
        category:
          type: string
          description: |
            - `sanctions_list` - a sanctions-list hit.
            - `pep_list` - a PEP-register hit.
          enum:
            - sanctions_list
            - pep_list
        status:
          type: string
          description: The effective decision now (yours prevails over OZAV's).
          enum:
            - open
            - false_positive
            - confirmed
        decided_by:
          type:
            - string
            - "null"
          description: "`you` or `ozav`."
          enum:
            - you
            - ozav
            - null
        reason:
          type:
            - string
            - "null"
          description: The reason of the effective decision.
        decided_at:
          type:
            - string
            - "null"
          format: date-time
          description: When the effective decision was made.
    ListEntryCreateRequest:
      type: object
      additionalProperties: false
      required:
        - list
        - type
        - value
      properties:
        list:
          type: string
          description: |
            - `block` - subjects matching this entry escalate (`review`, or `decline` with `action`).
              A block match always wins over an allow match.
            - `allow` - suppresses only contact signals for a matching subject - never a block-list,
              sanctions or PEP match. A free or disposable email domain cannot be allow-listed.
          enum:
            - block
            - allow
        type:
          type: string
          description: |
            What the value is (no faces, no wallet addresses), and which screenings it can match:

            | type | inline subject (`person`/`business`) | `customer_id` subject |
            |---|---|---|
            | `name` | the name sent | the stored name |
            | `document` | never (inline screenings carry no document) | the stored tax id |
            | `email` | the `email` sent | the stored email |
            | `email_domain` | the domain of the `email` sent | the domain of the stored email |
          enum:
            - name
            - document
            - email
            - email_domain
        value:
          type: string
          minLength: 1
          maxLength: 320
          description: The value. Stored only as a keyed hash plus a masked hint - it is never returned in full.
        action:
          type: string
          description: |
            Block list only (default `review`):
            - `review` - a match makes `suggested_decision` `review`.
            - `decline` - a match makes `suggested_decision` `decline`.
          enum:
            - review
            - decline
    DecisionCreateRequest:
      description: What to decide. `type` selects the shape of `input`.
      oneOf:
        - type: object
          required:
            - type
            - input
          additionalProperties: false
          properties:
            type:
              type: string
              const: name.match
              description: Is this name on a sanctions list?
            input:
              type: object
              required:
                - name
              additionalProperties: false
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: Full name of the person or business.
                date_of_birth:
                  type: string
                  format: date
                  description: YYYY-MM-DD. Without it a full-name match is `REVIEW`, never `DENY`.
        - type: object
          required:
            - type
            - input
          additionalProperties: false
          properties:
            type:
              type: string
              const: destination.ownership
              description: Is this destination the customer's own?
            input:
              type: object
              required:
                - rail
                - destination_tax_id
                - customer_tax_id
              additionalProperties: false
              properties:
                rail:
                  type: string
                  minLength: 1
                  maxLength: 40
                  description: The payout rail, e.g. `pix`. The same-owner rule applies to Pix only.
                destination_tax_id:
                  type: string
                  minLength: 1
                  maxLength: 40
                  description: CPF/CNPJ of the destination account holder.
                customer_tax_id:
                  type: string
                  minLength: 1
                  maxLength: 40
                  description: CPF/CNPJ of your customer.
        - type: object
          required:
            - type
            - input
          additionalProperties: false
          properties:
            type:
              type: string
              const: wallet.risk
              description: Is this address on a sanctions list?
            input:
              type: object
              required:
                - address
              additionalProperties: false
              properties:
                address:
                  type: string
                  minLength: 10
                  maxLength: 120
                  description: The wallet address.
                chain:
                  type: string
                  maxLength: 40
                  description: Optional network label, recorded as given.
        - type: object
          required:
            - type
            - input
          additionalProperties: false
          properties:
            type:
              type: string
              const: transaction.outbound
              description: "One outgoing payment: the other party, wallet and ownership together."
            input:
              type: object
              required:
                - destination
              additionalProperties: false
              properties:
                amount_usd:
                  type: number
                  exclusiveMinimum: 0
                  description: Optional. Recorded for context; the verdict does not depend on it.
                counterparty:
                  type: object
                  required:
                    - name
                  additionalProperties: false
                  properties:
                    name:
                      type: string
                      minLength: 1
                      maxLength: 200
                      description: Full name of the other party to the payment.
                    date_of_birth:
                      type: string
                      format: date
                      description: YYYY-MM-DD.
                destination:
                  type: object
                  additionalProperties: false
                  properties:
                    wallet_address:
                      type: string
                      minLength: 10
                      maxLength: 120
                      description: Destination wallet address.
                    rail:
                      type: string
                      minLength: 1
                      maxLength: 40
                      description: "Payout rail. `rail`, `destination_tax_id` and `customer_tax_id` go together: with them, ownership is checked."
                    destination_tax_id:
                      type: string
                      minLength: 1
                      maxLength: 40
                      description: CPF/CNPJ of the destination account holder.
                    customer_tax_id:
                      type: string
                      minLength: 1
                      maxLength: 40
                      description: CPF/CNPJ of your customer.
    DecisionOutcomeRequest:
      type: object
      required:
        - outcome
      additionalProperties: false
      properties:
        outcome:
          type: string
          description: |
            - `confirmed_risk` - the risk was real.
            - `false_positive` - the flag was wrong.
            - `completed` - the operation went ahead and completed normally.
            - `reversed` - the operation was reversed or refunded.
            - `chargeback` - the operation ended in a chargeback.
          enum:
            - confirmed_risk
            - false_positive
            - completed
            - reversed
            - chargeback
        note:
          type: string
          minLength: 1
          maxLength: 500
          description: Optional free text.
    AgentPlatformRequest:
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 120
          description: The platform's name, as companies will see it.
        first_party:
          type: boolean
          description: Omit it, or send false, for a platform that runs agents for other companies. Set it to run agents that spend your own organization's money. A first-party platform receives grants only from the organization that registered it, and a platform that is not first-party can never receive a grant from its own organization. The mode is fixed once the platform has a grant; asking for the other mode on a registered platform answers `409 AGENT_PLATFORM_MODE_CONFLICT`.
    AgentPlatform:
      type: object
      required:
        - id
        - object
        - name
        - status
        - first_party
        - created_at
      properties:
        id:
          type: string
          description: The platform id (`ozav_apl_...`). Companies use it to authorize you.
        object:
          type: string
          const: agent_platform
          description: Always the literal `agent_platform`.
        name:
          type: string
          description: The platform's name.
        status:
          type: string
          enum:
            - active
            - suspended
          description: Whether the platform can act.
        first_party:
          type: boolean
          description: True when the platform runs agents for its own organization only.
        created_at:
          type: string
          format: date-time
          description: When it was registered.
    AgentMandate:
      type: object
      additionalProperties: false
      required:
        - actions
        - valid_until
      description: What a company allows agents to do with its money. A missing list means no restriction on that dimension; an empty list allows nothing.
      properties:
        actions:
          type: array
          minItems: 1
          items:
            type: string
            enum:
              - x402_payment
              - mpp_payment
              - fiat_payout
              - fiat_funding
              - payee_registration
              - prepaid_consumption
              - yield_application
              - account_opening
              - account_payout
              - yield_redemption
          description: The kinds of action allowed. `fiat_funding` is a Pix charge that becomes stablecoin in the company's wallet; it is not spending. `payee_registration` lets the agent register a payout destination, and needs `payee_rules`. `prepaid_consumption` lets the agent consume credit the company bought outside OZAV; it moves no money. `yield_application` lets the agent apply the company's custodied balance to a managed yield strategy, and needs `yield_rules`; it is not spending. `account_opening` lets the agent open a named fiat account for the account holder, and needs `account_rules`; it moves no money. `account_payout` lets the agent pay out from an account it opened, under the same spending limits as `fiat_payout`. `yield_redemption` lets the agent take the company's balance back out of a managed yield strategy; it needs no rules, has no limit and is not spending (the money returns to the company).
        per_payment_limit_usd:
          type: number
          description: Largest single payment, in US dollars.
        daily_limit_usd:
          type: number
          description: Largest total per UTC day, in US dollars.
        total_budget_usd:
          type: number
          description: Largest total over the life of the mandate, in US dollars.
        human_approval_above_usd:
          type: number
          description: From this amount up, a person at the company approves.
        recurring_budget:
          type: object
          additionalProperties: false
          required:
            - period
            - amount_usd
            - timezone
          description: A budget that resets on a calendar window, for example US$ 200 per month. Spending inside the current window (every agent of the grant together; for an agent's own mandate, that agent identity alone, so the grant's budget is the aggregate cap) cannot pass `amount_usd`. A request counts against the window it was created in, from the moment it is recorded (including one waiting for approval). It sits on top of `daily_limit_usd` and `total_budget_usd`. A payment or payout that would pass it is refused with the reason `above_recurring_budget`. A payout that is returned releases its amount.
          properties:
            period:
              type: string
              enum:
                - day
                - week
                - month
              description: "The window: a local day, a local week (Monday to Sunday) or a calendar month."
            amount_usd:
              type: number
              description: The most that can be spent inside one window, in US dollars.
            timezone:
              type: string
              maxLength: 64
              pattern: ^(UTC|[A-Za-z]+(/[A-Za-z0-9_+-]+)+)$
              description: A time zone name from the tz database, for example `America/Sao_Paulo`. The window resets at local midnight.
            reset_day:
              type: integer
              minimum: 1
              maximum: 28
              description: "Monthly only: the day of the month the window resets on. Default 1."
        funding_limits:
          type: object
          additionalProperties: false
          description: "Limits for `fiat_funding`, separate from spending: money coming in does not use the budget above."
          properties:
            per_request_usd:
              type: number
              description: Largest single funding, in US dollars of stablecoin delivered.
            daily_usd:
              type: number
              description: Largest total per UTC day, in US dollars. A charge that expires unpaid does not count.
        payee_rules:
          type: object
          additionalProperties: false
          required:
            - holder
          description: What the agent may register as a payout destination. Required when `actions` has `payee_registration`, and only allowed then. A missing list puts no limit on that dimension; an empty one allows nothing.
          properties:
            holder:
              type: string
              enum:
                - self
                - third_party_identified
              description: "Whose account: `self` is the account holder's own (the declared tax id must match the document on file); `third_party_identified` also allows another holder, as long as a tax id is declared."
            currencies:
              type: array
              maxItems: 30
              items:
                type: string
              description: Allowed currencies of the destination, for example `BRL`.
            rails:
              type: array
              maxItems: 30
              items:
                type: string
              description: Allowed rails of the destination, for example `pix`.
            tax_ids:
              type: array
              maxItems: 200
              items:
                type: string
              description: Only these tax ids may be declared for the holder. Compared without punctuation and case.
            max_per_day:
              type: integer
              minimum: 1
              maximum: 1000
              description: Most destinations the agent registers per UTC day. A registration that was refused or failed does not count.
        yield_rules:
          type: object
          additionalProperties: false
          required:
            - allowed_strategies
          description: What the agent may apply to managed yield. Required when `actions` has `yield_application`, and only allowed then. An application stays the company's money (it becomes a yield position in its name), so the spending limits above do not apply to it, only these. The approval threshold (`human_approval_above_usd`) does apply.
          properties:
            allowed_strategies:
              type: array
              minItems: 1
              maxItems: 50
              items:
                type: string
                pattern: ^[A-Za-z0-9][A-Za-z0-9-]{1,62}$
              description: Strategy ids from `GET /yield/strategies`, for example `base-usdc-prime`. Compared without case. Only strategies in a dollar stablecoin can be applied by an agent.
            per_application_usd:
              type: number
              description: Largest single application, in US dollars of stablecoin.
            daily_usd:
              type: number
              description: Largest total per UTC day, in US dollars. An application that was refused or failed before money moved does not count.
        account_rules:
          type: object
          additionalProperties: false
          required:
            - max_accounts
          description: What named fiat accounts the agent may open for the account holder. Required when `actions` has `account_opening`, and only allowed then. A missing list puts no limit on that dimension; an empty one allows nothing. When a list of countries or rails exists, the request must name its country or rail. Corridors are named by currency, country and rail.
          properties:
            currencies:
              type: array
              maxItems: 10
              items:
                type: string
                pattern: ^[A-Z]{3}$
              description: Allowed currencies, for example `BRL`.
            countries:
              type: array
              maxItems: 50
              items:
                type: string
                pattern: ^[A-Z]{2}$
              description: Allowed countries of the account, for example `BR`.
            rails:
              type: array
              maxItems: 20
              items:
                type: string
                pattern: ^[a-z_]{2,20}$
              description: Allowed rails, for example `pix` or `fedwire`.
            max_accounts:
              type: integer
              minimum: 1
              maximum: 20
              description: Most accounts the agents of the grant open over its life (for an agent's own mandate, that agent alone). An opening that was refused or failed does not count.
        allowed_payees:
          type: array
          description: Who can be paid.
          items:
            type: object
            required:
              - type
            properties:
              type:
                type: string
                enum:
                  - address
                  - payout_destination
                  - agent_created
                description: An on-chain address, a registered payout destination, or `agent_created` for any destination this agent registered itself.
              network:
                type: string
                description: "For `address`: the network, as CAIP-2 (for example `eip155:137`)."
              address:
                type: string
                description: "For `address`: the address."
              id:
                type: string
                description: "For `payout_destination`: its id (`ozav_pd_...`)."
        allowed_domains:
          type: array
          items:
            type: string
          description: Hosts of the resources that can be paid (`api.example.com`, or `*.example.com` for subdomains).
        allowed_networks:
          type: array
          items:
            type: string
          description: Networks, as CAIP-2.
        allowed_assets:
          type: array
          items:
            type: string
          description: Assets, for example `USDC`.
        allowed_corridors:
          type: array
          items:
            type: string
          description: "`SOURCE>TARGET`: `USDC>BRL` for a payout, `BRL>USDC` for a funding."
        valid_from:
          type: string
          format: date-time
          description: When the mandate starts. Defaults to now.
        valid_until:
          type: string
          format: date-time
          description: When the mandate ends.
    AgentGrantRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - platform_id
        - mandate
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 255
          pattern: ^[A-Za-z0-9._:-]+$
          description: "Your key for this grant: repeating the call with it returns the same grant."
        platform_id:
          type: string
          description: The platform whose agents receive the mandate (`ozav_apl_...`).
        customer_id:
          type: string
          description: One of your customers (`ozav_cus_...`) whose money the agents move. Omit it to grant your organization's own money.
        mandate:
          $ref: "#/components/schemas/AgentMandate"
    AgentGrantVersionRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - expected_version
        - mandate
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 255
          pattern: ^[A-Za-z0-9._:-]+$
          description: "Your key for this change: repeating the call with it returns the same version."
        expected_version:
          type: integer
          minimum: 1
          description: The mandate version you edited (`mandate_version` of the grant you read). If the current version is another one the call answers `409 STALE_MANDATE_VERSION`.
        mandate:
          $ref: "#/components/schemas/AgentMandate"
    AgentGrant:
      type: object
      required:
        - id
        - object
        - status
        - customer
        - approved_via
        - platform
        - first_party
        - mandate_version
        - mandate
        - approved_at
        - revoked_at
        - created_at
      properties:
        id:
          type: string
          description: The grant id (`ozav_agr_...`).
        object:
          type: string
          const: agent_grant
          description: Always the literal `agent_grant`.
        status:
          type: string
          enum:
            - pending_consent
            - active
            - suspended
            - revoked
            - expired
          description: Whether agents can act under it.
        customer:
          type:
            - string
            - "null"
          description: The customer whose money the grant moves (`ozav_cus_...`). Null when it is the organization's own money, and always null for the platform that received it.
        approved_via:
          type:
            - string
            - "null"
          enum:
            - portal_mfa
            - api_key
            - null
          description: "How the grant was approved: in the portal with the authenticator code, or through `POST /agent-grants` with the organization's admin key."
        first_party:
          type: boolean
          description: "True when the organization that gave the grant runs the platform itself (its own agents, its own money). The approval is the same as for any grant: in the portal, an administrator confirms it with the authenticator code; through `POST /agent-grants`, the call with the organization's admin key is the approval (`approved_via`)."
        platform:
          type:
            - string
            - "null"
          description: The platform it was given to (`ozav_apl_...`).
        mandate_version:
          type:
            - integer
            - "null"
          description: The mandate version in force.
        mandate:
          description: The mandate in force; null once revoked.
          oneOf:
            - $ref: "#/components/schemas/AgentMandate"
            - type: "null"
        approved_at:
          type:
            - string
            - "null"
          format: date-time
          description: When a person at the company approved it.
        revoked_at:
          type:
            - string
            - "null"
          format: date-time
          description: When it was revoked.
        created_at:
          type: string
          format: date-time
          description: When it was created.
    AgentGrantList:
      type: object
      required:
        - object
        - data
      properties:
        object:
          type: string
          const: list
          description: Always the literal `list`.
        data:
          type: array
          items:
            $ref: "#/components/schemas/AgentGrant"
          description: The grants.
    AgentRegisterRequest:
      type: object
      additionalProperties: false
      required:
        - grant
        - label
        - platform_agent_ref
      properties:
        grant:
          type: string
          description: The grant the agent acts under (`ozav_agr_...`).
        label:
          type: string
          minLength: 1
          maxLength: 120
          description: A name for the agent.
        platform_agent_ref:
          type: string
          minLength: 1
          maxLength: 200
          description: The agent's id on your platform. Unique per grant.
        parent_agent:
          type: string
          description: The parent agent (`ozav_agt_...`), for a sub-agent.
        own_mandate:
          $ref: "#/components/schemas/AgentOwnMandate"
    AgentOwnMandate:
      type: object
      additionalProperties: false
      description: "A child agent's own mandate. Every field is optional: a field you leave out inherits the value of the mandate it sits under (the parent agent's own mandate, or the grant's), and the agent is stored with the explicit result. Objects are merged field by field, so `funding_limits: { daily_usd: 100 }` keeps the parent's `per_request_usd`; inside such an object, members that are required in `AgentMandate` (for example `payee_rules.holder`) are optional here and inherit the same way (`AgentOwn*` schemas). `{}` is valid and stores an exact copy of the parent's mandate. Inherited fields follow the parent: when the grant publishes a new mandate version they are derived again from it, so narrowing the grant narrows them and widening the grant widens them (inherited means the same as the company). Only the fields you set explicitly are the agent's own; if one of those is wider than the new parent, the publish is refused and names the agent and the field. A value you send replaces the parent's and must still be equal or narrower (an empty list allows nothing). `null` is not accepted anywhere: leave the field out to inherit it. A rules object (`payee_rules`, `yield_rules`, `account_rules`) is inherited only while `actions` still includes the action it governs."
      properties:
        actions:
          $ref: "#/components/schemas/AgentMandate/properties/actions"
        per_payment_limit_usd:
          $ref: "#/components/schemas/AgentMandate/properties/per_payment_limit_usd"
        daily_limit_usd:
          $ref: "#/components/schemas/AgentMandate/properties/daily_limit_usd"
        total_budget_usd:
          $ref: "#/components/schemas/AgentMandate/properties/total_budget_usd"
        human_approval_above_usd:
          $ref: "#/components/schemas/AgentMandate/properties/human_approval_above_usd"
        recurring_budget:
          $ref: "#/components/schemas/AgentOwnRecurringBudget"
        funding_limits:
          $ref: "#/components/schemas/AgentMandate/properties/funding_limits"
        payee_rules:
          $ref: "#/components/schemas/AgentOwnPayeeRules"
        yield_rules:
          $ref: "#/components/schemas/AgentOwnYieldRules"
        account_rules:
          $ref: "#/components/schemas/AgentOwnAccountRules"
        allowed_payees:
          $ref: "#/components/schemas/AgentMandate/properties/allowed_payees"
        allowed_domains:
          $ref: "#/components/schemas/AgentMandate/properties/allowed_domains"
        allowed_networks:
          $ref: "#/components/schemas/AgentMandate/properties/allowed_networks"
        allowed_assets:
          $ref: "#/components/schemas/AgentMandate/properties/allowed_assets"
        allowed_corridors:
          $ref: "#/components/schemas/AgentMandate/properties/allowed_corridors"
        valid_from:
          $ref: "#/components/schemas/AgentMandate/properties/valid_from"
        valid_until:
          $ref: "#/components/schemas/AgentMandate/properties/valid_until"
    AgentOwnPayeeRules:
      type: object
      additionalProperties: false
      description: What the agent may register as a payout destination. Required when `actions` has `payee_registration`, and only allowed then. A missing list puts no limit on that dimension; an empty one allows nothing.
      properties:
        holder:
          type: string
          enum:
            - self
            - third_party_identified
          description: "Whose account: `self` is the account holder's own (the declared tax id must match the document on file); `third_party_identified` also allows another holder, as long as a tax id is declared."
        currencies:
          type: array
          maxItems: 30
          items:
            type: string
          description: Allowed currencies of the destination, for example `BRL`.
        rails:
          type: array
          maxItems: 30
          items:
            type: string
          description: Allowed rails of the destination, for example `pix`.
        tax_ids:
          type: array
          maxItems: 200
          items:
            type: string
          description: Only these tax ids may be declared for the holder. Compared without punctuation and case.
        max_per_day:
          type: integer
          minimum: 1
          maximum: 1000
          description: Most destinations the agent registers per UTC day. A registration that was refused or failed does not count.
    AgentOwnYieldRules:
      type: object
      additionalProperties: false
      description: What the agent may apply to managed yield. Required when `actions` has `yield_application`, and only allowed then. An application stays the company's money (it becomes a yield position in its name), so the spending limits above do not apply to it, only these. The approval threshold (`human_approval_above_usd`) does apply.
      properties:
        allowed_strategies:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
            pattern: ^[A-Za-z0-9][A-Za-z0-9-]{1,62}$
          description: Strategy ids from `GET /yield/strategies`, for example `base-usdc-prime`. Compared without case. Only strategies in a dollar stablecoin can be applied by an agent.
        per_application_usd:
          type: number
          description: Largest single application, in US dollars of stablecoin.
        daily_usd:
          type: number
          description: Largest total per UTC day, in US dollars. An application that was refused or failed before money moved does not count.
    AgentOwnAccountRules:
      type: object
      additionalProperties: false
      description: What named fiat accounts the agent may open for the account holder. Required when `actions` has `account_opening`, and only allowed then. A missing list puts no limit on that dimension; an empty one allows nothing. When a list of countries or rails exists, the request must name its country or rail. Corridors are named by currency, country and rail.
      properties:
        currencies:
          type: array
          maxItems: 10
          items:
            type: string
            pattern: ^[A-Z]{3}$
          description: Allowed currencies, for example `BRL`.
        countries:
          type: array
          maxItems: 50
          items:
            type: string
            pattern: ^[A-Z]{2}$
          description: Allowed countries of the account, for example `BR`.
        rails:
          type: array
          maxItems: 20
          items:
            type: string
            pattern: ^[a-z_]{2,20}$
          description: Allowed rails, for example `pix` or `fedwire`.
        max_accounts:
          type: integer
          minimum: 1
          maximum: 20
          description: Most accounts the agents of the grant open over its life (for an agent's own mandate, that agent alone). An opening that was refused or failed does not count.
    AgentOwnRecurringBudget:
      type: object
      additionalProperties: false
      description: A budget that resets on a calendar window, for example US$ 200 per month. Spending inside the current window (every agent of the grant together; for an agent's own mandate, that agent identity alone, so the grant's budget is the aggregate cap) cannot pass `amount_usd`. A request counts against the window it was created in, from the moment it is recorded (including one waiting for approval). It sits on top of `daily_limit_usd` and `total_budget_usd`. A payment or payout that would pass it is refused with the reason `above_recurring_budget`. A payout that is returned releases its amount.
      properties:
        period:
          type: string
          enum:
            - day
            - week
            - month
          description: "The window: a local day, a local week (Monday to Sunday) or a calendar month."
        amount_usd:
          type: number
          description: The most that can be spent inside one window, in US dollars.
        timezone:
          type: string
          maxLength: 64
          pattern: ^(UTC|[A-Za-z]+(/[A-Za-z0-9_+-]+)+)$
          description: A time zone name from the tz database, for example `America/Sao_Paulo`. The window resets at local midnight.
        reset_day:
          type: integer
          minimum: 1
          maximum: 28
          description: "Monthly only: the day of the month the window resets on. Default 1."
    AgentFundingRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - amount
        - rail
        - target
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
          description: Your key for this funding. The same key returns the same recorded charge and never creates a second one.
        amount:
          type: object
          additionalProperties: false
          required:
            - amount
            - currency
          description: What the company pays, in reais or US dollars. At most two decimal places and greater than zero.
          properties:
            amount:
              $ref: "#/components/schemas/DecimalAmount"
            currency:
              type: string
              enum:
                - BRL
                - USD
        rail:
          type: string
          enum:
            - pix
            - wire
            - fedwire
            - swift
          description: How the company pays. Reais only by `pix`; US dollars by `wire`, `fedwire` or `swift`. Any other pairing is `400`. A dollar funding by wire is credited only when the bank statement shows the account holder as the sender (otherwise it is held for review). In the sandbox the bank instructions are simulated.
        target:
          type: object
          additionalProperties: false
          required:
            - wallet_id
            - currency
            - chain
          description: The company's managed wallet the stablecoin lands in.
          properties:
            wallet_id:
              type: string
              description: "`ozav_wal_...`"
            currency:
              type: string
              enum:
                - USDC
                - USDT
            chain:
              type: string
              description: The network the stablecoin arrives on, for example `polygon`.
    AgentAllocationMoveRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - amount
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
        amount:
          type: object
          additionalProperties: false
          required:
            - amount
            - currency
          properties:
            amount:
              $ref: "#/components/schemas/DecimalAmount"
            currency:
              type: string
              enum:
                - USDC
                - USDT
    AgentWalletRequest:
      type: object
      additionalProperties: false
      required:
        - custody
        - network
      properties:
        custody:
          type: string
          enum:
            - bring_your_own
            - ozav_enclave
          description: "`bring_your_own`: a Safe the company deployed (needs `safe_address`). `ozav_enclave`: a wallet OZAV holds for the agent (takes no `safe_address`)."
        safe_address:
          type: string
          description: The Safe's address (`0x...`). Required with `bring_your_own`, refused with `ozav_enclave`.
        network:
          type: string
          enum:
            - base
            - polygon
          description: The network of the Safe, or the network to record the address for when OZAV holds the wallet (one wallet serves both).
    AgentWalletWithdrawRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
        amount:
          type: object
          description: How much to withdraw. Omit it to withdraw the whole balance.
          additionalProperties: false
          required:
            - amount
            - currency
          properties:
            amount:
              $ref: "#/components/schemas/DecimalAmount"
            currency:
              type: string
              enum:
                - USDC
    AgentWalletWithdrawal:
      type: object
      required:
        - id
        - object
        - agent
        - status
        - trigger
        - chain
        - amount
        - settled_amount
        - refusal_code
        - parts
        - created_at
      properties:
        id:
          type: string
          description: The withdrawal id (`ozav_awd_...`).
        object:
          type: string
          enum:
            - agent_wallet_withdrawal
        agent:
          type: string
          description: The agent id (`ozav_agt_...`).
        status:
          type: string
          enum:
            - processing
            - settled
            - refused
            - in_review
            - empty
          description: "`processing`: parts are still to run (call again with the same key). `settled`: every part settled. A success answer carries only these two. `refused` and `in_review` come inside `details.withdrawal` of the `422 AGENT_WALLET_WITHDRAW_REFUSED` / `AGENT_WALLET_WITHDRAW_ABOVE_WALLET_LIMIT` and `503 AGENT_WALLET_WITHDRAW_OUTCOME_UNKNOWN` errors. `empty` is only a revocation that found the wallet empty. The webhook `agent_wallet_withdrawal.updated` carries this same object in `data`, so any of the five can arrive there."
        trigger:
          type: string
          enum:
            - api
            - revocation
        chain:
          type: string
          enum:
            - polygon
        amount:
          $ref: "#/components/schemas/AgentWalletWithdrawalAmount"
        settled_amount:
          $ref: "#/components/schemas/AgentWalletWithdrawalAmount"
        refusal_code:
          type:
            - string
            - "null"
          enum:
            - ABOVE_WALLET_LIMIT
            - REFUSED
            - null
          description: Only when `status` is `refused`.
        parts:
          type: array
          description: The parts, in the order they run, each at most the wallet's sealed per-payment limit.
          items:
            type: object
            required:
              - amount
              - status
              - tx_hash
            properties:
              amount:
                $ref: "#/components/schemas/AgentWalletWithdrawalAmount"
              status:
                type: string
                enum:
                  - pending
                  - processing
                  - settled
                  - refused
                  - in_review
              tx_hash:
                type:
                  - string
                  - "null"
                description: The Polygon transaction, once known.
        created_at:
          type: string
          format: date-time
    AgentWalletWithdrawalAmount:
      type: object
      required:
        - amount
        - currency
      properties:
        amount:
          type: string
          pattern: ^(0|[1-9][0-9]{0,14})(\.[0-9]{1,6})?$
          description: Decimal USDC, trailing zeros removed. `0` when nothing has settled yet.
        currency:
          type: string
          enum:
            - USDC
    AgentWallet:
      type: object
      required:
        - object
        - custody
        - smart_accounts
        - enclave_wallets
      properties:
        object:
          type: string
          enum:
            - agent_wallet
        custody:
          type: string
          enum:
            - ozav
            - bring_your_own
            - ozav_enclave
        network:
          type: string
          enum:
            - base
            - polygon
          description: "Only in the answer to `POST` with `custody: ozav_enclave`: the network asked for."
        status:
          type: string
          enum:
            - pending
            - active
          description: "Only in the answer to `POST` with `custody: ozav_enclave`: `pending` until the wallet exists."
        address:
          type: string
          description: "Only when `status` is `active`: the wallet's address (`0x...`), the same on Base and Polygon."
        per_payment_cap:
          type: object
          description: "Only in the answer to `POST` with `custody: ozav_enclave`: the largest USDC payment the wallet will sign, sealed when it was created."
          required:
            - amount_atomic
            - asset
            - decimals
          properties:
            amount_atomic:
              type: string
              description: Whole units of 10^-decimals, as text.
            asset:
              type: string
              enum:
                - USDC
            decimals:
              type: integer
              enum:
                - 6
        enclave_wallets:
          type: array
          description: The addresses OZAV holds for this agent, one per network recorded.
          items:
            type: object
            required:
              - network
              - address
            properties:
              network:
                type: string
                enum:
                  - base
                  - polygon
              address:
                type: string
        smart_accounts:
          type: array
          items:
            type: object
            required:
              - network
              - safe_address
              - safe_version
              - company_owner
              - ozav_signer
              - threshold
              - verified_at
            properties:
              network:
                type: string
                enum:
                  - base
                  - polygon
              safe_address:
                type: string
              safe_version:
                type: string
              company_owner:
                type: string
                description: "The owner that is not OZAV: the company's key, or its agent's."
              ozav_signer:
                type: string
                description: OZAV's policy key, the other owner.
              threshold:
                type: integer
                enum:
                  - 2
              verified_at:
                type: string
                format: date-time
    AgentAllocation:
      type: object
      required:
        - object
        - agent_id
        - balances
      properties:
        object:
          type: string
          enum:
            - agent_allocation
        agent_id:
          type: string
          description: The agent id (`ozav_agt_...`).
        balances:
          type: array
          items:
            type: object
            required:
              - currency
              - allocated
              - spent
              - available
            properties:
              currency:
                type: string
                enum:
                  - USDC
                  - USDT
              allocated:
                $ref: "#/components/schemas/DecimalAmount"
              spent:
                $ref: "#/components/schemas/DecimalAmount"
              available:
                $ref: "#/components/schemas/DecimalAmount"
    AgentPrepaidResourceRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - seller_domain
        - unit
        - units
      description: Credit bought outside OZAV, registered for a grant's agents.
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
          description: Your key for this registration.
        agent_id:
          type: string
          description: Only this agent of the grant may consume it (`ozav_agt_...`). Leave it out for every agent.
        seller_domain:
          type: string
          maxLength: 253
          description: The seller's host (`api.openai.com`). `allowed_domains` of the mandate applies to it.
        unit:
          type: string
          pattern: ^[a-z0-9_]{1,32}$
          description: What a unit is (`tokens`, `requests`, `credits`).
        units:
          type: string
          pattern: ^[1-9][0-9]{0,15}$
          description: How many units were bought, a whole number as a string.
        expires_at:
          type: string
          format: date-time
          description: When the seller stops honoring it. Leave it out if it does not expire.
        description:
          type: string
          maxLength: 200
          description: A note for the company.
    AgentPrepaidResource:
      type: object
      required:
        - id
        - object
        - grant
        - agent
        - seller_domain
        - unit
        - initial_units
        - balance_units
        - expires_at
        - description
        - status
        - created_at
      properties:
        id:
          type: string
          description: The resource id (`ozav_ppr_...`).
        object:
          type: string
          const: agent_prepaid_resource
          description: Always the literal `agent_prepaid_resource`.
        grant:
          type: string
          description: The grant.
        agent:
          type:
            - string
            - "null"
          description: The only agent that may consume it, or null for every agent of the grant.
        seller_domain:
          type: string
          description: The seller's host.
        unit:
          type: string
          description: What a unit is.
        initial_units:
          type: string
          description: The units registered.
        balance_units:
          type: string
          description: The units left. Only goes down.
        expires_at:
          type:
            - string
            - "null"
          format: date-time
          description: When it stops being consumable.
        description:
          type:
            - string
            - "null"
          description: The company's note.
        status:
          type: string
          enum:
            - active
            - closed
          description: A closed resource is not consumed.
        created_at:
          type: string
          format: date-time
          description: When it was registered.
    AgentConsumptionRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - resource_id
        - units
      description: What the agent consumed from a prepaid resource.
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
          description: Your key for this consumption. The same key never debits twice.
        resource_id:
          type: string
          description: The prepaid resource (`ozav_ppr_...`).
        units:
          type: string
          pattern: ^[1-9][0-9]{0,15}$
          description: The units consumed, a whole number as a string.
    AgentPayeeRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - currency
        - rail
        - account_holder
        - details
      description: The body of `POST /payout-destinations` without `customer_id` (the destination always belongs to the account holder of the grant), plus your key.
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
          description: Your key for this registration. The same key returns the same destination and never creates a second one.
        currency:
          type: string
          description: The currency of the destination, for example `BRL`.
        rail:
          type: string
          description: The rail, for example `pix`.
        account_holder:
          type: object
          description: Who owns the account, as in `POST /payout-destinations`. `name` and `type` are required.
          required:
            - name
            - type
          properties:
            name:
              type: string
            type:
              type: string
              enum:
                - individual
                - business
            tax_id:
              type: object
              description: The holder's document, as in `POST /payout-destinations`.
              properties:
                country:
                  type: string
                type:
                  type: string
                value:
                  type: string
        details:
          type: object
          description: The account details of the rail, as in `POST /payout-destinations`.
        metadata:
          type: object
          description: Free-form key-value pairs, as in `POST /payout-destinations`.
    AgentYieldApplicationRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - strategy_id
        - amount
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
          description: Your key for this application. The same key returns the same recorded application and never applies twice.
        strategy_id:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9-]{1,62}$
          description: The strategy, from `GET /yield/strategies` (for example `base-usdc-prime`). It must be in the mandate's `yield_rules.allowed_strategies`.
        amount:
          type: object
          additionalProperties: false
          required:
            - amount
            - currency
          description: What is applied from the company's custodied balance.
          properties:
            amount:
              $ref: "#/components/schemas/DecimalAmount"
            currency:
              type: string
              pattern: ^[A-Z0-9]{2,10}$
              description: The strategy's asset, for example `USDC`.
    AgentYieldApplication:
      description: The `AgentAction` of a yield application, with the managed yield order as it is now.
      allOf:
        - $ref: "#/components/schemas/AgentAction"
        - type: object
          required:
            - order
          properties:
            order:
              description: The managed yield order. `null` before it exists.
              oneOf:
                - type: object
                  required:
                    - id
                    - status
                    - detail
                    - tx_hash
                  properties:
                    id:
                      type: string
                      description: The order id (the same as `yield_application.application_id`).
                    status:
                      type: string
                      enum:
                        - pending
                        - running
                        - applied
                        - refused
                        - failed
                        - indeterminate
                      description: "`applied`: confirmed on chain. `refused` / `failed`: nothing moved. `indeterminate`: stopped without knowing whether money moved; OZAV checks the chain before it closes."
                    detail:
                      type:
                        - string
                        - "null"
                      description: Why it was refused or failed, when it was. Only the company reads it; `null` for the platform.
                    tx_hash:
                      type:
                        - string
                        - "null"
                      description: The on-chain transaction, once known. Only the company reads it; `null` for the platform.
                - type: "null"
    AgentYieldRedemptionRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - strategy_id
      oneOf:
        - required:
            - amount
        - required:
            - all
      description: "Send exactly one of `amount` and `all: true`."
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
          description: Your key for this redemption. The same key returns the same recorded redemption and never redeems twice.
        strategy_id:
          type: string
          pattern: ^[A-Za-z0-9][A-Za-z0-9-]{1,62}$
          description: The strategy, from `GET /yield/strategies` (for example `base-usdc-prime`).
        amount:
          type: object
          additionalProperties: false
          required:
            - amount
            - currency
          description: What is taken back to the company's custody address.
          properties:
            amount:
              $ref: "#/components/schemas/DecimalAmount"
            currency:
              type: string
              pattern: ^[A-Z0-9]{2,10}$
              description: The strategy's asset, for example `USDC`.
        all:
          type: boolean
          enum:
            - true
          description: Redeem the whole position in the strategy. Not together with `amount`.
    AgentYieldRedemption:
      description: The `AgentAction` of a yield redemption, with the managed yield order as it is now.
      allOf:
        - $ref: "#/components/schemas/AgentAction"
        - type: object
          required:
            - order
          properties:
            order:
              description: The managed yield order. `null` before it exists.
              oneOf:
                - type: object
                  required:
                    - id
                    - status
                    - detail
                    - tx_hash
                  properties:
                    id:
                      type: string
                      description: The order id (the same as `yield_redemption.redemption_id`).
                    status:
                      type: string
                      enum:
                        - pending
                        - running
                        - redeemed
                        - refused
                        - failed
                        - indeterminate
                      description: "`redeemed`: confirmed on chain, the money is back with the company. `refused` / `failed`: nothing moved. `indeterminate`: stopped without knowing whether money moved; OZAV checks the chain before it closes."
                    detail:
                      type:
                        - string
                        - "null"
                      description: Why it was refused or failed, when it was. Only the company reads it; `null` for the platform.
                    tx_hash:
                      type:
                        - string
                        - "null"
                      description: The on-chain transaction, once known. Only the company reads it; `null` for the platform.
                - type: "null"
    AgentAccountOpeningRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - currency
      description: The body of `POST /accounts` without `customer_id` (the account always belongs to the account holder of the grant), plus your key.
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
          description: Your key for this opening. The same key returns the same account and never opens a second one.
        account_type:
          type: string
          enum:
            - named_fiat_account
          description: As in `POST /accounts`. Optional.
        currency:
          type: string
          description: The account's currency, for example `BRL`, as in `POST /accounts`.
        country:
          type: string
          description: The account's country, as in `POST /accounts`. Required when the mandate's `account_rules` list countries.
        rail:
          type: string
          description: The account's rail, as in `POST /accounts`. Required when the mandate's `account_rules` list rails.
        metadata:
          type: object
          additionalProperties:
            type: string
            maxLength: 500
          description: Free-form key-value pairs, as in `POST /accounts`. OZAV adds `ozav_agent` and `ozav_agent_action`.
    AgentAccountActionWithAccount:
      description: The account opening, with the account it opened.
      allOf:
        - $ref: "#/components/schemas/AgentAction"
        - type: object
          required:
            - named_account
          properties:
            named_account:
              description: The account as `GET /accounts/{account_id}` shows it; `null` when it could not be read right now.
              oneOf:
                - $ref: "#/components/schemas/Account"
                - type: "null"
    AgentAccountList:
      type: object
      required:
        - object
        - data
        - has_more
      properties:
        object:
          type: string
          const: list
        has_more:
          type: boolean
        data:
          type: array
          items:
            type: object
            required:
              - object
              - action_id
              - status
              - account_id
              - currency
              - country
              - rail
              - created_at
              - named_account
            properties:
              object:
                type: string
                const: agent_account
              action_id:
                type: string
                description: The opening action (`ozav_aga_...`).
              status:
                type: string
                description: The opening action's status, as on `AgentAction`.
              account_id:
                type:
                  - string
                  - "null"
                description: The account it opened (`ozav_acc_...`); `null` when it did not.
              currency:
                type:
                  - string
                  - "null"
              country:
                type:
                  - string
                  - "null"
              rail:
                type:
                  - string
                  - "null"
              created_at:
                type: string
                format: date-time
              named_account:
                description: The account as it is now; `null` when it was never opened or could not be read right now.
                oneOf:
                  - $ref: "#/components/schemas/Account"
                  - type: "null"
    AgentAccountPayoutRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - amount
        - payout_destination_id
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
          description: Your key for this payout. The same key returns the same order and never pays twice.
        amount:
          type: string
          pattern: ^(0|[1-9]\d{0,14})\.\d{2}$
          description: The amount in the account's own currency, with exactly two decimals, greater than zero.
        payout_destination_id:
          type: string
          description: A payout destination (`ozav_pd_...`) of the same account holder, in the account's currency.
        purpose:
          type: string
          minLength: 1
          maxLength: 140
          description: As in `POST /accounts/{account_id}/payouts`.
    AgentAccountPayoutAction:
      description: The payout from an account, with the order OZAV placed.
      allOf:
        - $ref: "#/components/schemas/AgentAction"
        - type: object
          properties:
            account_payout:
              type:
                - object
                - "null"
              description: The order OZAV placed (`payout_id`, `payout_status`, `amount`, `currency`), or why it was refused. `null` while it waits for approval.
    AgentPayoutRequest:
      type: object
      additionalProperties: false
      required:
        - idempotency_key
        - payout_destination_id
        - amount
        - source
      properties:
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
          description: Your key for this payout. The same key returns the same recorded payout and never pays twice.
        payout_destination_id:
          type: string
          description: The company's registered payout destination (`ozav_pd_...`).
        amount:
          type: object
          additionalProperties: false
          required:
            - amount
            - currency
          description: What the beneficiary receives.
          properties:
            amount:
              $ref: "#/components/schemas/DecimalAmount"
            currency:
              type: string
              pattern: ^[A-Z]{3}$
              description: ISO 4217 code, e.g. `BRL`.
        source:
          description: Where the stablecoin leaves from, either the company's managed wallet or the agent's own wallet.
          oneOf:
            - type: object
              additionalProperties: false
              required:
                - wallet_id
                - currency
                - chain
              description: The company's managed wallet the stablecoin leaves from.
              properties:
                wallet_id:
                  type: string
                  description: "`ozav_wal_...`"
                currency:
                  type: string
                  enum:
                    - USDC
                    - USDT
                chain:
                  type: string
                  description: The network the funds leave from, e.g. `polygon`.
            - type: object
              additionalProperties: false
              required:
                - type
                - currency
                - chain
              description: The agent's own wallet in OZAV's infrastructure (production, Polygon).
              properties:
                type:
                  type: string
                  enum:
                    - agent_wallet
                currency:
                  type: string
                  enum:
                    - USDC
                chain:
                  type: string
                  enum:
                    - polygon
    AgentActionException:
      type: object
      description: An exception case (dispute) on an agent action, with the evidence OZAV recorded when it was opened and, once decided, the remedy.
      required:
        - id
        - object
        - action
        - reason_code
        - opened_by
        - status
        - due_at
        - created_at
      properties:
        id:
          type: string
          description: "`ozav_agx_...`"
        object:
          type: string
          enum:
            - agent_action_exception
        action:
          type: string
          description: The agent action (`ozav_aga_...`).
        reason_code:
          type: string
          enum:
            - not_delivered
            - wrong_amount
            - duplicate_payment
            - not_authorized
            - other
        opened_by:
          type: string
          enum:
            - platform
            - account_holder
        status:
          type: string
          enum:
            - open
            - under_review
            - resolved_refunded
            - resolved_denied
            - closed_legacy
          description: Forward only. A resolved case never reopens. `closed_legacy` is an older case (opened before cases had their own status) whose ticket was already closed; it has no recorded outcome or evidence.
        due_at:
          type: string
          format: date-time
        evidence:
          type:
            - object
            - "null"
          additionalProperties: true
          description: What OZAV recorded from the action when the case was opened (action, type, mandate version and its SHA-256, decision, reasons, status, amounts, payee, receipt and its SHA-256, timestamps). Never changes.
        evidence_sha256:
          type:
            - string
            - "null"
          description: SHA-256 (hex) of `evidence`.
        remedy:
          type:
            - object
            - "null"
          description: Set when the case is resolved.
          properties:
            channel:
              type: string
              enum:
                - pix_med
                - manual_transfer
                - onchain_payee_block
                - card_chargeback
                - none
            reference:
              type:
                - string
                - "null"
              description: OZAV's protocol number for the remedy.
        decided_at:
          type:
            - string
            - "null"
          format: date-time
        support_ticket:
          type:
            - string
            - "null"
          description: OZAV's support ticket number.
        created_at:
          type: string
          format: date-time
    AgentRequestSignatureRequest:
      type: object
      additionalProperties: false
      required:
        - authority
      properties:
        authority:
          type: string
          maxLength: 260
          description: The host (and port, when not 443) the agent will call, as in the request's `Host` - `api.example.com`. No scheme, path or credentials.
        method:
          type: string
          maxLength: 16
          description: Optional. The HTTP method; when sent, the signature covers it.
        path:
          type: string
          maxLength: 2048
          description: Optional. The request path, starting with `/`, without query string or fragment; when sent, the signature covers it.
        signature_agent_format:
          type: string
          enum:
            - string
            - dictionary
          description: Optional. How `Signature-Agent` is written. `string` (the default) is `"https://ozav.io"`, the form Cloudflare verifies today; `dictionary` is `sig1="https://ozav.io"`, the form of the IETF Web Bot Auth protocol.
    AgentRequestSignature:
      type: object
      description: The headers OZAV signed for one request of the agent.
      required:
        - object
        - agent
        - headers
        - created_at
        - expires_at
        - key_id
        - directory_url
        - simulated
      properties:
        object:
          type: string
          enum:
            - agent_request_signature
        agent:
          type: string
          description: The agent id (`ozav_agt_...`).
        headers:
          type: object
          description: Add these headers to the request, unchanged.
          required:
            - Signature-Agent
            - Signature-Input
            - Signature
            - OZAV-Agent
          properties:
            Signature-Agent:
              type: string
              description: 'The directory origin: `"https://ozav.io"`, or `sig1="https://ozav.io"` with `signature_agent_format: dictionary`.'
            Signature-Input:
              type: string
            Signature:
              type: string
            OZAV-Agent:
              type: string
              description: The agent id
              covered by the signature.: null
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          description: 60 seconds after `created_at`.
        key_id:
          type: string
          description: The SHA-256 thumbprint (RFC 7638) of OZAV's signing key.
        directory_url:
          type: string
          description: Where sites fetch OZAV's public keys.
        simulated:
          type: boolean
          description: "`true` in the sandbox: no directory lists the key."
    AgentPassport:
      type: object
      description: A passport OZAV signed for an agent. The `credential` is what the agent presents.
      required:
        - id
        - object
        - agent
        - format
        - credential
        - issued_at
        - expires_at
        - jwks_url
        - status_url
      properties:
        id:
          type: string
          description: The passport id, also the token's `jti` (`ozav_psp_...`).
        object:
          type: string
          enum:
            - agent_passport
        agent:
          type: string
          description: The agent id (`ozav_agt_...`).
        format:
          type: string
          enum:
            - vc+sd-jwt
        credential:
          type: string
          description: "The SD-JWT in compact form: `<jwt>~<disclosure>~...~`."
        issued_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        jwks_url:
          type: string
          format: uri
          description: Where OZAV's public signing keys are published.
        status_url:
          type: string
          format: uri
          description: Where anyone checks this passport's status, without an API key.
        sandbox:
          type: boolean
          description: "Present and `true` on a sandbox (test) passport: signed with the sandbox key, never valid in production."
    Agent:
      type: object
      required:
        - id
        - object
        - label
        - platform_agent_ref
        - status
        - grant
        - grant_status
        - platform
        - parent_agent
        - mandate_version
        - mandate
        - own_mandate
        - can_transact
        - revoked_at
        - created_at
      properties:
        id:
          type: string
          description: The agent id (`ozav_agt_...`).
        object:
          type: string
          const: agent
          description: Always the literal `agent`.
        label:
          type: string
          description: The agent's name.
        platform_agent_ref:
          type: string
          description: The agent's id on the platform.
        status:
          type: string
          enum:
            - active
            - suspended
            - revoked
          description: The agent's own status.
        grant:
          type: string
          description: The grant it acts under.
        grant_status:
          type: string
          description: The grant's status.
        platform:
          type:
            - string
            - "null"
          description: The platform.
        parent_agent:
          type:
            - string
            - "null"
          description: The parent agent, for a sub-agent.
        mandate_version:
          type:
            - integer
            - "null"
          description: The grant's mandate version in force.
        mandate:
          description: The grant's mandate in force.
          oneOf:
            - $ref: "#/components/schemas/AgentMandate"
            - type: "null"
        own_mandate:
          description: The agent's own narrower mandate, if any.
          oneOf:
            - $ref: "#/components/schemas/AgentMandate"
            - type: "null"
        can_transact:
          type: boolean
          description: True when the agent and its grant are both active.
        revoked_at:
          type:
            - string
            - "null"
          format: date-time
          description: When the agent was revoked.
        created_at:
          type: string
          format: date-time
          description: When it was registered.
    AgentPaymentRequest:
      type: object
      additionalProperties: false
      required:
        - protocol
        - idempotency_key
      properties:
        protocol:
          type: string
          enum:
            - x402
            - mpp
            - pay_per_crawl
          description: "The protocol of the challenge. `pay_per_crawl` is Cloudflare's `crawler-price` form: send `resource_url` and `crawler_price` instead of a challenge header."
        idempotency_key:
          type: string
          minLength: 1
          maxLength: 200
          pattern: ^[A-Za-z0-9._:-]+$
          description: Your key for this payment. The same key returns the same request.
        payment_required_header:
          type: string
          description: "x402 v2: the value of the `PAYMENT-REQUIRED` header."
        payment_required_body:
          type: object
          description: "x402 v1: the JSON body of the 402 response."
        www_authenticate:
          type: string
          description: "MPP: the value of the `WWW-Authenticate` header."
        resource_url:
          type: string
          maxLength: 2048
          description: "`pay_per_crawl` only: the https URL of the page that answered 402. Its host is the signed `@authority`."
        crawler_price:
          type: string
          maxLength: 64
          description: "`pay_per_crawl` only: the value of the `crawler-price` header, exactly as received (`USD 0.01`: the currency, one space, a positive amount with up to 2 decimals)."
    AgentAction:
      type: object
      required:
        - id
        - object
        - agent
        - grant
        - mandate_version
        - nature
        - action
        - protocol
        - amount_usd
        - amount_atomic
        - asset
        - network
        - spread
        - payee
        - domain
        - decision
        - reasons
        - status
        - receipt
        - created_at
        - updated_at
      properties:
        id:
          type: string
          description: The action id (`ozav_aga_...`).
        object:
          type: string
          const: agent_action
          description: Always the literal `agent_action`.
        agent:
          type: string
          description: The agent.
        grant:
          type: string
          description: The grant.
        mandate_version:
          type: integer
          description: The mandate version it was checked against.
        nature:
          type: string
          enum:
            - new_money
            - simulated
            - prepaid
          description: |
            The nature of the money this action moves or asks for, set once when the action is created and never changed.
            `simulated` is sandbox money (nothing moved, nothing was reserved or signed), `new_money` is a production action
            that moves or requests real money, `prepaid` is a consumption of credit the company bought outside OZAV
            (`prepaid_consumption`; no money moves and there is no dollar amount).
            Filter the list with `?nature=`.
        action:
          type: string
          enum:
            - x402_payment
            - mpp_payment
            - fiat_payout
            - fiat_funding
            - payee_registration
            - prepaid_consumption
            - yield_application
            - account_opening
            - account_payout
            - yield_redemption
          description: What was requested.
        protocol:
          type:
            - string
            - "null"
          description: "`x402` or `mpp`."
        amount_usd:
          type:
            - string
            - "null"
          description: "The amount in US dollars, as a decimal string. On a `fiat_funding` it is the money coming in, and on a `yield_application` the amount applied to yield: neither is spending. It is `null` on a `yield_redemption`."
        amount_atomic:
          type:
            - string
            - "null"
          description: The amount in the asset's smallest unit.
        asset:
          type:
            - string
            - "null"
          description: The asset.
        network:
          type:
            - string
            - "null"
          description: The network, as CAIP-2.
        spread:
          description: "OZAV's spread on this payment, charged to the account holder in addition to `amount_usd` / `amount_atomic`, which stay what the payee is paid. 5 bps (0.05%) by default; the rate is fixed when the payment is accepted and `amount_atomic * bps / 10000` is rounded down, so a payment too small for one unit of spread pays none (`amount_atomic` `0`). The account balance must cover the amount plus the spread. The spread is not counted against the grant's budget or limits: they bound what the agent pays out. A spread is released with the payment when it expires or fails, and refunded with a Cloudflare commitment that was not billed. `null` on a payment that carries none (requests made before the spread existed)."
          oneOf:
            - type: object
              required:
                - amount_atomic
                - amount_usd
                - currency
                - bps
              properties:
                amount_atomic:
                  type: string
                  description: The spread in atomic USDC (6 decimals).
                amount_usd:
                  type: string
                  description: The spread in US dollars, as an exact decimal string.
                currency:
                  type: string
                  enum:
                    - USDC
                  description: The spread is always in USDC.
                bps:
                  type: integer
                  description: The rate applied, in basis points.
            - type: "null"
        payee:
          description: "Who is paid: an address on a network, or a registered payout destination. The same shape as `allowed_payees`."
          oneOf:
            - type: object
              required:
                - type
                - network
                - address
              properties:
                type:
                  type: string
                  enum:
                    - address
                  description: An on-chain address.
                network:
                  type: string
                  description: The CAIP-2 network (`eip155:137`).
                address:
                  type: string
                  description: The address paid.
            - type: object
              required:
                - type
                - id
              properties:
                type:
                  type: string
                  enum:
                    - payout_destination
                  description: A registered payout destination.
                id:
                  type: string
                  description: The payout destination id (`ozav_pd_...`).
            - type: "null"
        domain:
          type:
            - string
            - "null"
          description: "The host of the resource paid, as the platform reported it. OZAV cannot verify it: it limits nothing on its own, and the mandate's payees and amounts are what bind the money."
        decision:
          type: string
          enum:
            - within_mandate
            - needs_approval
            - outside_mandate
          description: What the mandate decided.
        reasons:
          type: array
          items:
            type: string
          description: "Why it is outside the mandate, when it is: English codes such as `above_daily_limit`, `above_per_payment_limit`, `corridor_not_allowed`, `payee_not_allowed`, `domain_not_allowed`, `mandate_expired`, `above_recurring_budget`, `above_agent_allocation`."
        status:
          type: string
          enum:
            - refused
            - awaiting_approval
            - authorized
            - signed
            - settled
            - executed
            - awaiting_payment
            - returned
            - expired
            - failed
          description: "Where the request is now. A payout moves from `executed` to `settled` when its transfer completes (the spend stays counted), or to `returned` when the transfer ended without delivering the money (`cancelled` or `expired`): the mandate budget is released. A transfer under review (including a Pix return) or one that failed stays `executed` until OZAV reviews it, and the receipt's `transfer_status` shows it (`failed`, `needs_review`)."
        receipt:
          description: "The settlement receipt, once there is one. For a signed stablecoin payment, OZAV asks the token contract whether the authorization was used: `authorization_used: true` with `checked_at` is the settlement proof, and `tx_hash` is the on-chain transaction that used it, when OZAV found it (evidence you can open in a block explorer; absent does not mean unsettled). For a local-currency payout, the transfer OZAV created (`transfer_id`, `transfer_status`, `quote_id`, `source_amount`, `target_amount`), or why it was refused before any money moved."
        created_at:
          type: string
          format: date-time
          description: When it was recorded.
        updated_at:
          type: string
          format: date-time
          description: When it last changed.
        consumption:
          type: object
          description: "Only on a `prepaid_consumption`: what was consumed, and what it left."
          required:
            - resource_id
            - units
            - seller_domain
            - unit
            - balance_after
          properties:
            resource_id:
              type:
                - string
                - "null"
              description: The prepaid resource (`ozav_ppr_...`).
            units:
              type:
                - string
                - "null"
              description: The units consumed, a whole number as a string.
            seller_domain:
              type:
                - string
                - "null"
              description: The seller the resource was bought from.
            unit:
              type:
                - string
                - "null"
              description: What a unit is (`tokens`). Absent on a refusal.
            balance_after:
              type:
                - string
                - "null"
              description: The balance this consumption left. Absent on a refusal.
        yield_application:
          type: object
          description: "Only on a `yield_application`: the strategy and amount applied from the company's custodied balance, and the managed yield order once it exists. `amount_usd` carries the dollars (an application is not spending)."
          required:
            - strategy_id
            - amount
            - currency
            - chain
            - application_id
          properties:
            strategy_id:
              type:
                - string
                - "null"
              description: The strategy (`GET /yield/strategies`).
            amount:
              type:
                - string
                - "null"
              description: The amount applied, as a decimal string in `currency`.
            currency:
              type:
                - string
                - "null"
              description: The strategy's asset, for example `USDC`.
            chain:
              type:
                - string
                - "null"
              description: The strategy's network, for example `base`.
            application_id:
              type:
                - string
                - "null"
              description: The managed yield order. `null` until the order exists.
        yield_redemption:
          type: object
          description: "Only on a `yield_redemption`: the strategy and the amount (or `all`) taken back to the company's custody address, and the managed yield order once it exists. A redemption is not spending, so `amount_usd` is `null`."
          required:
            - strategy_id
            - all
            - amount
            - currency
            - chain
            - redemption_id
          properties:
            strategy_id:
              type:
                - string
                - "null"
              description: The strategy (`GET /yield/strategies`).
            all:
              type: boolean
              description: "`true` when the whole position was asked for."
            amount:
              type:
                - string
                - "null"
              description: The amount asked, as a decimal string in `currency`. `null` when `all` is true.
            currency:
              type:
                - string
                - "null"
              description: The strategy's asset, for example `USDC`.
            chain:
              type:
                - string
                - "null"
              description: The strategy's network, for example `base`.
            redemption_id:
              type:
                - string
                - "null"
              description: The managed yield order. `null` until the order exists.
        account:
          type: object
          description: "Only on `account_opening` and `account_payout`: the named account the opening asked for and created (its `id` exists once the account does), or the one a payout pays from."
          required:
            - id
            - currency
          properties:
            id:
              type:
                - string
                - "null"
              description: The account (`ozav_acc_...`).
            currency:
              type:
                - string
                - "null"
              description: The account's currency.
            country:
              type:
                - string
                - "null"
              description: "Only on an opening: the country asked."
            rail:
              type:
                - string
                - "null"
              description: "Only on an opening: the rail asked."
        payment:
          type: object
          description: "Only in the response that signed the payment (the POST, or the platform's first read of an approved payment): the header to send back to the resource. Delivered once."
          required:
            - header_name
            - header_value
            - valid_before
            - simulated
          properties:
            header_name:
              type: string
              enum:
                - PAYMENT-SIGNATURE
                - X-PAYMENT
                - Authorization
                - crawler-exact-price
              description: The header to set.
            header_value:
              type: string
              description: Its value.
            valid_before:
              type: string
              format: date-time
              description: When the signed authorization stops being valid.
            simulated:
              type: boolean
              description: "True in the sandbox: the signature is all zeros and no resource accepts it."
            headers:
              type: object
              description: "Only on Cloudflare pay-per-crawl (`cloudflare:402`): every header to send with the request, the payment and the HTTP message signature that covers it. With `protocol: pay_per_crawl` they are `crawler-exact-price`, `signature-agent`, `signature-input` and `signature`."
              additionalProperties:
                type: string
        payout:
          type: object
          description: "Only on a local-currency payout: the transfer OZAV created for it."
          properties:
            transfer_id:
              type: string
              description: The `ozav_tr_` transfer, in the company's account.
            transfer_status:
              type: string
              description: The transfer's current status (`payout_initiated`, `completed`, `failed`, `needs_review`, and so on). OZAV keeps it up to date after the payout is created; a change arrives as `agent_action.updated` even when the action's own status does not move.
            source_type:
              type: string
              enum:
                - agent_wallet
              description: Present when the payout was funded from the agent's own wallet.
            quote_id:
              type: string
            source_amount:
              $ref: "#/components/schemas/DecimalAmount"
            target_amount:
              $ref: "#/components/schemas/DecimalAmount"
            simulated:
              type: boolean
              description: "`true` in the sandbox: no money moved and nothing external was called."
        funding:
          type: object
          description: "Only on a funding: the Pix charge OZAV created, in the company's account."
          properties:
            transfer_id:
              type: string
              description: The `ozav_tr_` transfer that settles when the company pays.
            transfer_status:
              type: string
              description: The transfer's status when this response was built.
            quote_id:
              type: string
            source_amount:
              $ref: "#/components/schemas/DecimalAmount"
            source_currency:
              type: string
              enum:
                - BRL
            target_amount:
              $ref: "#/components/schemas/DecimalAmount"
            target_currency:
              type: string
              enum:
                - USDC
                - USDT
            simulated:
              type: boolean
              description: "`true` in the sandbox: the Pix code is fake and no bank accepts it."
            payment_instructions:
              type:
                - object
                - "null"
              description: What to hand to whoever pays. Present while the charge can still be paid.
              properties:
                rail:
                  type: string
                  enum:
                    - pix
                br_code:
                  type:
                    - string
                    - "null"
                  description: The Pix copy-and-paste code.
                qr_code_image:
                  type:
                    - string
                    - "null"
                  description: The QR code as a data URL.
                expires_at:
                  type:
                    - string
                    - "null"
                  format: date-time
        exception:
          allOf:
            - $ref: "#/components/schemas/AgentActionException"
          description: Only in the webhook `agent_action.updated` sent when the action's exception case (dispute) is opened or changes status. Read the case with `GET /agent-actions/{action_id}/exceptions`.
    Decision:
      type: object
      required:
        - id
        - object
        - type
        - effect
        - reasons
        - checks
        - policy_version
        - outcome
        - outcome_at
        - created_at
      properties:
        id:
          type: string
          description: The decision id (`ozav_dec_...`).
        object:
          type: string
          const: decision
          description: Always the literal `decision`.
        type:
          type: string
          description: The decision type requested.
          enum:
            - name.match
            - destination.ownership
            - wallet.risk
            - transaction.outbound
        effect:
          type: string
          description: "`ALLOW`, `REVIEW` (a person should look) or `DENY` (OZAV holds the proof of the risk)."
          enum:
            - ALLOW
            - REVIEW
            - DENY
        reasons:
          type: array
          description: Why. When `effect` is not `ALLOW`, only the reasons that caused it.
          items:
            type: object
            required:
              - code
              - message
            properties:
              code:
                type: string
                description: Stable machine code, e.g. `NAME_MATCHES_SANCTIONS_LIST`.
              message:
                type: string
                description: A human-readable sentence.
        checks:
          type: array
          description: Each check that ran and what it found.
          items:
            type: object
            required:
              - check
              - result
            properties:
              check:
                type: string
                description: "`sanctions_list`, `designated_addresses` or `destination_ownership`."
              result:
                type: string
                description: What the check found.
        policy_version:
          type: string
          description: The version of OZAV's rules that produced the verdict.
        outcome:
          type:
            - string
            - "null"
          description: The outcome you reported, or `null`.
          enum:
            - confirmed_risk
            - false_positive
            - completed
            - reversed
            - chargeback
            - null
        outcome_at:
          type:
            - string
            - "null"
          format: date-time
          description: When the outcome was reported.
        created_at:
          type: string
          format: date-time
          description: When the decision was made.
    ListEntry:
      type: object
      required:
        - id
        - object
        - list
        - type
        - value_hint
        - created_at
      properties:
        id:
          type: string
          description: The entry id (`ozav_lst_...`).
        object:
          type: string
          const: list_entry
          description: Always the literal `list_entry`.
        list:
          type: string
          description: "`block` or `allow`."
          enum:
            - block
            - allow
        type:
          type: string
          description: The value type.
          enum:
            - name
            - document
            - email
            - email_domain
        action:
          type:
            - string
            - "null"
          description: "`review` or `decline` for block entries; `null` for allow entries."
          enum:
            - review
            - decline
            - null
        value_hint:
          type: string
          description: A masked hint of the value (e.g. `J*** D***`, `•••789`, `j***@***`). Never the full value, never an email domain; a document shorter than 8 characters shows only `•••`.
        created_at:
          type: string
          format: date-time
          description: When the entry was created.
    ListEntryDeleted:
      type: object
      required:
        - id
        - object
        - deleted
      properties:
        id:
          type: string
          description: The entry id (`ozav_lst_...`).
        object:
          type: string
          const: list_entry
          description: Always the literal `list_entry`.
        deleted:
          type: boolean
          const: true
          description: Always `true`.
    EvidencePack:
      type: object
      description: |
        What OZAV verified and decided about one customer, assembled at request
        time. An empty section means absence of record, never approval.
      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
    ScreeningSignal:
      type: object
      description: One explained finding of a screening.
      required:
        - code
        - category
        - severity
        - evidence
        - why_it_matters
      properties:
        code:
          type: string
          description: |
            Stable machine code of the finding. A plain string, not a closed enum: new codes are
            added over time, so treat an unknown code by its `category` and `severity`. Known values:
            - `sanctions_match` / `sanctions_possible_match` - strong / partial sanctions-list name match.
            - `pep_match` / `pep_possible_match` - strong / partial PEP-register name match.
            - `disposable_email` - disposable (temporary) mailbox service.
            - `email_domain_not_found` - the email domain does not exist (businesses).
            - `email_domain_no_mx` - the email domain has no mail server (businesses).
            - `email_domain_dns_unresolved` - the email domain's DNS did not answer; not evaluated.
            - `invalid_email_syntax` - not a name@domain address.
            - `recently_registered_domain` - company domain registered less than 90 days ago.
            - `free_email_for_business` - a company using a free email provider.
            - `domain_without_spf_dmarc` - company domain publishes neither SPF nor DMARC.
            - `domain_unrelated_to_name` - company domain contains no word of the company name.
            - `invalid_phone` - not a valid number for the given country.
            - `non_personal_phone` - virtual, toll-free or premium-rate line.
            - `phone_country_mismatch` - calling code differs from the declared country.
            - `org_blocklist_match` - the subject is on your organization's block list.
            - `org_allowlist_match` - the subject is on your organization's allow list; contact signals
              were suppressed (block-list, sanctions and PEP matches never are).
            - `org_lists_unavailable` - your own lists could not be checked; screen again shortly.
            - `pep_exact_name_only` - the name is too common on the PEP register to be compared word
              by word, so it was compared by exact name and aliases only. `result` is `review`, and
              this is not transient: retrying returns the same. Have a person review the subject.
        category:
          type: string
          description: |
            Where the finding comes from:
            - `sanctions_list` - a sanctions / designated-persons list.
            - `pep_list` - a politically-exposed-persons register.
            - `contact` - the email or phone sent with the subject.
            - `org_list` - your organization's own block/allow lists (`/lists/entries`).
          enum:
            - sanctions_list
            - pep_list
            - contact
            - org_list
        severity:
          type: string
          description: |
            How much the finding weighs. Every list signal (`sanctions_list`, `pep_list`) is `high`.
            - `high` - escalates the decision on its own.
            - `medium` - below the escalation threshold; a human weighs it with the rest.
            - `low` - informational.
          enum:
            - high
            - medium
            - low
        evidence:
          type: string
          description: What was seen, in plain language.
        why_it_matters:
          type: string
          description: Why this finding matters for due diligence.
    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:
            - `sanctions` - consolidated sanctions / designated-persons lists.
            - `pep` - politically exposed persons.
            - `adverse_media` - negative-news screening.
          items:
            type: string
            enum:
              - sanctions
              - pep
              - adverse_media
        status:
          type: string
          description: |
            Processing state of the screening:
            - `pending` - the screening is still running.
            - `completed` - the screening finished; read the outcome from `result`.
          enum:
            - pending
            - completed
        result:
          type: string
          description: |
            Conservative, fail-closed outcome:
            - `clear` - every requested list was fully consulted with zero matches.
            - `match_found` - a strong (exact/alias) designated-list match.
            - `review` - a weaker match, or a requested list that could not be fully
              consulted; never silently cleared.

            Telling the two `review` causes apart, because they call for opposite actions and the
            answer is already in this response - read `matches`:

            - `review` with a non-empty `matches` - a weak (token-level) hit. A human decides.
              Retrying changes nothing; the same subject screens the same way.
            - `review` with an empty `matches` - every list you requested came back without a hit,
              and at least one of them could not be fully consulted. Nobody matched anything. This
              is transient on our side: retry later rather than queueing the subject for review.
              One exception, flagged in `signals`: `pep_exact_name_only` means the name is too
              common on the PEP register for a word-by-word comparison, so only exact name and
              aliases were compared. That is not transient - retrying returns the same; queue the
              subject for human review.

            The two are exhaustive for `review`, by construction: a weak hit is counted from the
            same array that `matches` is serialized from, so an empty `matches` leaves an
            unconsultable source as the only remaining cause.

            Which source degraded is not published. It is an operational fact about OZAV's
            ingestion, not about your subject, and the action it implies (retry) is the same
            whichever one it was.
          enum:
            - clear
            - match_found
            - review
        matches:
          type: array
          description: |
            The matches found. Empty when the outcome is `clear` - and also empty on the
            `review` that means a list could not be consulted, which is why `result` and this
            field have to be read together. See `ScreeningMatch`.
          items:
            $ref: "#/components/schemas/ScreeningMatch"
        suggested_decision:
          type: string
          description: |
            OZAV's suggested decision for this subject, computed and stored when the screening ran
            (reading it later returns that decision, not a recomputation). It only escalates:
            - `decline` - a strong (exact/alias) sanctions match corroborated by a second attribute:
              the `date_of_birth` you sent matches the date on the list entry. A name-only match -
              no date on either side, or a different date - is `review`, never `decline`, because a
              homonym must not be declined automatically.
              Consequences: a `business` subject cannot reach `decline` (businesses carry no date
              of birth), and a `customer_id` subject is screened by name only today (the
              customer's stored date of birth is not used) - both stop at `review`.
            - `review` - any list signal (every sanctions or PEP signal escalates), a `high`
              contact signal, or a `review` result (see `result`).
            - `clear` - no list match, every requested list consulted, and any contact signals
              below the escalation threshold (`medium`/`low` contact signals may be present).
            `result` keeps its list-only meaning; `suggested_decision` also weighs contact signals.
          enum:
            - clear
            - review
            - decline
        signals:
          type: array
          description: |
            Every finding behind `suggested_decision`, explained: what was seen and why it matters.
            List matches appear here too (with `category` `sanctions_list` or `pep_list`), next to
            contact signals (`contact`) when email/phone were sent and contact signals are enabled.
            An empty array is only "nothing found" together with `contact_evaluated` (see below).
          items:
            $ref: "#/components/schemas/ScreeningSignal"
        contact_evaluated:
          type: boolean
          description: |
            `true` when the email/phone you sent were fully evaluated. `false` when no email or
            phone was sent, contact signals are disabled on the OZAV platform (a platform-wide
            setting, not per account), the evaluation failed, or the email domain's DNS did not
            answer in time (then `email_domain_dns_unresolved` appears in `signals`). With `false`,
            the absence of contact signals says nothing about the contact data.
        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.
        Completing or simulating a verification updates the customer's KYC
        status. In sandbox, approval can materialize simulated wallet and
        BRL/Pix account projections. Live wallet provisioning and live bank
        account opening remain separate enabled steps.
      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
            capture (government ID, selfie, liveness; KYB/UBO for a business customer).

            **In sandbox this field never appears.** A `test` key never starts a hosted
            session, because a real session bills a third party per attempt. Exercise the outcome
            with `POST /verifications/{verification_id}/simulate` instead - that is the sandbox
            affordance, and it lets you drive `approved` or `rejected` yourself.

            **With a `live` key, two things must both be true**, and the second is not something you
            control:

            1. Your `capabilities` include at least one that hosted capture covers:
               `kyc_document`, `liveness`, `face_match`, `age_estimation`, `kyb`, `ubo`. Asking only
               for list/data checks (`aml_name_screening`, `email_verify`, `economic_capacity`, ...)
               never starts a session, because there is nothing for the end-user to do.
            2. Hosted capture is switched on at OZAV. This is a single platform-wide setting,
               **not** something enabled per organization - there is nothing to request for your
               account. While it is off, no `live` key starts a session.

            When either condition is missing - or the key is a `test` key - the verification is still
            created and is a valid record: it simply stays `pending` with `checks` at `not_started`
            and **no `session_url`**. That is not an error and retrying will not produce one.

            The field is never fabricated: if the session cannot be created, the request fails with a
            `503` and nothing is persisted, rather than returning a record with a broken link. The
            `code` says whether retrying helps: `VERIFICATION_SESSION_UNAVAILABLE` (retry shortly),
            `VERIFICATION_TEMPORARILY_UNAVAILABLE` (on OZAV's side, not caused by your data - retry
            later with the same data), `VERIFICATION_PROVIDER_REJECTED` or
            `VERIFICATION_PROVIDER_NOT_CONFIGURED` (contact OZAV support with the `request_id`).
        status:
          type: string
          description: |
            Overall state of the verification session:
            - `pending` - created; checks have not started.
            - `in_progress` - one or more checks are running.
            - `completed` - every check reached an outcome.
            - `expired` - the session expired before completing.
          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).
    Statement:
      type: object
      description: A monthly service statement. Amounts are in USD.
      required:
        - object
        - id
        - month
        - status
        - volume_usd
        - transactions
        - active_users
        - spread_client_share_usd
        - amount_due_usd
        - amount_credit_usd
        - issued_at
        - due_at
        - paid_at
        - grace_period
        - created_at
      properties:
        object:
          type: string
          const: statement
          description: Resource type discriminator; always `statement`.
        id:
          type: string
          description: "`ozav_stmt_` followed by the year and month."
          example: ozav_stmt_202609
        month:
          type: string
          description: The month the statement covers (YYYY-MM).
          example: 2026-09
        status:
          type: string
          enum:
            - pending
            - issued
            - overdue
            - paid
            - cancelled
          description: "`issued` past its due date reads as `overdue`."
        volume_usd:
          type: number
          description: Volume processed in the month.
        transactions:
          type: integer
          description: Number of transactions in the month.
        active_users:
          type: integer
          description: Customers active in the month.
        spread_client_share_usd:
          type: number
          description: Your share of the spread for the month.
        amount_due_usd:
          type: number
          description: What you owe OZAV for the month.
        amount_credit_usd:
          type: number
          description: What OZAV owes you for the month.
        issued_at:
          type:
            - string
            - "null"
          format: date-time
          description: When the statement was issued.
        due_at:
          type:
            - string
            - "null"
          format: date-time
          description: When payment is due.
        paid_at:
          type:
            - string
            - "null"
          format: date-time
          description: When it was paid.
        grace_period:
          type: boolean
          description: Whether the month fell in a grace period with no charge.
        created_at:
          type: string
          format: date-time
          description: When the statement was generated.
    StatementList:
      type: object
      required:
        - object
        - data
        - has_more
        - next_cursor
      properties:
        object:
          type: string
          const: list
          description: Always `list`.
        data:
          type: array
          description: The statements, newest first, up to the last 36 months.
          items:
            $ref: "#/components/schemas/Statement"
        has_more:
          type: boolean
          description: Always `false`; the list is not paginated.
        next_cursor:
          type: "null"
          description: Always `null`.
    StatementDetail:
      allOf:
        - $ref: "#/components/schemas/Statement"
        - type: object
          required:
            - lines
          properties:
            lines:
              type: array
              description: The itemized lines of the statement.
              items:
                type: object
                required:
                  - component
                  - payment_rail
                  - quantity
                  - unit_price_usd
                  - amount_usd
                properties:
                  component:
                    type: string
                    description: What the line charges or credits.
                  payment_rail:
                    type:
                      - string
                      - "null"
                    description: The payment rail
                    when the line is per rail.: null
                  quantity:
                    type: number
                    description: Quantity billed.
                  unit_price_usd:
                    type: number
                    description: Price per unit.
                  amount_usd:
                    type: number
                    description: Line total.
    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:
            - `test` - the sandbox; moves no real money.
            - `live` - production; can move real money.
          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
            a label and a mint-time template - **not** part of the permission
            decision, which stays `entitlements ∩ scopes`. A rotated key keeps it.

            **Absent** on a key whose scopes were listed one by one, which is
            every key created before roles existed. The field is omitted rather
            than sent as `null`, because `null` here would read as "this key has
            no powers" instead of "this key was not minted from a template".
        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.
    WebhookEventType:
      type: string
      description: |
        Event types currently emitted by API v2 webhooks. The catalog lists only
        events that actually fire today:
        - `webhook.test` - queued by `POST /webhook-endpoints/{id}/test`.
        - `customer.created` / `customer.updated` - emitted by the v2 customers surface.
        - `verification.created` / `verification.updated` - emitted by the v2 verification surface.
        - `screening.completed` - emitted when a screening completes.
        - `transfer.updated` / `transfer.completed` / `transfer.failed` - emitted by the v2 transfer surface.
        - `card.created` / `card.updated` - **card lifecycle only.** `card.created` fires on
          `POST /cards`; `card.updated` on a spending-controls change (`PATCH /cards/{id}`), on
          freeze / unfreeze / close, and when the card processor changes the card's status on
          its side: it confirms a `pending` card (now `active`), or freezes, closes (e.g. reported
          lost) or expires it. A processor change can arrive without any call of yours. Apart from
          confirming a `pending` card, it only ever restricts the card, never reopens it.
        - `card.fraud_confirmed` - emitted when OZAV reviews declines on one of your cards and
          confirms they were fraud. `data` is `{ card, confirmed_at }`, with `card` in the same
          shape as `card.updated`. **It is a notice, not an action:** the card is not frozen or
          closed by it. Deciding what to do with the card is yours - `POST /cards/{card_id}/freeze`
          or `/close`. It fires at most once per review, is never sent for a decline OZAV judged
          legitimate, and carries no internal identifiers.
        - `card.fraud_case_closed` - emitted when OZAV finishes reviewing declines on one of your
          cards, whatever the outcome. `data` is `{ card, outcome, fraud_category, decline_ids,
          closed_at }`, with `card` in the same shape as `card.updated` and `outcome` one of
          `fraud_confirmed`, `false_positive` (the declines were legitimate purchases: the
          cardholder can try again) or `inconclusive`. One review covers the declines of one fraud
          rule on the card: `decline_ids` lists them with the same `id`s that
          `GET /cards/{card_id}/declines` returns, oldest first, once per purchase even if the
          card processor retried it. If the review covered more than 500, these are the oldest
          500; the rest are on `/declines`. A decline with no `card.declined` (for example, the
          card had no public id yet, or the card processor sent no authorization reference) is not
          listed, so the list can be empty. `fraud_category` is the kind of rule, with the same
          values as on `card.declined`, and `null` if that rule no longer exists. A `fraud_confirmed` review also sends `card.fraud_confirmed`. It fires at
          most once per review and carries no internal identifiers.


        - `card.authorized` - emitted when a purchase on one of your cards is **approved** and the
          amount is held. `data` is `{ card_id, transaction_id, amount_usd, merchant }`. It is sent
          after the decision has already been returned to the card processor, never inside the
          processor's authorisation window, so subscribing to it cannot slow down or decline a
          purchase.
        - `card.settled` - emitted when a purchase **clears**. `data` is
          `{ card_id, transaction_id, amount_usd, settled_at, merchant }`. `amount_usd` is the
          final amount, which can differ from the authorised one (tips, fuel adjustments) - use this
          one for your ledger, not the authorised figure.


          `transaction_id` (`ozav_ctxn_...`) is the `id` of the same purchase in
          `GET /cards/{card_id}/authorizations` and `GET /cards/{card_id}/transactions`, so the
          authorisation, its settlement and the row in your ledger reconcile on one key. A settlement
          of an approved hold carries the same value as its `card.authorized`, including a settlement
          that reaches OZAV before the hold was recorded. Only a second capture against the same
          authorisation (a split shipment) gets its own row and its own id. On `card.authorized` it
          is `null` when the hold row does not exist or could not be read when the event was sent
          (the authorisation was released first, or recording the hold failed): read
          `/authorizations`, or wait for its `card.settled`.


          `card.settled` can arrive before `card.authorized` for the same purchase. When both carry
          the same `transaction_id`, upsert on it and never move a settled row back to authorised.


          Both carry the card's public id, never an internal identifier, and are de-duplicated on
          the processor's own reference: a redelivered authorisation or settlement does not notify
          you twice. A card that has never been seen through the v2 API has no public id, so no
          event is sent for it.


        - `card.declined` - emitted when a purchase on one of your cards is **refused**. `data` is
          `{ card_id, amount_usd, decline_code, fraud_category, merchant }`. A purchase OZAV refuses
          leaves no hold and no row in `/authorizations`; it is listed on
          `GET /cards/{card_id}/declines`, the same refusal this event announced.


          `fraud_category` says which kind of fraud rule refused the purchase, and is `null` for
          every `decline_code` other than `fraud_rule`. It is one of `velocity` (too many purchases
          or too much spend in a short window), `transaction_amount` (one purchase above a limit),
          `period_spend` (daily or monthly spend above a limit), `merchant_category`,
          `merchant_country`, `time_of_day`, or `other`. The rule's own name and text are never
          sent.


          `decline_code` is one of: `insufficient_balance`, `card_frozen`, `card_inactive`,
          `account_inactive`, `spending_limit`, `mcc_blocked`, `country_blocked`,
          `presence_blocked`, `fraud_rule`, or `temporarily_unavailable`.


          When the refusal is on our side rather than the cardholder's,
          you get `temporarily_unavailable` instead of our internal reason. It means the purchase
          can be retried and the cardholder did nothing wrong - which is the only part you can act
          on. We never send the operator-facing text behind a decision.
        - `card.refunded` - emitted when a merchant **returns** money to one of your cards. `data`
          is `{ card_id, transaction_id, amount_usd, refunded_at, merchant }`, with
          `transaction_id` the refund's own row in `GET /cards/{card_id}/transactions`.


          `amount_usd` is **positive**: the direction travels in the event type, not in the sign,
          for the same reason `/transactions` reports `direction: credit` rather than a negative
          number. A refund is not an authorisation - the merchant returns and the value credits,
          with nobody asked - so it never appears in `/authorizations`.
        - `card.reversed` - emitted when a held authorisation is released **without settling**:
          the merchant cancelled it, it expired, or the card issuer declined it after OZAV
          approved. The held amount and its surcharge return to the balance. `data` is `{ card_id,
          transaction_id, amount_usd, client_surcharge_usd, reason, reversed_at }`, with
          `transaction_id` the same as in the `card.authorized` it closes (null if the hold was
          never recorded) and `reason` one of `reversed`, `expired` or `declined`. In the sandbox,
          trigger it with `POST /cards/{card_id}/simulate/reversal`.
        - `card.three_ds_code` - emitted when the card issuer asks for a 3-D Secure one-time code
          on an online purchase and sends the code to OZAV instead of to the cardholder. Show it to
          your cardholder so they can finish the purchase. `data` is `{ card_id, code, amount,
          currency, expires_at }`, with `expires_at` an ISO 8601 UTC timestamp; `amount`,
          `currency` and `expires_at` are null when the issuer does not report them. A code that has
          already expired is not sent. The code works for that one purchase only: do not log or
          store it. Webhook redelivery sends the same code again until it expires.
        - `card.deletion_scheduled` - emitted when a card has been frozen long enough that the card
          issuer will delete it. `data` is `{ card_id, frozen_days, deletes_at }`, with `deletes_at`
          an ISO 8601 UTC timestamp counted from when the card was frozen (null if that is unknown).
          Unfreeze the card before then to keep it; a deleted card cannot be restored. You get one
          notice per freeze and day count, so a card frozen again is warned again.


          In the sandbox you create one with `POST /cards/{card_id}/simulate/purchase` and
          `is_refund: true` in the body.
        - `account.updated` - emitted when a named account's public status changes, so an
          integrator learns their customer can receive money without polling GET /accounts.
        - `account.credited` - emitted when money arrives in a customer's BRL named account.
          `data` is `{ account_id, movement }`, with `movement` in the same shape
          `GET /accounts/{account_id}/movements` lists it, and the same `id`. The credit is
          read from the account's statement every 10 minutes, so the event can arrive up to
          that long after the payer sent the Pix. It fires once per credit. In the sandbox
          you create one with `POST /accounts/{account_id}/simulate/credit`.
        - `deposit.credited` - emitted when crypto sent to one of your wallets has been credited
          and is spendable. Receiving crypto needs no call from you: the arrival is observed on
          chain and credited on its own, and it already shows up in `GET /transactions` and in
          `GET /wallets/{wallet_id}/balances`. This event is the push, so you do not have to poll
          for it. There is no `deposit.pending`: before a deposit is credited it can
          still reorganize on chain, and telling you early would invite you to act on value that
          may vanish.
        - `requirement.due` - emitted when compliance review determines which documents are still
          missing for a customer, so you learn what to collect without polling. Body:
          `{customer_id, object: requirement_request, due_at, reason, reason_label,
          documents: [{key, label, destination?}], purpose?, missing_fields?}`. With
          `reason: destination_unidentified` (`purpose: travel_rule`) each entry of
          `documents` carries `destination: {chain, address}`: say who holds that wallet with
          `POST /customers/{customer_id}/destination-identifications`. When the ask is for registration
          data rather than a file (`reason: registration_incomplete` or `data_inconsistent`),
          `missing_fields` lists what to send, in the same tokens as
          `requirements.missing_fields` (e.g. `phone`, `residential_address`,
          `expected_monthly_volume_usd`): send them with `PATCH /customers/{customer_id}` (or, for
          `owners`, `POST /customers/{customer_id}/associated-persons`). Absent when nothing asked
          for is a field. `tax_id` / `registration` there cannot be corrected once the customer
          is approved or frozen (`409 TAX_ID_IMMUTABLE`): create a new customer instead. Emitted
          once per ask, like a document ask. There is no
          `requirement.satisfied`: the customer's own `GET /customers/{id}` shrinks
          `requirements.documents_due` when a file lands, and announcing a second event would need
          a second emitter to avoid becoming an empty promise.
        - `document.reviewed` - emitted when OZAV's own document review accepts or rejects a
          supporting document (proof of address, bank statement, proof of income, company
          document): `{customer_id, document_id, key, label, status: accepted|rejected, reason,
          reason_label, reviewed_at}`. A rejection is followed by a fresh `requirement.due` for the
          same document, with a new deadline and the `reason`. Not emitted while a document is
          waiting on a human reviewer.
        - `wallet_policy.updated` - emitted when a change to a managed wallet's policy is
          decided: `{wallet_id, change: {id, kind: destination.add|limits.update, status, ...}}`.
          For a destination, `status: applied` means it is already in `destinations.allowed`.
          For limits, `status: approved` means accepted and not yet in force - the signed record
          is issued minutes later; confirm with `GET /wallets/{wallet_id}/policy`
          (`limits.valid_until`). `rejected` carries a `reason` when one was given. Not emitted
          when you PATCH: requesting is not changing.
        - `transfer.approval_required` - not emitted: no send waits on approver keys.
        - `limit_request.updated` - emitted when the public status of a limit increase request
          (`POST /customers/{customer_id}/limit-requests`) changes: `awaiting_documents`,
          `in_review`, `approved` (with `approved_limit`) or `declined`. `data` is the
          `LimitRequest` itself. Once per status. A decline carries no reason.
        - `agent_action.updated` - emitted when the public status of an agent action
          (`POST /agents/{agent_id}/payments`, `/payouts`, `/fundings`, `/payees`, `/consumptions`,
          `/yield-applications` or `/accounts`, and `POST /agent-actions/{action_id}/simulate` in the
          sandbox) changes, to both the
          agent platform and the company whose money it is. `data` is the `AgentAction` itself. Once
          per status. `authorized` after `awaiting_approval` means a person at the company approved: a payout then runs on its own, and for a payment you repeat
          the request with the same `idempotency_key` to receive the signature. `settled`
          means the token contract shows the authorization used. `returned` means a payout's transfer ended
          without delivering the money (`cancelled` or `expired`): the mandate's budget is released.
          A payout becomes `settled` when its transfer completes. A transfer that needs review (including a Pix return) or that failed
          stays `executed` until OZAV reviews it; the event is still sent when the receipt's `transfer_status` changes.
          A funding (`POST /agents/{agent_id}/fundings`) is announced `awaiting_payment` when its Pix charge exists, then
          `settled` when the company paid and the stablecoin arrived, or `returned` if the charge expired unpaid. A refusal carries the mandate's reasons
          only; compliance refusals carry no reason. Events are not guaranteed to arrive in order, and a
          status that changes twice before OZAV reads it (for example `authorized` then `signed` in one
          request) is announced once, as the status it has then: `GET /agent-actions/{action_id}` is the
          source of truth. An action waiting for approval that nobody decides ends as `expired`.
          When an exception case (dispute) on the action is opened or changes status (`open`,
          `under_review`, `resolved_refunded`, `resolved_denied`), the same event goes out with the case
          in `data.exception`, once per case status.
        - `agent_wallet_withdrawal.updated` - emitted to the account holder's organization when a withdrawal
          from an agent's wallet (`POST /agents/{agent_id}/wallet/withdraw`, or the one OZAV starts when the
          agent or its grant is revoked) is created (`processing`, or `empty` when a revoked agent's wallet
          held nothing) and when it ends: `settled`, `refused` (`settled_amount` says what moved before the
          refusal) or `in_review`. `data` is the `AgentWalletWithdrawal` itself. Once per status; a
          withdrawal that pauses and carries on later is not announced again until it ends. A status
          that changes before OZAV reads it is announced once, as the status it has then, and
          events may arrive out of order. Repeating the request with the same `idempotency_key`
          returns the withdrawal as it is now: that answer is the source of truth.
      enum:
        - webhook.test
        - customer.created
        - customer.updated
        - verification.created
        - verification.updated
        - screening.completed
        - transfer.updated
        - transfer.completed
        - transfer.failed
        - card.created
        - card.updated
        - card.fraud_confirmed
        - card.fraud_case_closed
        - card.authorized
        - card.settled
        - card.declined
        - card.refunded
        - card.reversed
        - card.three_ds_code
        - card.deletion_scheduled
        - account.updated
        - account.credited
        - deposit.credited
        - requirement.due
        - document.reviewed
        - wallet_policy.updated
        - transfer.approval_required
        - limit_request.updated
        - agent_action.updated
        - agent_wallet_withdrawal.updated
    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
            queued for later; events that occur while disabled are not replayed on re-enable.

            OZAV does not disable an endpoint on its own. A URL that has failed every delivery for
            weeks still reads `active`, so `status` alone is not a health signal - read `health`.
          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
            a while means it has never delivered, which is the failure that otherwise stays
            invisible for weeks. Absent when the aggregate could not be read; its absence never
            fails the request.

            `last_error` separates the two opposite causes, because the fix differs: an `HTTP 401`
            means your server answered and refused (a signature problem), while a transport error
            such as `dns error: ...` means nothing answered at all (hostname, TLS, firewall) and no
            signature change will help.
          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
        and network failures with backoff until the delivery succeeds or exhausts its
        attempt budget. Response bodies are stored only as truncated previews.
      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
        JSON body bytes with HMAC-SHA256. Deliveries include:
        - `X-OZAV-Signature: t=<unix>,v1=<hex>`, where `v1` is HMAC-SHA256 over
          `"<t>.<raw body>"`, keyed with your endpoint secret **exactly as returned,
          including the `ozav_whsec_` prefix**. The prefix is part of the key, not a
          display label: stripping it produces a completely different digest and every
          delivery fails verification. `v1` is lowercase hex; parse it out of the header
          rather than comparing the whole `t=...,v1=...` value.
        - `X-OZAV-Event-ID`, stable across retries for idempotency.
        - `X-OZAV-Delivery-ID`, unique to this endpoint/event delivery row.

        Reject signatures whose timestamp is outside your tolerance window (five
        minutes is recommended), and dedupe by `X-OZAV-Event-ID`.
      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"
        livemode:
          type: boolean
          description: |
            `true` for an event about live resources, `false` for one from the sandbox. An
            endpoint created in the dashboard before it asked for an environment receives both, so
            branch on this field rather than on how the endpoint was registered. `GET /events` always returns it. A webhook delivery
            carries it on every event emitted since the field was introduced; a retry of an older
            event can arrive without it. When it is missing, look the event up with
            `GET /events/{event_id}`: each key finds only the events of its own environment. An
            endpoint registered with an API key only ever receives events of that key's environment.
            Before this field was introduced, the `customer.updated` OZAV sends on its own after a
            review (a status change, a new limit or a request for information), `wallet_policy.updated` about a wallet with only a
            sandbox id, and `account.updated` about a sandbox account could be recorded as
            production; such an older event keeps `livemode: true`.
        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. Events about transfers,
            customers, cards, verifications and screenings carry it under the resource's own
            name: `data.transfer`, `data.customer`, `data.card`, `data.verification`,
            `data.screening`. Other events document their own `data` in the event list.

            It is a snapshot at event time, and it can be stale by the time you read it. When the
            current state matters (a decision about money), re-fetch by id rather than acting on
            this body. Deliveries can also arrive out of order.
    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):
                - `invalid_request_error` - malformed or unfulfillable input.
                - `authentication_error` - missing, invalid, revoked, or expired key.
                - `permission_error` - authenticated but not permitted (entitlement or scope).
                - `not_found_error` - the resource does not exist or is not visible.
                - `conflict_error` - conflicts with existing state (e.g. idempotency-key reuse).
                - `rate_limit_error` - too many requests; honor `Retry-After`.
                - `idempotency_error` - the idempotency layer was unreachable; retry.
                - `api_error` - an unexpected server-side or dependency error.
              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,
                INSUFFICIENT_SCOPE, IDEMPOTENCY_KEY_REUSED. Platform-wide codes any operation may
                return: API_V2_DISABLED (503, kill switch off), AUTH_UNAVAILABLE and
                LOOKUP_UNAVAILABLE (503, fail-closed on a dependency error), and, where an
                operation reaches a provisioning path outside your entitlements,
                PROVISIONING_NOT_IMPLEMENTED and
                VERIFICATION_SESSION_NOT_IMPLEMENTED (501).
            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,
                param}`). The router's 404 catch-all instead returns a
                diagnostic object variant:
                `{"diag": {"method": "...", "pathParts": ["..."]}}`.
              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
    Hex32:
      type: string
      pattern: ^[0-9a-f]{64}$
      description: 32 bytes, lowercase hex.
    VaultRootRequest:
      type: object
      required:
        - administrators
        - threshold
      properties:
        administrators:
          type: array
          minItems: 1
          maxItems: 64
          items:
            $ref: "#/components/schemas/Hex32"
          description: Ed25519 public keys of your administrators.
        threshold:
          type: integer
          minimum: 1
          maximum: 64
          description: How many administrators must sign. Never more than the number of administrators.
    VaultRoot:
      type: object
      properties:
        id:
          type: string
          description: "`ozav_vrt_...`"
        object:
          type: string
          enum:
            - vault_root
        administrators:
          type: array
          items:
            $ref: "#/components/schemas/Hex32"
        threshold:
          type: integer
        commitment:
          $ref: "#/components/schemas/Hex32"
        created_at:
          type: string
          format: date-time
    VaultAsset:
      type: object
      required:
        - family
        - id
      properties:
        family:
          type: string
          enum:
            - evm
            - solana
            - stellar
            - tron
        id:
          type: string
          description: "Asset id in hex: token contract (evm, tron: 20 bytes), mint (solana: 32 bytes)."
    VaultGrantRequest:
      type: object
      required:
        - approvers
        - threshold
        - operator_cap
        - cap_asset
        - valid_until
        - signatures
      properties:
        approvers:
          type: array
          minItems: 1
          maxItems: 32
          items:
            type: object
            required:
              - type
              - public
              - role
            properties:
              type:
                type: integer
                enum:
                  - 1
                  - 2
                  - 3
                description: 1 passkey, 2 Ed25519, 3 P-256.
              public:
                type: string
                description: "Hex: 32 bytes for Ed25519, 65 (uncompressed) for P-256 and passkey."
              role:
                type: integer
                enum:
                  - 1
                  - 2
                  - 3
                description: 1 operator (limited by `operator_cap`), 2 approver, 3 administrator.
              raw_key_material:
                type: boolean
                description: Only an administrator with a raw key may move key material.
              rp_id:
                type: string
                description: "Passkey only: your own domain."
              origins:
                type: array
                items:
                  type: string
                description: "Passkey only: https pages of the `rp_id` domain, up to 8."
        threshold:
          type: integer
          minimum: 1
        operator_cap:
          type: string
          description: Positive integer in decimal text, up to 128 bits, in the smallest unit of `cap_asset`.
        cap_asset:
          type: object
          required:
            - family
            - network
            - id
          properties:
            family:
              type: string
              enum:
                - evm
                - solana
                - stellar
                - tron
            network:
              type: integer
              description: EVM chain id; 0 on the other families.
            id:
              type: string
              description: Asset id in hex.
        valid_until:
          type: string
          format: date-time
          description: ISO-8601 UTC ending in Z.
        signatures:
          type: array
          description: Administrator signatures over the grant message.
          items:
            type: object
            required:
              - public
              - signature
            properties:
              public:
                $ref: "#/components/schemas/Hex32"
              signature:
                type: string
                description: 64 bytes, hex.
    VaultGrant:
      type: object
      properties:
        id:
          type: string
          description: "`ozav_vgr_...`"
        object:
          type: string
          enum:
            - vault_grant
        root_id:
          type: string
        commitment:
          $ref: "#/components/schemas/Hex32"
        valid_until:
          type: string
          format: date-time
        signatures:
          type: integer
          description: Valid administrator signatures.
        approvers:
          type: integer
        revoked_at:
          type:
            - string
            - "null"
          format: date-time
          description: When the grant was revoked; null while it is valid.
        created_at:
          type: string
          format: date-time
    VaultRequest:
      type: object
      required:
        - root_id
        - rules
        - share_2_destination
        - share_3_destination
        - root_authorization
      properties:
        root_id:
          type: string
        rules:
          type: object
          required:
            - destinations
          properties:
            destinations:
              type: array
              minItems: 1
              maxItems: 64
              items:
                type: object
                required:
                  - family
                  - address
                properties:
                  family:
                    type: string
                    enum:
                      - evm
                      - solana
                      - stellar
                      - tron
                  address:
                    type: string
                    description: "Hex: 20 bytes on evm and tron, 32 on solana and stellar."
            per_send_cap:
              type: string
              description: Positive decimal text. Goes with `cap_asset`.
            cap_asset:
              $ref: "#/components/schemas/VaultAsset"
            exportable:
              type: boolean
              description: Allow a recovery kit to be exported.
        share_2_destination:
          type: object
          required:
            - spki
          properties:
            spki:
              type: string
              description: DER public key (hex) that receives key share 2.
        share_3_destination:
          type: object
          required:
            - spki
          properties:
            spki:
              type: string
              description: DER public key (hex) that receives key share 3. Different from share 2.
        root_authorization:
          type: object
          required:
            - valid_until
            - signatures
          properties:
            valid_until:
              type: string
              format: date-time
            signatures:
              type: array
              items:
                type: object
                required:
                  - public
                  - signature
                properties:
                  public:
                    $ref: "#/components/schemas/Hex32"
                  signature:
                    type: string
    Vault:
      type: object
      properties:
        id:
          type: string
          description: "`ozav_vlt_...`"
        object:
          type: string
          enum:
            - vault
        root_id:
          type: string
        state:
          type: string
          enum:
            - pending
            - active
            - failed
        lineage:
          type: string
          description: Active only. Identifies the vault in the enclave (16 bytes, hex).
        addresses:
          type: array
          items:
            type: object
          description: Active only.
        receipt:
          type: object
          description: Active only. The enclave's signed answer and the sealed key shares for your destinations. Verify before depositing.
        activated_at:
          type: string
          format: date-time
        failure_code:
          type: string
          description: Failed only.
        created_at:
          type: string
          format: date-time
    VaultDestinationGrantRequest:
      type: object
      required:
        - destinations
        - valid_until
        - signatures
      properties:
        destinations:
          type: array
          minItems: 1
          maxItems: 64
          items:
            type: object
            required:
              - family
              - address
            properties:
              family:
                type: string
                enum:
                  - evm
                  - solana
                  - stellar
                  - tron
              address:
                type: string
                description: "Hex: 20 bytes on evm and tron, 32 on solana and stellar."
        valid_until:
          type: string
          format: date-time
          description: ISO-8601 UTC ending in Z. The grant stops permitting these destinations then.
        signatures:
          type: array
          description: Administrator signatures over the destination grant message (vault, sealed rules, destinations, expiry).
          items:
            type: object
            required:
              - public
              - signature
            properties:
              public:
                $ref: "#/components/schemas/Hex32"
              signature:
                type: string
                description: 64 bytes, hex.
    VaultDestinationGrant:
      type: object
      properties:
        id:
          type: string
          description: "`ozav_vdg_...`"
        object:
          type: string
          enum:
            - vault_destination_grant
        vault_id:
          type: string
        destinations:
          type: array
          items:
            type: object
        message:
          $ref: "#/components/schemas/Hex32"
        valid_until:
          type: string
          format: date-time
        signatures:
          type: integer
          description: Valid administrator signatures.
        created_at:
          type: string
          format: date-time
    VaultAttestation:
      type: object
      properties:
        object:
          type: string
          enum:
            - vault_attestation
        document:
          type: string
          description: The COSE_Sign1 attestation document, lowercase hex. Verify it yourself.
        observed_at:
          type: string
          format: date-time
          description: When OZAV received it. Not proof of freshness.
    VaultTransactionRequest:
      type: object
      required:
        - grant_id
        - family
        - derivation_path
        - raw_transaction
        - intent
        - user_envelope
        - approve_until
      properties:
        grant_id:
          type: string
          description: "`ozav_vgr_...` of a grant of the vault's root."
        destination_grant_id:
          type: string
          description: Optional. `ozav_vdg_...` of a destination grant of this vault.
        family:
          type: string
          enum:
            - evm
            - solana
            - tron
        derivation_path:
          type: string
          example: m/44'/60'/0'/0/0
        raw_transaction:
          type: string
          description: The exact bytes the vault will sign, hex, up to 64 KiB.
        intent:
          description: What the bytes do. The enclave checks it against the bytes and the vault rules.
          oneOf:
            - type: object
              title: evm
              required:
                - chain_id
                - token
                - destination
                - amount
                - fee_cap_wei
              properties:
                chain_id:
                  type: integer
                  minimum: 1
                token:
                  type: string
                  description: 20 bytes, hex.
                destination:
                  type: string
                  description: 20 bytes, hex.
                amount:
                  type: string
                  description: Positive integer in decimal text, smallest unit.
                fee_cap_wei:
                  type: string
            - type: object
              title: tron
              required:
                - token
                - destination
                - amount
                - fee_cap_sun
              properties:
                token:
                  type: string
                destination:
                  type: string
                amount:
                  type: string
                fee_cap_sun:
                  type: string
            - type: object
              title: solana
              required:
                - mint
                - destination_owner
                - amount
              properties:
                mint:
                  type: string
                  description: 32 bytes, hex.
                destination_owner:
                  type: string
                  description: 32 bytes, hex.
                amount:
                  type: string
        user_envelope:
          type: object
          description: Your key share 3, sealed to your KMS key. Passed to the enclave, never opened by OZAV.
        approve_until:
          type: string
          format: date-time
          description: ISO-8601 UTC ending in Z, up to 7 days ahead.
    VaultTransaction:
      type: object
      properties:
        id:
          type: string
          description: "`ozav_vtx_...`"
        object:
          type: string
          enum:
            - vault_transaction
        vault_id:
          type: string
        status:
          type: string
          enum:
            - awaiting_approvals
            - signed
            - rejected
            - submitted
            - expired
        grant_id:
          type: string
        challenge:
          $ref: "#/components/schemas/Hex32"
        approve_until:
          type: string
          format: date-time
        request:
          type: object
          description: The transaction as stored, with the intent normalized.
        approvals:
          type: object
          properties:
            required:
              type: integer
            received:
              type: integer
        signing:
          type: boolean
          description: True while the quorum is met and the enclave has not answered.
        signed_transaction:
          type: string
          description: Signed status only. Hex. Broadcast it yourself.
        failure_code:
          type: string
          description: Rejected status only.
        created_at:
          type: string
          format: date-time
        completed_at:
          type:
            - string
            - "null"
          format: date-time
    VaultApprovalRequest:
      type: object
      required:
        - type
        - public
        - signature
        - valid_until
      properties:
        type:
          type: string
          enum:
            - ed25519
            - p256
            - passkey
        public:
          type: string
          description: "Hex: 32 bytes for ed25519, 65 for p256 and passkey."
        signature:
          type: string
          description: "Hex. Ed25519: 64 bytes. P-256 and passkey: DER."
        valid_until:
          type: string
          format: date-time
          description: The transaction's `approve_until`.
        auth_data:
          type: string
          description: Passkey only, hex.
        client_data_json:
          type: string
          description: Passkey only, hex.
        authenticator_counter:
          type: integer
          description: Passkey only.
