# Schemas — Swaps API

Reusable request and response schemas from the bundled OpenAPI contract. The YAML blocks preserve the published schema constraints.

<a id="money"></a>
## Money

```yaml
type: object
description: >-
  A money value. `amount` is a minor-unit integer as a string; `decimals` states the scale so a
  client never re-derives it (USDC is 6). Floats never appear on a money path.
required:
  - amount
  - currency
  - decimals
properties:
  amount:
    type: string
    pattern: ^-?[0-9]+$
    example: '100000000'
    description: >-
      Minor-unit integer string. A negative value appears only on signed ledger rows; every creating
      request uses `MoneyPositive` and the router rejects zero or negative amounts.
  currency:
    type: string
    description: ISO 4217 code or a stablecoin symbol (`USDC`, `USDC.e`, `pathUSD`).
    example: USDC
  decimals:
    type: integer
    minimum: 0
    maximum: 18
    example: 6
additionalProperties: false
```

<a id="moneypositive"></a>
## MoneyPositive

```yaml
allOf:
  - $ref: '#/components/schemas/Money'
  - type: object
    properties:
      amount:
        type: string
        pattern: ^[1-9][0-9]*$
description: >-
  A `Money` value that must be strictly positive — used on every request field that creates or moves
  value.
```

<a id="moneynonnegative"></a>
## MoneyNonNegative

```yaml
allOf:
  - $ref: '#/components/schemas/Money'
  - type: object
    properties:
      amount:
        type: string
        pattern: ^[0-9]+$
description: >-
  A `Money` value that must be zero or positive (never negative) — for a real figure that may
  legitimately be zero, e.g. a free/included line item's unit price. `Money` itself permits a
  leading `-` for signed ledger rows; this variant never does.
```

<a id="depositinstructions"></a>
## DepositInstructions

```yaml
type: object
description: >
  Where to send crypto, how much, on which chain. One shape for every product (payer sessions,

  self-custody sells, wallet deposits, off-ramp funding). `address` is case-preserved verbatim.

  For a bridged (non-Tempo) leg the amount is a quote: `amount_is_estimate` is true,
  `quote_expires_at`

  is set, `bridge_fee` states the provider haircut and `gross_up: true` means the payer's send
  amount

  already absorbs it so the destination receives the exact `amount_out` (D-60 default).
required:
  - address
  - chain
  - amount
properties:
  address:
    type: string
  chain:
    type: string
    enum:
      - tempo
      - base
      - ethereum
      - polygon
      - arbitrum
      - optimism
      - solana
      - avalanche
      - celo
      - stellar
      - tron
    x-swaps-open-enum: true
    description: >
      The network to send on, in Swaps' own network ids. On a Bridge sell it is every network a sell
      can be

      made on (`avalanche`, `celo`, `stellar` and `tron` added 2026-10-15); treat an unrecognized
      value as a

      network you cannot send on.
  asset:
    type: string
    description: Asset symbol to send on `chain` (e.g. `USDC`, `USDC.e`).
  chain_id:
    type: integer
    description: |
      Numeric chain id, when `chain` alone does not pin a single concrete network — e.g.
      `tempo` covers both Moderato (testnet, 42431) and Mainnet (4217), and `asset`'s
      contract address is not guaranteed distinct across them (a signer must not infer the
      network from the symbol alone). Absent when the rail has no such ambiguity.
  token_address:
    type: string
    description: |
      On-chain contract address `asset` resolves to on `chain`, verbatim case, when this
      attempt has one frozen (e.g. the TIP-20 token address a Tempo Pay splitter accepts).
      The authoritative send target for an in-app signer — never re-derive it from `asset`
      against a local registry, which can drift from what the attempt actually froze.
      Absent when the rail's `asset` has no single on-chain contract identity.
  amount:
    $ref: '#/components/schemas/Money'
  amount_out:
    $ref: '#/components/schemas/Money'
  amount_is_estimate:
    type: boolean
    default: false
    description: >
      `true` when `amount` is a quote that may move (a bridged leg). `false` when it does not move:
      on a

      Bridge sell `amount` is the amount of `asset` the order was quoted for — send exactly it on
      `chain`, in

      one transfer. The order sells the first deposit that arrives, whatever its amount
      (2026-10-28), and a

      later deposit is not added to it; a deposit below the sell's minimum may not complete the
      sale. The

      payout follows what arrives: Bridge takes the 1 % fee from it, rounded up to the cent, and, on
      a

      currency conversion, converts at its own rate at that moment, so the payout can differ from
      the quote.
  quote_expires_at:
    type: string
    format: date-time
  bridge_fee:
    $ref: '#/components/schemas/Money'
  gross_up:
    type: boolean
  memo:
    type: string
    description: >
      On-chain memo / reference when the rail requires one; never on Tempo pay-ins. On a Bridge sell
      it is

      Bridge's `blockchain_memo` (Stellar, Tron) and MUST be sent with the deposit; an address whose
      memo

      cannot be published is never published, and on Stellar and Tron (memo-based networks) a sell's
      address is

      published only together with its memo.
  expires_at:
    type: string
    format: date-time
    description: >
      Send before this. On a Bridge sell: 24 hours after the order was created; after it the order
      is

      cancelled and its deposit address no longer belongs to it. Absent when the order has no
      deadline.
  provider:
    type: string
    enum:
      - relay
    x-swaps-open-enum: true
    description: |
      The third party that moves the funds on this leg, when one does (`relay` on a
      `crypto_relay` payment). Absent when the payer pays the destination directly.
unevaluatedProperties: false
```

<a id="bankdepositinstructions"></a>
## BankDepositInstructions

```yaml
type: object
description: >
  Bank details a holder funds from (virtual accounts, payroll funding, and — K3b — a payment-links
  payer

  session's bank-rail attempt). Masked on read where the canon requires it. Every field is an
  allowlist read

  off the provider's own payload for the chosen rail: a field this schema doesn't name is dropped,
  and a field

  it does name but the provider omitted for that rail is left unset — never guessed, never a
  fabricated value.
properties:
  currency:
    type: string
  rail:
    type: string
  bank_name:
    type: string
  beneficiary_name:
    type: string
  holder_kind:
    type:
      - string
      - 'null'
    enum:
      - own_name
      - provider_name
      - developer_name
      - null
    description: >-
      D-111's classification. Binding on every `BankDepositInstructions` consumer, present and
      future: classify the beneficiary name the provider actually returned on THIS record — never
      derived from the currency or the rail, and never invented when the provider returned no name
      at all (`null`, explicit, never merely omitted). `own_name`: an ordinary beneficiary name (the
      holder's own, or — for a payment-links payer session — whatever name the provider actually
      returned). `provider_name`: the beneficiary is the provider's own settlement entity (e.g.
      Bridge's "Bridge Building S.A." prefix — KNOWN FALSE POSITIVE: a genuine beneficiary whose own
      name happens to start with "bridge", e.g. "Bridgewater Ltd", also reads as `provider_name`;
      tightening the match is a follow-up, not fixed here). `developer_name`: a memo-matched,
      flexible-amount collection account issued in the developer's own name ("Swaps") —
      payment-links bank-rail pay-ins are EXPECTED to route through exactly this shape, though that
      expectation has not yet been confirmed against a live provider response for that specific
      endpoint.
  account_number:
    type: string
  routing_number:
    type: string
  sort_code:
    type: string
    description: UK Faster Payments — 6-digit domestic sort code, alongside `account_number`.
  clabe:
    type: string
    description: >-
      Mexico SPEI — the 18-digit interbank account key (replaces `account_number`/`routing_number`
      for this rail).
  pix_key:
    type: string
    description: Brazil PIX — the key the payer sends to; often the only field this rail populates.
  br_code:
    type: string
    description: >-
      Brazil PIX — the Pix pay code («Pix Copia e Cola», BR Code) a BRL virtual account returns in
      place of a `pix_key` (payroll funding): the payer pastes it into their bank app. Set only when
      the provider returned it; never derived from another field.
  bre_b_key:
    type: string
    description: >-
      Colombia Bre-B (`rail: bre_b`) — the key the payer sends to, with `reference`. A Bre-B
      instruction without it carries no way to pay and is not published at all.
  bank_address:
    type: string
  iban:
    type: string
  bic:
    type: string
  reference:
    type: string
    description: >-
      The transfer memo/reference text. When `matching` is `reference`, this is what the provider
      matches the deposit BY — the payer must type it into their bank transfer exactly, or the
      deposit lands unmatched on a pooled account. When `matching` is `account_number`, this is
      recommended (it speeds up reconciliation) but never required — the deposit is already matched
      by the account it landed on. Absent `matching` altogether (a record from before this field
      existed), treat `reference` as required, matching this schema's pre-existing behavior.
  matching:
    type: string
    enum:
      - reference
      - account_number
    description: >-
      How the provider attributes THIS deposit to the payer, independent of `holder_kind`.
      `reference`: a memo-matched transfer on a pooled account — the provider matches by the
      `reference` text alone, which the payer must type exactly, or the deposit is unmatched.
      `account_number`: the account itself (`account_number`/`iban`/`clabe`/`sort_code`, whichever
      this rail populates) is unique to this customer, so the provider matches by account rather
      than memo; `reference`, when present, is recommended for faster reconciliation but is never
      required to complete the payment. Omitted when the provider payload carries no field this
      schema could use to decide (e.g. `pix`, where a Pix key alone is already a complete,
      self-matching destination).
unevaluatedProperties: false
```

<a id="errortype"></a>
## ErrorType

```yaml
type: string
description: The recovery class an agent must distinguish (API-CANON §5).
enum:
  - invalid_request
  - authentication_error
  - permission_error
  - not_found
  - conflict
  - idempotency_error
  - rate_limit_error
  - capability_unavailable
  - temporarily_unavailable
  - quota_exhausted
  - provider_error
  - api_error
```

<a id="error"></a>
## Error

```yaml
type: object
required:
  - error
properties:
  error:
    type: object
    required:
      - type
      - code
      - message
      - request_id
    properties:
      type:
        $ref: '#/components/schemas/ErrorType'
      code:
        type: string
        description: >-
          Stable machine code, additive-only; maps onto the browser vocabulary in
          `src/lib/errors.ts`.
        example: individual_amount_above_limit
      message:
        type: string
      doc_url:
        type: string
        format: uri
      request_id:
        type: string
      param:
        type: string
        description: The offending request field, when one can be named.
      details:
        type: object
        additionalProperties: true
        description: >-
          Machine-readable context for the codes that publish some — the values an agent would
          otherwise have to parse out of `message`. Read it per `code`, never generically: a code
          documents its own keys, and a code that publishes none omits the object entirely. The kill
          switches name themselves: a `temporarily_unavailable` (503, `Retry-After: 60`) caused by
          the global `api_v1` switch carries `reason: "api_disabled"`, one caused by a resource's
          own switch (`api_v1.<resource>`) carries `reason: "resource_disabled"`, and one caused by
          a feature switch inside a resource that is on carries `reason: "feature_disabled"` (today
          the support objects of `support_cases.create` and `support_cases.related_objects.list`,
          and the support files of the `support_cases.attachments.*` operations and of a
          `support_cases.messages.create` or `support_cases.create` that names files). They are a
          deliberate off state, not an outage, so a caller (the dashboard's "is this resource on?"
          probe, for one) can tell them from one. Every other 503 carries none of these values, a
          refusal because the switch could not be read included.
          `allowed_rails_invalid_for_currency` carries `currency`, `allowed_rails` and
          `offerable_rails`. `rail_not_allowed` carries `reason` when the refusal has a more
          specific cause than the code alone — today only `merchant_fiat_payin_pending` (a p2p
          merchant's own Bridge fiat pay-in is not active, so no fiat rail can be offered to their
          payer); absent for every other `rail_not_allowed` refusal. Same underlying condition
          (C4-D23) as `CapabilityCorridor.blocked_reason: 'payin_fiat_pending'` (`platform.yaml`) at
          `/v1/capabilities` — this is the payer's execution-time refusal, that is the merchant's
          own capability resource; the two scopes are never merged into one code.
          `temporarily_unavailable` from `orders.create` (`POST /v1/orders`) carries `order_id` when
          a Bridge-native buy's local order already exists before the failure that produced this
          response (BSR-3, 2026-09-18) — but not always: `BRIDGE_TRANSFER_RECONCILE_FAILED` proves a
          row exists (a `23505` unique violation on `provider_idempotency_key`) yet can still fail
          to read its id back, so it ships `quote_id` instead (BSR-13, 2026-09-20) — as does the
          genuinely no-order-exists-yet tail, which used to ship no `details` at all alongside a
          `Retry-After` that could never be honored (see the `503` response's own description below
          for why). Treat `order_id`'s presence as "an order exists, reconcile against it" and
          `quote_id`'s presence (with no `order_id`) as "no order to name yet, but here is what to
          request a new quote against" — never absent together on this operation's
          `temporarily_unavailable` `503`s once the pre-Bridge-execute checks have passed. Do not
          infer retry safety from `Retry-After`'s presence or absence on this operation — several
          other `temporarily_unavailable` branches from the SAME operation (an unreadable quote row,
          a stale gross-minimum snapshot, a route-claim sealing failure) are genuinely
          side-effect-free (nothing was created) and still ship no `Retry-After` header.

          BSR-3 fixer round 6 (independent review, 2026-09-18), correcting round 5's revision of
          this text, which wrongly conflated `error.type` (always `temporarily_unavailable` for
          every `503` this operation returns) with `error.code`, which DOES vary: the three
          pre-Bridge-execute checks below fail closed on their own more specific codes, not the
          generic one. There is no `retryable` field anywhere in the `/v1` error envelope, and this
          operation's internal Bridge-side classification is deliberately kept server-side
          (`.claude/rules/money.md`). Check `code` FIRST; for the generic `temporarily_unavailable`
          codes, `message`'s exact text is stable and matched here by substring — but treat
          `error.details.order_id`'s presence/absence as the primary "does an order exist" signal,
          not `message` alone: several strings below are shared by branches that differ only in
          whether `details` is set, and by very different Idempotency-Key dispositions
          (`supabase/functions/api-v1/router.ts`'s `classifyFor5xx`):
            - `code: "destination_check_unavailable"` — "Could not verify this destination right now" (a
              destination-provenance/velocity lookup failed) or "Address screening is temporarily unavailable"
              (the screening provider is down under the fail-closed policy) — both fire before any Bridge
              call; `Retry-After: 5`, genuinely side-effect-free (nothing created), a SAME-key replay is safe.
              Also "We couldn't check this address right now. Try again shortly." — the inbound rule's wallet
              lookup failed for a buy into a wallet address, before any order row; side-effect-free, with
              `error.details.quote_id` and no `Retry-After`.
            - `code: "customer_status_unavailable"` — "Could not verify this customer right now" — the stored
              Bridge customer-status snapshot is stale/degraded and this operation refuses to trust it for a
              money decision; before any Bridge call; `Retry-After: 5`, side-effect-free, SAME-key replay safe.
              (BSR-13 fixer round 3 note, 2026-09-21 — these two, and the offer-freshness check right after
              them, all run BEFORE the gross-minimum/route-claim-sealing/daily-cap group above dropped
              `Retry-After` from; keeping it here is a STRUCTURAL argument — earlier in the request means
              more of the offer's ~27-29s TTL typically remains — not a measured one: no harness proved a 5s
              wait always lands before that TTL elapses for these two specifically. Tracked under BSR-14
              alongside the offer-TTL fix itself; treat `Retry-After` here as "usually still enough runway",
              not a guarantee.)
            - "Could not create this order — request a new quote and retry under a new Idempotency-Key" —
              `code: "temporarily_unavailable"`; emitted ONLY by
              `mapBridgeExecuteFailure`'s final, unclassified tail (no known Bridge failure code, no
              `transactionId`): no order exists yet. `Retry-After` is NEVER present here (BSR-13,
              2026-09-20 — was `Retry-After: 5`; LIVE evidence, dev, 3/3, proved a SAME-key replay after
              that advertised wait a dead end regardless: `409 idempotency_failed`, since this branch
              ships no `sideEffectFree` marker so `classifyFor5xx` keeps the Idempotency-Key slot
              reserved). The message now names BOTH halves of the actual recovery explicitly (BSR-13
              fixer round 1, 2026-09-21) — a genuinely fresh `POST /v1/quotes` under a NEW
              Idempotency-Key, matching `docs/api/RESOURCE-MODEL.md`'s kept-reservation rule — rather
              than leaving the NEW-key half only in this prose: a caller who re-POSTs the SAME
              `quote_id` under a new key instead re-prices a Bridge offer already partway through its own
              ~27-29s TTL by the time `handleBridgeExecuteTransfer` is reached, and usually gets
              `409 requote_required`/`404` on the offer/quote itself — a genuinely NEW quote does not have
              that problem, since its own offer TTL has not started counting down yet.
              `error.details.quote_id` is set instead of `Retry-After`, so the caller has a handle to
              correlate this refusal with while getting that fresh quote.
            - "Could not create this order right now — nothing was created; retry with the same
              Idempotency-Key" — the gross-minimum and route-claim-sealing failures, BEFORE
              `handleBridgeExecuteTransfer` is ever called: genuinely side-effect-free (the router releases the
              caller's Idempotency-Key reservation), no `Retry-After` ships, and a SAME-key replay is the
              correct retry — a NEW key would only re-quote and mint a new offer for no reason.
              `error.details.quote_id` IS set (BSR-13 fixer round 3, 2026-09-21 — these two shipped
              `details: {}` through round 2 even though `quoteRowId` was already in scope at both catches;
              closed to actually satisfy this operation's "never absent together" promise above). Neither
              advertises a wait, so neither makes the BSR-13 broken-promise claim above (`Retry-After` that
              cannot succeed) — but "before `handleBridgeExecuteTransfer`" does NOT mean "before the offer
              TTL has run": both checks fire in the same last-few-steps window as the daily-value-cap check
              below, seconds (not minutes) before Bridge execute, so the SAME-key replay answers whatever the
              underlying quote/offer's CURRENT state supports — it can still surface `409 requote_required`
              on THAT retry if the offer has since expired. Extending the offer TTL, or letting this
              operation re-price in place, is tracked separately (BSR-14); this bullet only guarantees the
              retry's Idempotency-Key semantics, not that the retry succeeds.
            - "Could not verify this account right now — request a new quote and retry" —
              `assertWithinBridgeNativeDailyCap`'s two structural-failure branches (unusable cap/amount
              inputs; the daily-cap RPC itself missing or erroring), taken at the LAST point before
              `handleBridgeExecuteTransfer` — the same position in the flow as the fixed `!rowMayExist` tail
              above. `Retry-After` REMOVED here too (BSR-13 fixer round 2, 2026-09-21 — was `Retry-After: 5`,
              flagged as the same broken promise the tail round 1 already fixed: by the time this check runs
              the winning offer is already deep into its own ~27-29s TTL, so advertising a further wait
              routinely lands the replay on an expired offer). The unusable-inputs branch (fires BEFORE the
              RPC call) is genuinely side-effect-free — nothing was reserved. The RPC branch (fires AFTER
              `reserve_bridge_native_daily_value` was called, on a transport error or an unreadable response
              row) is NOT provably side-effect-free: that RPC inserts its reservation row before returning
              when it allows the request, so a failure observed after a committed call leaves a live
              reservation counting against the caller for up to `BRIDGE_NATIVE_RESERVATION_TTL_SECONDS`
              (BSR-13 fixer round 3, 2026-09-21 — corrected from "nothing was reserved" for this branch,
              which the RPC's own commit-then-fail path can falsify). Both branches ARE safe on the
              Idempotency-Key axis — no order was created either way, so a SAME-key replay is correct — which
              is why the message says "retry", not "get a new Idempotency-Key"; the caller's own reservation,
              if one was taken, self-expires on that same TTL rather than needing an explicit release. The
              message names the real next step (a fresh quote) instead of implying an immediate same-request
              retry will succeed, and `error.details.quote_id` gives a correlation handle for it.
            - "A transfer attempt for this request may already exist — check GET /v1/orders before retrying" —
              emitted by two DIFFERENT unclassifiable-outcome branches, both now setting
              `error.details.quote_id` (no `order_id` to name, but the quote is a real reconcile handle):
              `mapBridgeExecuteFailure`'s tail when a Bridge failure code proves (or cannot rule out) a row
              may exist but no `transactionId` came back (`BRIDGE_TRANSFER_RECONCILE_FAILED` /
              `_INTENT_UNREADABLE` / `_RECOVERY_LOOKUP_FAILED`, BSR-13 2026-09-20); and a throw from
              `handleBridgeExecuteTransfer` itself caught before that function's OWN `try`/`catch` even
              starts (its prologue — route-claim open, ownership check, idempotency-key derivation), which
              can say nothing about whether Step 4's intent insert or Step 5's provider call ran — this one
              shipped `details: {}` until BSR-13 fixer round 1 (2026-09-21) closed the same gap, since it
              fires strictly after the pre-Bridge-execute checks and `quoteRowId` is in scope at that catch
              too. Neither carries `Retry-After`, and neither is side-effect-free (a same-key replay
              answers `409 idempotency_failed`) — poll `GET /v1/orders` (filter by the request's own
              attributes) before retrying under any key in both cases.
            - "Order was created but its id could not be determined — check GET /v1/orders shortly" —
              `handleBridgeExecuteTransfer` answered success but the response carried no `transactionId`; NO
              `order_id` in `details` (there is nothing to name), but `error.details.quote_id` IS set
              (BSR-13 fixer round 1, 2026-09-21 — this branch is reached only once Bridge execute has
              already succeeded, so a transactions row provably exists); poll `GET /v1/orders` (filter by
              the request's own attributes), never a specific `{id}`.
            - "Order was created but could not be read back — check GET /v1/orders shortly" — the id IS known
              (`transactionId` came back) but this service's own read-back of that row failed; `order_id` IS
              present in `details`; poll `GET /v1/orders/{id}`.
            - "Order was created but could not be confirmed — check GET /v1/orders shortly" — `order_id` is
              present; the row is expected to reach a terminal state on its own; poll `GET /v1/orders/{id}`.
            - "A compliance attestation for this order is incomplete (see order_id) — do not fund it yet. Check
              GET /v1/orders for the latest status." — `order_id` is present; the Bridge transfer already
              committed and is not rejected; poll, but do not fund the order until it resolves.
            - "An order record exists for this attempt (see order_id); it may not be payable. Do not retry —
              contact support if it stays pending." — `order_id` is present; this is a DEFINITIVE Bridge
              rejection that will never reach a terminal state on its own (BSR-20,
              github.com/swapsapp/swaps/issues/3312) — never retry, and do not poll expecting resolution; a
              same-key retry answers `409` and a re-quoted retry mints a duplicate provider transfer, which is
              exactly what this message exists to prevent.
          `conflict` from the same operation's `409` carries `order_id` for the identical reason (a
          terminal, already-existing order that cannot be retried as-is) for three of its causes;
          the OTHER `409 conflict` this operation returns, `requote_required`, never sets `details`
          even though a row can already be visible on `GET /v1/orders` for that case too (BSR-18,
          not yet closed) — do not assume every `409 conflict` from this operation carries
          `order_id`. `permission_error` from the same operation's `403` also carries `order_id`
          when Bridge rejects the selected fiat payment rail AFTER the local order already exists —
          request a new quote and a different payment rail rather than retrying this one; a
          pre-order-creation rail rejection sets no `details`.

          `payout_preflight_rejected` from `payroll_runs.execute` (`POST
          /v1/payroll_runs/{id}/execute`, a run paid from a converted pool) carries
          `preflight_reason` — `provider_refused` (the provider refused the exact body),
          `route_invalid` (its dry run answered `state: invalid`) or `dry_run_returned_transfer`
          (its dry run answered with a transfer object: the account is on operator hold, `403
          payroll_operator_hold`, until Swaps has reconciled with the provider) — `item_id`, and an
          allowlisted classification of the provider's answer: `provider_status`,
          `provider_error_class`, `invalid_fields` (the request fields the provider named, or
          `other`), `notes_count`, `notes_classes`. Never the provider's text. Nothing was queued;
          the run stays funded.

          `attachment_rejected` from `support_cases.attachments.complete` (`422`) carries `reason`,
          one of the ten values of `SupportCaseAttachment.rejection.reason`;
          `attachment_limit_reached` (`409`) carries `limit`: `message` (5 files on one message),
          `case` (25 on one request) or `open_uploads` (10 uploads not yet sent). Neither carries
          the file's name or content.
```

<a id="listmeta"></a>
## ListMeta

```yaml
type: object
required:
  - has_more
  - next_cursor
properties:
  has_more:
    type: boolean
  next_cursor:
    type:
      - string
      - 'null'
    description: >-
      A1-2: required (still nullable) — every producer already emits `null` rather than omitting the
      field when there is no next page, so this only removes a third, unused wire state (absent) a
      strict client had to handle for nothing.
```

<a id="objectbase"></a>
## ObjectBase

```yaml
type: object
description: Fields present on every first-class object.
required:
  - id
  - object
  - livemode
  - created_at
properties:
  id:
    type: string
    description: Prefixed, opaque. The prefix is part of the contract; the suffix is never parsed.
    example: pl_01J9ZK3Q8M2F5A7C9E1G3H5J7K
  object:
    type: string
    example: payment_link
  livemode:
    type: boolean
  created_at:
    type: string
    format: date-time
  updated_at:
    type: string
    format: date-time
```

<a id="account"></a>
## Account

```yaml
type: object
description: |
  The caller's currently selected account (D-109). One live account per login is supported
  (`POST /accounts` refuses a second). `GET /account`
  always reads "the selected one" — the account named by `Swaps-Account`, or the caller's
  `default_account_id` — never a lookup by id; the id-prefix convention (RESOURCE-MODEL §0.2)
  still applies so the same shape is reachable from `GET /accounts`.
required:
  - id
  - display_name
  - email
  - member_since
  - customer_type
  - country
  - language
  - access_state
  - livemode
  - market
  - support_contact
properties:
  id:
    type: string
    pattern: ^acct_
    description: >-
      The account's id. Never looked up directly by a business key or a session — reached only as
      "the selected account" (`GET /account`) or via `GET /accounts` (mine).
  display_name:
    type: string
  email:
    type: string
    format: email
  member_since:
    type: string
    format: date-time
  customer_type:
    type: string
    description: >-
      `individual` or `business`. Follows the account owner's customer (`customers.get`, `GET
      /customers/{id}`): it is the type Swaps has recorded for that customer, not a live read from
      our verification partner. Swaps sets it whenever a customer of the owner is recorded or
      changes type, in either direction (`business` when any owner has a business customer). Until
      the owner has a customer, the type the account was opened with. Not writable through `PATCH
      /account`, and `customers.create` does not write it. A test-mode account keeps the type it was
      created with. Not itself a KYB projection (RESOURCE-MODEL §2.4).
  country:
    type:
      - string
      - 'null'
    description: >-
      ISO 3166-1 alpha-2. `null` until a source column exists (проверить — public.users has none
      today).
  language:
    type:
      - string
      - 'null'
    description: >-
      `null` until a source column exists (проверить — public.users has none today). Always `null`
      for an `sk_test_` key (test mode).
  access_state:
    type: string
    description: Reason for a restricted/suspended/blocked state stays internal — never on the wire.
    enum:
      - active
      - restricted
      - suspended
      - blocked
  notification_preferences:
    type: object
    description: >-
      For an `sk_test_` key (test mode) always `{marketing: false, product_tips: true}`, not the
      owner's values; a test-mode dashboard session sees its own.
    required:
      - marketing
      - product_tips
    properties:
      marketing:
        type: boolean
        description: Opt-in. Backs `user_preferences.marketing_consent`.
      product_tips:
        type: boolean
        description: >-
          Opt-out — enabled unless explicitly set false. Backs
          `user_preferences.email_notifications`.
  dashboard_preferences:
    type: object
    description: >-
      Cross-device UI preferences persisted server-side (R18 TA-G9) — not `UI-private`, since prod
      already syncs them. For an `sk_test_` key (test mode) always `{}`, not the owner's values; a
      test-mode dashboard session sees its own.
    properties:
      money_tools_hidden:
        type: array
        items:
          type: string
        description: Money-tools tile ids the holder has hidden from Today.
      readiness_card:
        type: string
        description: Collapse state of the persistent readiness card.
        enum:
          - expanded
          - collapsed
      setup_guide_mode:
        type: string
        enum:
          - expanded
          - minimized
          - launcher
  livemode:
    type: boolean
  support_contact:
    type:
      - object
      - 'null'
    description: >-
      L4-9 — an EXPLICIT opt-in support contact for the payer surfaces
      (`PaymentSession.merchant_contact` on `/pay/<token>`). NEVER derived from `email` above (the
      login e-mail), KYC, Bridge or settlement data — `null` until the holder sets it here. Also
      `null` for an `sk_test_` key: withheld in test mode, not unset.
    properties:
      email:
        type: string
        format: email
        maxLength: 254
      url:
        type: string
        format: uri
        pattern: ^https://
        maxLength: 512
    additionalProperties: false
  market:
    type: object
    description: >-
      BL-25 (D-PF-9a) — the persona's market for the embedded Buy & sell widget, which previously
      had no signal to resolve country/currency from and fell back to the public-site default
      regardless of who was signed in. A read-only projection over the same static country→currency
      registry the public-site widget itself defaults from (`_shared/services/config.ts`'s
      `countries.json`, ISO 4217 local currency per ISO 3166-1 alpha-2 country) — never a
      live/provider-routed pick, so this carries no money path. Not itself writable. Resolved, in
      order of trust, from `country` above (declared once at `POST /accounts`, validated against
      this same registry there), then the holder's Bridge KYC address country (not published as its
      own field), then the public-site no-signal default. Always present — `source` names which case
      applied, and a consumer MUST treat `source: 'default'` as "no persona signal at all" rather
      than seed anything from it (it is the exact value an anonymous, signed-out visitor gets).
    required:
      - country
      - currency
      - source
    properties:
      country:
        type: string
        description: >-
          ISO 3166-1 alpha-2. The resolved market country — see `market`'s own description for the
          three-tier resolution order; NOT simply a mirror of `country` above (that is only the
          first, highest-trust tier).
      currency:
        type: string
        description: >-
          ISO 4217. The market country's own local currency from the registry — not a
          provider/route-aware pick, so a corridor this currency cannot actually settle today is
          still reported honestly; the widget's own existing fallback resolution (already exercised
          for an explicit `?country=` query param) decides what to do about that, same as today.
      source:
        type: string
        enum:
          - declared
          - kyc
          - default
        description: >-
          `declared` when the holder's own `country` (above) drove the pick. `kyc` when there was no
          usable `country` but the holder's Bridge KYC address country resolved to a registry entry
          instead. `default` when NEITHER resolved — no persona signal at all, and a consumer must
          not seed the widget from it (it is the identical value an anonymous, signed-out visitor
          gets).
    additionalProperties: false
  readiness:
    allOf:
      - $ref: '#/components/schemas/AccountReadiness'
    description: >-
      Present only when `?expand=readiness` (or `?expand=readiness,setup_guide`) is requested on
      `GET /account` AND that expansion succeeded (API-PERF-1 M5, fix round 1) — the exact `GET
      /account/readiness` payload, merged in. Absent, never `null`, when not requested; also absent,
      with a matching entry in `expand_errors`, when requested but the expansion itself failed (an
      unresolvable owner or a Bridge outage) — a failed expansion never fails the account read.
  setup_guide:
    allOf:
      - $ref: '#/components/schemas/SetupGuide'
    description: >-
      Present only when `?expand=setup_guide` (or `?expand=readiness,setup_guide`) is requested on
      `GET /account` AND that expansion succeeded (API-PERF-1 M5, fix round 1) — the exact `GET
      /account/setup_guide` payload, merged in. Absent, never `null`, when not requested; also
      absent, with a matching entry in `expand_errors`, when requested but the expansion itself
      failed.
  expand_errors:
    type: array
    description: >-
      Present only when at least one requested `?expand=` field could not be computed (API-PERF-1
      fix round 1). The account read itself, and any OTHER requested expansion that DID succeed, are
      still published — this names exactly which field is missing and why, rather than failing the
      whole response the way the standalone `GET /account/readiness`/`GET /account/setup_guide`
      routes do on the same failure. Absent entirely when `?expand=` is unset, or when every
      requested field succeeded.
    items:
      type: object
      required:
        - field
        - code
      properties:
        field:
          type: string
          enum:
            - readiness
            - setup_guide
        code:
          type: string
          description: >-
            `not_found` (no resolvable account owner), `temporarily_unavailable` (the live Bridge
            customer-status call failed), or `internal_error` (an unexpected failure inside that one
            field's own computation) — the same code vocabulary `error.code` uses elsewhere in this
            API, never a bespoke one.
          enum:
            - not_found
            - temporarily_unavailable
            - internal_error
      additionalProperties: false
  caller:
    type: object
    description: >-
      Who is making this request, on this account: the credential type and, for a session, the
      caller's membership role. Read it before offering an owner/admin-only action (e.g. changing
      `display_name` with `PATCH /account`, API keys, the wallet families) instead of learning it
      from a `403 role_denied`. It comes from the same resolved credential the role gate checks,
      never a second lookup. Present on `GET /account` and `PATCH /account`, and on the one `GET
      /accounts` item this request's credential resolved to (the selected account). Absent, not
      `null`, on any other account (the other `GET /accounts` items, `POST /accounts`): this request
      never resolved a role there — select that account with `Swaps-Account` and read `GET
      /account`. Published in test and live mode alike.
    required:
      - credential
      - role
    properties:
      credential:
        type: string
        x-swaps-open-enum: true
        enum:
          - dashboard_session
          - business_key
        description: >-
          The credential class, in `x-swaps-caller` vocabulary: `dashboard_session` (a signed-in
          dashboard user's session) or `business_key` (an `sk_live_…`/`sk_test_…` key). May gain a
          member within `/v1`; treat an unknown value as opaque.
      role:
        oneOf:
          - $ref: '#/components/schemas/AccountMemberRole'
          - type: 'null'
        description: >-
          A `dashboard_session`'s membership role on this account — the value the role gate reads.
          An owner/admin-only operation passes that gate exactly when this is `owner` or `admin`, an
          owner-only one (`POST /customers`, `POST /customers/{id}/verification_links`) exactly when
          it is `owner`; any other value, including one this client does not recognize, is refused
          with `403 role_denied`. The operation's scope still applies after the gate (a `member`
          holds only the `*.read` scopes). Always `null` for a `business_key`: a key is the account
          itself and holds no membership, so it is never role-gated — its authority is its `scopes`,
          and an operation it cannot call answers `403 scope_denied`.
    additionalProperties: false
    example:
      credential: dashboard_session
      role: member
additionalProperties: false
```

<a id="accountmemberrole"></a>
## AccountMemberRole

```yaml
type: string
x-swaps-open-enum: true
description: >-
  A caller's membership role on an account (`account_members.role`). `owner` and `admin` may change
  the account's own shape and mint durable credentials; `member` reads (its scopes are the `*.read`
  half). No flow assigns `admin` today. Response-only and open: a role added within `/v1` (e.g. a
  team role) is additive — treat a value you do not recognize as neither owner nor admin.
enum:
  - owner
  - admin
  - member
```

<a id="accountupdaterequest"></a>
## AccountUpdateRequest

```yaml
type: object
description: >-
  Partial update. `email`, `country`, `customer_type` and `access_state` are not writable here —
  email change is an identity-plane operation and the rest are derived elsewhere.
properties:
  display_name:
    type: string
    minLength: 1
    maxLength: 80
    description: >-
      The account's name. 1–80 characters; leading and trailing whitespace is trimmed, and a value
      that is empty after trimming is refused with `400 invalid_request`, `param: display_name`.
      Only an account owner or admin may change it (a member gets `403`). Payment links, the payer
      page, receipts and invoice emails show the profile name of the person the link is recorded
      under (for a link created through `/v1`, the account's first owner), not this account name. On
      a live account, saving this name also sets it for each owner who belongs to no other live
      account, so their payers see it at once. An owner who belongs to more than one live account
      keeps their current name on all of them until payer names are resolved per account.
  language:
    type: string
  support_contact:
    type:
      - object
      - 'null'
    description: >-
      L4-9 — the opt-in support contact the payer surfaces may show (§0.12 PATCH null-clears
      convention): omitted leaves it unchanged, an object sets it, explicit `null` clears it back to
      unset. At least one of `email`/`url` must be present on a set (never a bare `{}`).
    properties:
      email:
        type: string
        format: email
        maxLength: 254
      url:
        type: string
        format: uri
        pattern: ^https://
        maxLength: 512
    additionalProperties: false
  notification_preferences:
    type: object
    properties:
      marketing:
        type: boolean
      product_tips:
        type: boolean
  dashboard_preferences:
    type: object
    properties:
      money_tools_hidden:
        type: array
        items:
          type: string
      readiness_card:
        type: string
        enum:
          - expanded
          - collapsed
      setup_guide_mode:
        type: string
        enum:
          - expanded
          - minimized
          - launcher
additionalProperties: false
```

<a id="accountcreaterequest"></a>
## AccountCreateRequest

```yaml
type: object
description: >-
  The body of `POST /accounts`, which is not open today: one account per login is supported, and a
  login that already owns a live account gets `409 account_already_exists`. The contract it will
  carry: an individual or a business account under the same login (D-109), never a sandbox (`POST
  /accounts/{id}/sandboxes` is D-110, a later PR); the caller becomes its sole owner member.
required:
  - display_name
  - customer_type
properties:
  display_name:
    type: string
  customer_type:
    type: string
    enum:
      - individual
      - business
  country:
    type: string
    description: >-
      ISO 3166-1 alpha-2. Optional — unset until the holder declares one. Fix round 1 (P2):
      validated against the same `countries.json` registry `market` (on `Account`) reads from —
      case/whitespace-normalized on write, an unrecognised value is a `400 invalid_request` (`param:
      'country'`), never silently stored and never able to disagree with the resolved
      `market.country` for the same account.
additionalProperties: false
```

<a id="accountlist"></a>
## AccountList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Account'
unevaluatedProperties: false
```

<a id="accountreadiness"></a>
## AccountReadiness

```yaml
type: object
description: |
  The single resolved kind for the persistent Today readiness card, re-derivable on every
  mount (RESOURCE-MODEL §2.7; R18 §3). Not a list — one kind wins.
required:
  - kind
  - title
  - body
properties:
  kind:
    type: string
    enum:
      - error
      - checking
      - pending_transaction
      - verification
      - accept_terms
      - address
      - first_trade
      - returning
  title:
    type: string
  body:
    type: string
  facts:
    type: array
    items:
      type: string
    description: Short supporting facts rendered under the body copy (e.g. a pending-count sentence).
  next_action:
    type:
      - object
      - 'null'
    properties:
      label:
        type: string
      href:
        type: string
  degraded:
    type: boolean
    description: >-
      Present, and `true`, only when the Bridge customer status this card was built from is the last
      persisted snapshot because the live read failed (`Customer.degraded`). The card then states no
      verified identity. Absent on a live read.
  verification_checked_at:
    type: string
    format: date-time
    description: >-
      When the Bridge verification facts this card was built from were last checked: the moment of a
      live read, or, with `verification=snapshot`, when Swaps last received the customer's whole
      state from Bridge (never more than 24 hours earlier). Show it next to the status ("Checked
      …"). Absent when there is no Bridge customer, or for a degraded read of a check whose time is
      unknown.
additionalProperties: false
```

<a id="setupguide"></a>
## SetupGuide

```yaml
type: object
description: |
  Six sections, eighteen steps (RESOURCE-MODEL §2.7; R18 §3 TA-G7): Overview(1), Wallet(2),
  Payment links(5), Payroll(4), Pay an invoice(4), Crypto processing(2).
required:
  - sections
  - done
  - total
properties:
  sections:
    type: array
    items:
      type: object
      required:
        - id
        - title
        - steps
        - done
        - total
      properties:
        id:
          type: string
          enum:
            - overview
            - wallet
            - payment_links
            - payroll
            - pay_invoice
            - crypto_processing
        title:
          type: string
        steps:
          type: array
          items:
            type: object
            required:
              - id
              - label
              - done
            properties:
              id:
                type: string
              label:
                type: string
              done:
                type: boolean
        done:
          type: integer
          minimum: 0
        total:
          type: integer
          minimum: 0
  done:
    type: integer
    minimum: 0
  total:
    type: integer
    minimum: 0
  next_step:
    type:
      - string
      - 'null'
  degraded:
    type: boolean
    description: >-
      Present, and `true`, only when the Bridge customer status this guide was built from is the
      last persisted snapshot because the live read failed (`Customer.degraded`). `verify_identity`
      and `confirm_eligible` then report `done: false` even where the snapshot says verified, and
      `next_step` never names a step withheld for that reason: it is the step a live read of the
      same status names. Absent on a live read.
  verification_checked_at:
    type: string
    format: date-time
    description: >-
      When the Bridge verification facts behind the verification steps were last checked: the moment
      of a live read, or, with `verification=snapshot`, when Swaps last received the customer's
      whole state from Bridge (never more than 24 hours earlier). Show it next to the status
      ("Checked …"). Absent when there is no Bridge customer, or for a degraded read of a check
      whose time is unknown.
additionalProperties: false
```

<a id="activityrow"></a>
## ActivityRow

```yaml
type: object
description: >-
  One row of the cross-product read model over the events outbox. Not independently addressable —
  `object` names the underlying resource a client should read for detail.
required:
  - product
  - object
  - title
  - status
  - status_group
  - amount
  - direction
  - occurred_at
properties:
  product:
    type: string
    enum:
      - payment_links
      - payouts
      - payroll
      - buy_sell
      - wallet
      - crypto_processing
  object:
    type: object
    description: Pointer to the backing `/v1` resource — read it there for the full projection.
    required:
      - id
      - type
    properties:
      id:
        type: string
      type:
        type: string
        description: The pointed-to resource's `object` discriminator, e.g. `payout`, `order`, `payment_link`.
  title:
    type: string
  status:
    type: string
    description: >-
      The underlying object's own status string, verbatim. Not a shared cross-product vocabulary —
      collisions between products' status words are named in R18 §5 and are not resolved by this
      field.
  status_group:
    type: string
    description: >
      This resource's own six-bucket set (`draft`, `pending`, `processing`, `completed`, `failed`,

      `returned`) — not a vocabulary shared with payment_links (`open`, `needs_attention`, `paid`,
      `ended`)

      or with payouts/payroll_runs/orders (`needs_you`, `in_progress`, `done`). Read each of those

      resources' own `status_group` for their set.
    enum:
      - draft
      - pending
      - processing
      - completed
      - failed
      - returned
  amount:
    $ref: '#/components/schemas/Money'
  direction:
    type: string
    enum:
      - in
      - out
  amount_usd_equivalent:
    description: >-
      Nullable — absent when no fresh conversion exists. Never inflate the figure by omitting a
      stale one.
    oneOf:
      - type: 'null'
      - type: object
        required:
          - amount
          - currency
          - decimals
          - fresh_as_of
        properties:
          amount:
            type: string
            pattern: ^-?[0-9]+$
          currency:
            type: string
          decimals:
            type: integer
          fresh_as_of:
            type: string
            format: date-time
  counterparty:
    type:
      - string
      - 'null'
  provider:
    description: Null when this row has no single owning provider (e.g. a payroll run).
    oneOf:
      - $ref: '#/components/schemas/Provider'
      - type: 'null'
  occurred_at:
    type: string
    format: date-time
additionalProperties: false
```

<a id="activitylist"></a>
## ActivityList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/ActivityRow'
unevaluatedProperties: false
```

<a id="activityproductstatusbucket"></a>
## ActivityProductStatusBucket

```yaml
type: object
description: >-
  One (product, status_group) cell of `ActivitySummary.by_product_status` (K8c, §52 C4-D14) — an
  honest count plus a minor-unit sum, never client math. `amount` is `null` when the cell has no
  rows, when its rows carry more than one currency OR the same currency at more than one decimal
  scale (summing across either would need an FX rate or a rescale this endpoint does not fabricate),
  or when a row's amount could not be parsed: `count` still reflects every row in the cell, but a
  sum that quietly excluded the unparseable row's contribution, or combined mismatched scales, would
  be a wrong number, not an honest one.
required:
  - count
  - amount
properties:
  count:
    type: integer
    minimum: 0
  amount:
    oneOf:
      - type: 'null'
      - $ref: '#/components/schemas/Money'
additionalProperties: false
```

<a id="activityproductstatustotals"></a>
## ActivityProductStatusTotals

```yaml
type: object
description: The six `status_group` buckets for one product.
required:
  - draft
  - pending
  - processing
  - completed
  - failed
  - returned
properties:
  draft:
    $ref: '#/components/schemas/ActivityProductStatusBucket'
  pending:
    $ref: '#/components/schemas/ActivityProductStatusBucket'
  processing:
    $ref: '#/components/schemas/ActivityProductStatusBucket'
  completed:
    $ref: '#/components/schemas/ActivityProductStatusBucket'
  failed:
    $ref: '#/components/schemas/ActivityProductStatusBucket'
  returned:
    $ref: '#/components/schemas/ActivityProductStatusBucket'
additionalProperties: false
```

<a id="activitysummary"></a>
## ActivitySummary

```yaml
type: object
description: >-
  Unbounded counts across every row on record, independent of the paginated window `/v1/activity`
  returns (RESOURCE-MODEL §0.12) — a cursor list never carries a total, this does.
required:
  - total
  - by_date_window
  - by_product
  - by_status_group
  - by_product_status
properties:
  total:
    type: integer
    minimum: 0
  by_date_window:
    type: object
    properties:
      today:
        type: integer
        minimum: 0
      last_7d:
        type: integer
        minimum: 0
      last_30d:
        type: integer
        minimum: 0
      last_90d:
        type: integer
        minimum: 0
  by_product:
    type: object
    properties:
      payment_links:
        type: integer
        minimum: 0
      payouts:
        type: integer
        minimum: 0
      payroll:
        type: integer
        minimum: 0
      buy_sell:
        type: integer
        minimum: 0
      wallet:
        type: integer
        minimum: 0
      crypto_processing:
        type: integer
        minimum: 0
  by_status_group:
    type: object
    properties:
      draft:
        type: integer
        minimum: 0
      pending:
        type: integer
        minimum: 0
      processing:
        type: integer
        minimum: 0
      completed:
        type: integer
        minimum: 0
      failed:
        type: integer
        minimum: 0
      returned:
        type: integer
        minimum: 0
  by_product_status:
    type: object
    description: >-
      Per-product × lifecycle-status totals for the product hub sub-lines and Today tiles (K8c, §52
      C4-D14) — the same six products and six status groups as `by_product`/`by_status_group`,
      crossed. Computed over the same capped read as the rest of this resource.
    required:
      - payment_links
      - payouts
      - payroll
      - buy_sell
      - wallet
      - crypto_processing
    properties:
      payment_links:
        $ref: '#/components/schemas/ActivityProductStatusTotals'
      payouts:
        $ref: '#/components/schemas/ActivityProductStatusTotals'
      payroll:
        $ref: '#/components/schemas/ActivityProductStatusTotals'
      buy_sell:
        $ref: '#/components/schemas/ActivityProductStatusTotals'
      wallet:
        $ref: '#/components/schemas/ActivityProductStatusTotals'
      crypto_processing:
        $ref: '#/components/schemas/ActivityProductStatusTotals'
    additionalProperties: false
additionalProperties: false
```

<a id="eventtype"></a>
## EventType

```yaml
type: string
description: >
  The catalogue of dotted event names an `Event.type` / a webhook endpoint's `event_types[]` entry
  may carry

  (RESOURCE-MODEL §3 "publish" rows plus v2 additions; dead types are excluded). This is the CLOSED,

  REQUEST-side form: `webhook_endpoints.create`/`.update`'s `event_types` and `events.list`'s `type`
  filter

  use it, and an unrecognized or retired name there is `400 invalid_request`. `EventTypeOpen`
  (below) is the

  same list for RESPONSE fields, where an unrecognized member is an opaque string a client must not
  fail on —

  the two are one vocabulary read from two sides, not a contradiction: a request is validated
  against what

  this deployment knows, while a response may come from a deployment newer than the client. The list
  grows

  within `/v1` without a version cut.

  `x-swaps-event-status` marks every member `live` (written to the `api_events` outbox today) or
  `catalogued`

  (not written to the outbox: never delivered to a webhook endpoint and never listed by `/v1/events`
  or

  `/v1/activity` until its outbox allowlist ships; it is accepted in a subscription, and a
  per-resource event

  list such as `payouts.events.list` or `payroll_runs.events.list` may still show it from the
  product's own

  ledger). The marking is generated from `EVENT_PAYLOAD_ALLOWLIST`

  (`packages/contracts-api/events.ts`) by `scripts/openapi/normalize.mjs`: the outbox writer drops a
  type

  whose allowlist is empty, so the allowlist is what decides whether a type can ever reach the
  outbox. No historical

  backfill for any type. Producers of the live families: `payment_link.*`/`payment.*` — the
  payment-link

  service layer (merchant mutations, the Bridge webhook reducer, the expiry/reminder cron) and the
  Tempo watcher

  (`payment.underpaid`/`.overpaid`; `payment.unmatched` ONLY for a deposit to a closed, unpaid
  payment's

  address — a second deposit to an already-paid payment, or a deposit matching no payment, is
  recorded for

  support and not published on `/v1`; `payment.unmatched` is also emitted once, by the payment-link
  service

  layer, when a merchant cancels an `underpaid` `crypto_tempo`/`crypto_relay` payment); `payout.*` —

  `appendPayoutEvent` (api-v1 or dashboard/admin actions); `payroll_run.*` — `payroll/lib.ts`'s

  `appendEvent`; `subscription.*` — the crypto subscriptions service; `order.*` —
  `emitOrderOutboxEvent`;

  `customer.*` — `emitCustomerOutboxEvent`; `test.ping` — `webhook_endpoints.send_test_event`,
  delivered

  only to the endpoint under test; `webhook_endpoint.disabled` — `api-webhooks-worker` when an
  endpoint

  auto-disables.
enum:
  - payment_link.created
  - payment_link.activated
  - payment_link.viewed
  - payment_link.paid
  - payment_link.settled
  - payment_link.partially_paid
  - payment_link.refunded
  - payment_link.cancelled
  - payment_link.expired
  - payment_link.reminder_sent
  - payment_link.reminder_schedule_updated
  - payment.created
  - payment.marked_sent
  - payment.awaiting
  - payment.detected
  - payment.processing
  - payment.paid
  - payment.settled
  - payment.underpaid
  - payment.overpaid
  - payment.unmatched
  - payment.expired
  - subscription.created
  - subscription.paused
  - subscription.resumed
  - subscription.cancelled
  - subscription.invoice_issued
  - subscription.invoice_overdue
  - subscription.invoice_paid
  - payout.created
  - payout.funded
  - payout.awaiting_funds
  - payout.funds_received
  - payout.processing
  - payout.paid
  - payout.settled
  - payout.failed
  - payout.returned
  - payout.cancelled
  - payout.paid_with_shortfall
  - payout.marked_sent
  - payout.replaced
  - payout.created_as_replacement
  - payout.beneficiary_added
  - payout.configuration_committed
  - payroll_run.created
  - payroll_run.approved
  - payroll_run.cancelled
  - payroll_run.funding_instructions_requested
  - payroll_run.funding_instructions_verified
  - payroll_run.funding_instructions_failed
  - payroll_run.funding_instructions_blocked
  - payroll_run.funding_verified
  - payroll_run.underfunded
  - payroll_run.execution_requested
  - payroll_run.execution_started
  - payroll_run.execution_blocked
  - payroll_run.completed
  - payroll_run.partial
  - payroll_run.failed
  - payroll_item.destination_updated
  - payroll_item.destination_changed
  - payroll_item.payout_queued
  - payroll_item.payout_processing
  - payroll_item.payout_paid
  - payroll_item.payout_failed
  - payroll_item.payout_returned
  - payroll_template.created
  - deposit_intent.created
  - deposit_intent.source_detected
  - deposit_intent.bridging_started
  - deposit_intent.settled
  - deposit_intent.failed
  - deposit_intent.expired
  - deposit_intent.recovery_started
  - deposit_intent.manual_recovery_required
  - send_intent.created
  - send_intent.source_submitted
  - send_intent.bridging_started
  - send_intent.settled
  - send_intent.failed
  - send_intent.expired
  - offramp_intent.created
  - offramp_intent.funds_received
  - offramp_intent.payment_submitted
  - offramp_intent.payment_processed
  - offramp_intent.refunded
  - offramp_intent.refund_failed
  - offramp_intent.canceled
  - virtual_account.created
  - virtual_account.deactivated
  - virtual_account.reactivated
  - virtual_account.deposit_received
  - wallet.created
  - conversion.built
  - conversion.submitted
  - conversion.settled
  - conversion.failed
  - customer.verification_state_changed
  - customer.rejected
  - customer.requirements_updated
  - capability.blocked
  - capability.unblocked
  - order.created
  - order.status_changed
  - order.failed
  - order.refunded
  - order.cancelled
  - screening.completed
  - screening.risk_elevated
  - account.access_state_changed
  - address_book.entry_rechecked
  - credit.consumed
  - credit.purchased
  - test.ping
  - webhook_endpoint.disabled
x-swaps-event-status:
  payment_link.created: live
  payment_link.activated: live
  payment_link.viewed: live
  payment_link.paid: live
  payment_link.settled: live
  payment_link.partially_paid: live
  payment_link.refunded: live
  payment_link.cancelled: live
  payment_link.expired: live
  payment_link.reminder_sent: live
  payment_link.reminder_schedule_updated: live
  payment.created: live
  payment.marked_sent: live
  payment.awaiting: live
  payment.detected: catalogued
  payment.processing: catalogued
  payment.paid: live
  payment.settled: live
  payment.underpaid: live
  payment.overpaid: live
  payment.unmatched: live
  payment.expired: live
  subscription.created: live
  subscription.paused: live
  subscription.resumed: live
  subscription.cancelled: live
  subscription.invoice_issued: live
  subscription.invoice_overdue: live
  subscription.invoice_paid: live
  payout.created: live
  payout.funded: live
  payout.awaiting_funds: catalogued
  payout.funds_received: catalogued
  payout.processing: catalogued
  payout.paid: catalogued
  payout.settled: catalogued
  payout.failed: catalogued
  payout.returned: catalogued
  payout.cancelled: live
  payout.paid_with_shortfall: catalogued
  payout.marked_sent: live
  payout.replaced: catalogued
  payout.created_as_replacement: live
  payout.beneficiary_added: live
  payout.configuration_committed: catalogued
  payroll_run.created: live
  payroll_run.approved: live
  payroll_run.cancelled: live
  payroll_run.funding_instructions_requested: catalogued
  payroll_run.funding_instructions_verified: catalogued
  payroll_run.funding_instructions_failed: catalogued
  payroll_run.funding_instructions_blocked: catalogued
  payroll_run.funding_verified: live
  payroll_run.underfunded: catalogued
  payroll_run.execution_requested: catalogued
  payroll_run.execution_started: live
  payroll_run.execution_blocked: catalogued
  payroll_run.completed: live
  payroll_run.partial: live
  payroll_run.failed: live
  payroll_item.destination_updated: catalogued
  payroll_item.destination_changed: catalogued
  payroll_item.payout_queued: catalogued
  payroll_item.payout_processing: catalogued
  payroll_item.payout_paid: catalogued
  payroll_item.payout_failed: catalogued
  payroll_item.payout_returned: catalogued
  payroll_template.created: catalogued
  deposit_intent.created: catalogued
  deposit_intent.source_detected: catalogued
  deposit_intent.bridging_started: catalogued
  deposit_intent.settled: catalogued
  deposit_intent.failed: catalogued
  deposit_intent.expired: catalogued
  deposit_intent.recovery_started: catalogued
  deposit_intent.manual_recovery_required: catalogued
  send_intent.created: catalogued
  send_intent.source_submitted: catalogued
  send_intent.bridging_started: catalogued
  send_intent.settled: catalogued
  send_intent.failed: catalogued
  send_intent.expired: catalogued
  offramp_intent.created: catalogued
  offramp_intent.funds_received: catalogued
  offramp_intent.payment_submitted: catalogued
  offramp_intent.payment_processed: catalogued
  offramp_intent.refunded: catalogued
  offramp_intent.refund_failed: catalogued
  offramp_intent.canceled: catalogued
  virtual_account.created: catalogued
  virtual_account.deactivated: catalogued
  virtual_account.reactivated: catalogued
  virtual_account.deposit_received: catalogued
  wallet.created: catalogued
  conversion.built: catalogued
  conversion.submitted: catalogued
  conversion.settled: catalogued
  conversion.failed: catalogued
  customer.verification_state_changed: live
  customer.rejected: live
  customer.requirements_updated: live
  capability.blocked: catalogued
  capability.unblocked: catalogued
  order.created: live
  order.status_changed: live
  order.failed: live
  order.refunded: live
  order.cancelled: live
  screening.completed: catalogued
  screening.risk_elevated: catalogued
  account.access_state_changed: catalogued
  address_book.entry_rechecked: catalogued
  credit.consumed: catalogued
  credit.purchased: catalogued
  test.ping: live
  webhook_endpoint.disabled: live
```

<a id="eventtypeopen"></a>
## EventTypeOpen

```yaml
type: string
x-swaps-open-enum: true
description: >-
  The exact same catalogue as `EventType`, as the RESPONSE-side form (`Event.type`,
  `WebhookEvent.type`, `WebhookEndpoint.event_types`): an unrecognized member is an opaque string,
  never a deserialization failure, because a delivery may carry a type added after the client was
  generated. A REQUEST that names a type still validates against the closed `EventType` and answers
  `400 invalid_request` for an unknown name. Kept as its own schema so the open-enum marker never
  reaches a request field. `x-swaps-event-status` is the same generated live/catalogued marking as
  on `EventType`.
enum:
  - payment_link.created
  - payment_link.activated
  - payment_link.viewed
  - payment_link.paid
  - payment_link.settled
  - payment_link.partially_paid
  - payment_link.refunded
  - payment_link.cancelled
  - payment_link.expired
  - payment_link.reminder_sent
  - payment_link.reminder_schedule_updated
  - payment.created
  - payment.marked_sent
  - payment.awaiting
  - payment.detected
  - payment.processing
  - payment.paid
  - payment.settled
  - payment.underpaid
  - payment.overpaid
  - payment.unmatched
  - payment.expired
  - subscription.created
  - subscription.paused
  - subscription.resumed
  - subscription.cancelled
  - subscription.invoice_issued
  - subscription.invoice_overdue
  - subscription.invoice_paid
  - payout.created
  - payout.funded
  - payout.awaiting_funds
  - payout.funds_received
  - payout.processing
  - payout.paid
  - payout.settled
  - payout.failed
  - payout.returned
  - payout.cancelled
  - payout.paid_with_shortfall
  - payout.marked_sent
  - payout.replaced
  - payout.created_as_replacement
  - payout.beneficiary_added
  - payout.configuration_committed
  - payroll_run.created
  - payroll_run.approved
  - payroll_run.cancelled
  - payroll_run.funding_instructions_requested
  - payroll_run.funding_instructions_verified
  - payroll_run.funding_instructions_failed
  - payroll_run.funding_instructions_blocked
  - payroll_run.funding_verified
  - payroll_run.underfunded
  - payroll_run.execution_requested
  - payroll_run.execution_started
  - payroll_run.execution_blocked
  - payroll_run.completed
  - payroll_run.partial
  - payroll_run.failed
  - payroll_item.destination_updated
  - payroll_item.destination_changed
  - payroll_item.payout_queued
  - payroll_item.payout_processing
  - payroll_item.payout_paid
  - payroll_item.payout_failed
  - payroll_item.payout_returned
  - payroll_template.created
  - deposit_intent.created
  - deposit_intent.source_detected
  - deposit_intent.bridging_started
  - deposit_intent.settled
  - deposit_intent.failed
  - deposit_intent.expired
  - deposit_intent.recovery_started
  - deposit_intent.manual_recovery_required
  - send_intent.created
  - send_intent.source_submitted
  - send_intent.bridging_started
  - send_intent.settled
  - send_intent.failed
  - send_intent.expired
  - offramp_intent.created
  - offramp_intent.funds_received
  - offramp_intent.payment_submitted
  - offramp_intent.payment_processed
  - offramp_intent.refunded
  - offramp_intent.refund_failed
  - offramp_intent.canceled
  - virtual_account.created
  - virtual_account.deactivated
  - virtual_account.reactivated
  - virtual_account.deposit_received
  - wallet.created
  - conversion.built
  - conversion.submitted
  - conversion.settled
  - conversion.failed
  - customer.verification_state_changed
  - customer.rejected
  - customer.requirements_updated
  - capability.blocked
  - capability.unblocked
  - order.created
  - order.status_changed
  - order.failed
  - order.refunded
  - order.cancelled
  - screening.completed
  - screening.risk_elevated
  - account.access_state_changed
  - address_book.entry_rechecked
  - credit.consumed
  - credit.purchased
  - test.ping
  - webhook_endpoint.disabled
x-swaps-event-status:
  payment_link.created: live
  payment_link.activated: live
  payment_link.viewed: live
  payment_link.paid: live
  payment_link.settled: live
  payment_link.partially_paid: live
  payment_link.refunded: live
  payment_link.cancelled: live
  payment_link.expired: live
  payment_link.reminder_sent: live
  payment_link.reminder_schedule_updated: live
  payment.created: live
  payment.marked_sent: live
  payment.awaiting: live
  payment.detected: catalogued
  payment.processing: catalogued
  payment.paid: live
  payment.settled: live
  payment.underpaid: live
  payment.overpaid: live
  payment.unmatched: live
  payment.expired: live
  subscription.created: live
  subscription.paused: live
  subscription.resumed: live
  subscription.cancelled: live
  subscription.invoice_issued: live
  subscription.invoice_overdue: live
  subscription.invoice_paid: live
  payout.created: live
  payout.funded: live
  payout.awaiting_funds: catalogued
  payout.funds_received: catalogued
  payout.processing: catalogued
  payout.paid: catalogued
  payout.settled: catalogued
  payout.failed: catalogued
  payout.returned: catalogued
  payout.cancelled: live
  payout.paid_with_shortfall: catalogued
  payout.marked_sent: live
  payout.replaced: catalogued
  payout.created_as_replacement: live
  payout.beneficiary_added: live
  payout.configuration_committed: catalogued
  payroll_run.created: live
  payroll_run.approved: live
  payroll_run.cancelled: live
  payroll_run.funding_instructions_requested: catalogued
  payroll_run.funding_instructions_verified: catalogued
  payroll_run.funding_instructions_failed: catalogued
  payroll_run.funding_instructions_blocked: catalogued
  payroll_run.funding_verified: live
  payroll_run.underfunded: catalogued
  payroll_run.execution_requested: catalogued
  payroll_run.execution_started: live
  payroll_run.execution_blocked: catalogued
  payroll_run.completed: live
  payroll_run.partial: live
  payroll_run.failed: live
  payroll_item.destination_updated: catalogued
  payroll_item.destination_changed: catalogued
  payroll_item.payout_queued: catalogued
  payroll_item.payout_processing: catalogued
  payroll_item.payout_paid: catalogued
  payroll_item.payout_failed: catalogued
  payroll_item.payout_returned: catalogued
  payroll_template.created: catalogued
  deposit_intent.created: catalogued
  deposit_intent.source_detected: catalogued
  deposit_intent.bridging_started: catalogued
  deposit_intent.settled: catalogued
  deposit_intent.failed: catalogued
  deposit_intent.expired: catalogued
  deposit_intent.recovery_started: catalogued
  deposit_intent.manual_recovery_required: catalogued
  send_intent.created: catalogued
  send_intent.source_submitted: catalogued
  send_intent.bridging_started: catalogued
  send_intent.settled: catalogued
  send_intent.failed: catalogued
  send_intent.expired: catalogued
  offramp_intent.created: catalogued
  offramp_intent.funds_received: catalogued
  offramp_intent.payment_submitted: catalogued
  offramp_intent.payment_processed: catalogued
  offramp_intent.refunded: catalogued
  offramp_intent.refund_failed: catalogued
  offramp_intent.canceled: catalogued
  virtual_account.created: catalogued
  virtual_account.deactivated: catalogued
  virtual_account.reactivated: catalogued
  virtual_account.deposit_received: catalogued
  wallet.created: catalogued
  conversion.built: catalogued
  conversion.submitted: catalogued
  conversion.settled: catalogued
  conversion.failed: catalogued
  customer.verification_state_changed: live
  customer.rejected: live
  customer.requirements_updated: live
  capability.blocked: catalogued
  capability.unblocked: catalogued
  order.created: live
  order.status_changed: live
  order.failed: live
  order.refunded: live
  order.cancelled: live
  screening.completed: catalogued
  screening.risk_elevated: catalogued
  account.access_state_changed: catalogued
  address_book.entry_rechecked: catalogued
  credit.consumed: catalogued
  credit.purchased: catalogued
  test.ping: live
  webhook_endpoint.disabled: live
```

<a id="event"></a>
## Event

```yaml
description: >
  The one event envelope for every per-object event list in the API: `object` is fixed to `event`
  and

  `data.object` is required. First-class and `$ref`-able from any fragment — a payment link's, a
  payout's

  or any other resource's own event list is `Event` items, never a bespoke per-product event schema.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - type
      - data
      - api_version
    properties:
      id:
        type: string
        pattern: ^evt_
      object:
        type: string
        enum:
          - event
      type:
        $ref: '#/components/schemas/EventTypeOpen'
      data:
        $ref: '#/components/schemas/EventData'
      request:
        type:
          - object
          - 'null'
        description: >-
          Null for events with no originating API request (a webhook reducer, a cron sweep, a
          watcher).
        properties:
          id:
            type:
              - string
              - 'null'
          idempotency_key:
            type:
              - string
              - 'null'
      api_version:
        type: string
        format: date
        description: The `Swaps-Version` this event was minted under.
unevaluatedProperties: false
```

<a id="eventdata"></a>
## EventData

```yaml
type: object
required:
  - object
properties:
  object:
    type: object
    description: >-
      The affected resource's own allowlisted projection at the time of the event — never the raw
      internal row. Deliberately THINNER than a `GET` of the same resource: fields that need
      gateway-only computation (a payment link's `payable_rails`/`payable_rail_kinds`, its line
      `items`) are never recomputed here, so they are absent from this projection even where `GET`
      would carry them — never guessed or backfilled from a stale value.
    additionalProperties: true
  detail:
    type: string
    minLength: 1
    description: >-
      A short, human-readable summary of the event, built server-side from the producer's own record
      of what happened (e.g. `3 rows · USD`, `Jamie Rivera · bank account`) — never a substitute for
      `data.object`, and never more than what the resource's own published projections already
      disclose, and never an internal actor's identity (an account's approving user, an operator)
      unless that identity is itself a published field elsewhere the same caller can already read.
      Populated per event type by the producer that emits it (K7b ships it for
      `payroll_run.*`/`payroll_item.*`); absent, not an empty string, where no producer has one yet.
unevaluatedProperties: false
```

<a id="webhookevent"></a>
## WebhookEvent

```yaml
description: >
  The body of every outbound webhook delivery (the OpenAPI `webhooks` of this document), signed with

  `Swaps-Signature` (docs/api/WEBHOOKS.md §3) — verify the signature over the raw bytes BEFORE
  parsing. The

  same event `GET /v1/events/{id}` returns, minus `object` and `request`, which a delivery never
  carries.

  `api_version` is the receiving endpoint's own pinned `Swaps-Version`, not necessarily the one the
  event

  was minted under. Delivery is at-least-once and ordering is best-effort: deduplicate on `id`.
type: object
required:
  - id
  - type
  - created_at
  - livemode
  - api_version
  - data
properties:
  id:
    type: string
    pattern: ^evt_
  type:
    $ref: '#/components/schemas/EventTypeOpen'
  created_at:
    type: string
    format: date-time
  livemode:
    type: boolean
  api_version:
    type: string
    format: date
    description: The receiving endpoint's pinned `Swaps-Version`.
  data:
    $ref: '#/components/schemas/EventData'
additionalProperties: false
```

<a id="eventlist"></a>
## EventList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Event'
unevaluatedProperties: false
```

<a id="webhookendpoint"></a>
## WebhookEndpoint

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - url
      - event_types
      - enabled
    properties:
      id:
        type: string
        pattern: ^whe_
      object:
        type: string
        enum:
          - webhook_endpoint
      url:
        type: string
        format: uri
        description: >
          Public https only. Rejected at create/update time if it is a `swaps.app` host or a Swaps
          project's own `supabase.co` host, or

          resolves to a private, link-local, loopback or otherwise reserved destination (SSRF
          hardening); the delivery worker re-resolves and re-checks

          the SAME way immediately before every send attempt and never follows a redirect — a 3xx
          response

          is recorded as a failed delivery attempt, not a success. This narrows, but does not
          eliminate, a

          DNS-rebinding window: the guard's own resolution and the subsequent `fetch()` are two
          separate DNS

          lookups a hostile or compromised resolver could answer differently (docs/api/WEBHOOKS.md
          §7).
      event_types:
        type: array
        items:
          $ref: '#/components/schemas/EventTypeOpen'
        description: >
          Event types this endpoint receives. Empty means "all" (every type this account can ever
          emit,

          present and future). An unknown or retired name is `400 invalid_request`, at create and
          update alike

          (validated against the closed `EventType` at that time — this response echo is open only
          because

          it is an A1-2 response field, not because an unrecognized value can actually appear here).
          A type

          marked `catalogued` in `EventType`'s `x-swaps-event-status` is accepted but delivers
          nothing until its outbox allowlist ships.
      enabled:
        type: boolean
      disabled_reason:
        type:
          - string
          - 'null'
        enum:
          - manual
          - auto_disabled_repeated_failures
          - null
        description: >
          Set only when `enabled` is false: `manual` (the owner disabled it via update) or

          `auto_disabled_repeated_failures` (K9's own auto-disable after too many consecutive

          fully-exhausted deliveries — see `webhook_deliveries.status`). Null whenever `enabled` is
          true.
      description:
        type:
          - string
          - 'null'
      secret_prefix:
        type:
          - string
          - 'null'
        description: >
          The signing secret's own non-secret prefix (e.g. `whsec_ab12`) — always present once an
          endpoint has

          a secret, safe to display anywhere the full secret is not (it never re-derives the full
          value).
      secret:
        type:
          - string
          - 'null'
        description: |
          The full signing secret, in cleartext. Present ONLY in the response to `create` and
          `rotate_secret` — stored hashed thereafter, never returned again and never re-derivable.
unevaluatedProperties: false
```

<a id="webhookendpointcreaterequest"></a>
## WebhookEndpointCreateRequest

```yaml
type: object
required:
  - url
properties:
  url:
    type: string
    format: uri
    description: >
      Public https only. A private, link-local, loopback or otherwise reserved destination is
      rejected,

      and so is any `swaps.app` host or a Swaps project's own `supabase.co` host (the platform never
      delivers to itself).
  event_types:
    type: array
    items:
      $ref: '#/components/schemas/EventType'
    description: >
      Subset of the closed event registry this endpoint receives. Omitted or empty means "all". An
      unknown

      or retired name is `400 invalid_request`. A type marked `catalogued` in `EventType`'s

      `x-swaps-event-status` is accepted but delivers nothing until its outbox allowlist ships.
  description:
    type: string
additionalProperties: false
```

<a id="webhookendpointupdaterequest"></a>
## WebhookEndpointUpdateRequest

```yaml
type: object
properties:
  url:
    type: string
    format: uri
    description: >
      Public https only. A private, link-local, loopback or otherwise reserved destination is
      rejected,

      and so is any `swaps.app` host or a Swaps project's own `supabase.co` host (the platform never
      delivers to itself).
  event_types:
    type: array
    items:
      $ref: '#/components/schemas/EventType'
    description: An unknown or retired name is `400 invalid_request`.
  enabled:
    type: boolean
    description: |
      Setting `false` disables the endpoint (`disabled_reason` becomes `manual`) and halts future
      deliveries — past deliveries stay in the log. Setting `true` re-enables it and clears
      `disabled_reason`, including one an auto-disable had set.
  description:
    type: string
additionalProperties: false
```

<a id="webhookendpointlist"></a>
## WebhookEndpointList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/WebhookEndpoint'
unevaluatedProperties: false
```

<a id="webhookdelivery"></a>
## WebhookDelivery

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - endpoint_id
      - event_id
      - attempt
      - status
    properties:
      id:
        type: string
        pattern: ^whd_
      object:
        type: string
        enum:
          - webhook_delivery
      endpoint_id:
        type: string
        pattern: ^whe_
      event_id:
        type: string
        pattern: ^evt_
      attempt:
        type: integer
        minimum: 0
        description: >-
          How many delivery attempts this row has made so far (the worker's own retry schedule; see
          docs/api/WEBHOOKS.md).
      status:
        type: string
        enum:
          - pending
          - succeeded
          - failed
          - exhausted
        description: >
          `pending`: never yet attempted, due now. `succeeded`: a 2xx response — terminal. `failed`:
          the most

          recent attempt failed and a further retry is already scheduled at `next_attempt_at`
          (automatic — no

          action needed). `exhausted`: every scheduled attempt failed and no further retry will
          happen — replay

          it explicitly with `webhook_deliveries.replay`.
      next_attempt_at:
        type:
          - string
          - 'null'
        format: date-time
        description: >-
          When the next automatic attempt is due. null once `status` is `succeeded` or `exhausted`:
          no further automatic attempt follows a terminal delivery.
      response_status:
        type:
          - integer
          - 'null'
        description: >-
          The HTTP status the endpoint returned, or null if the attempt never got a response
          (timeout, DNS/SSRF refusal, connection error).
      response_ms:
        type:
          - integer
          - 'null'
        description: >-
          Round-trip time in milliseconds for the most recent attempt. The response BODY is never
          stored.
      error:
        type:
          - string
          - 'null'
        description: >-
          A short machine-readable failure reason for the most recent attempt (e.g. `timeout`,
          `connection_error`, `non_2xx_response`, `attempt_limit_exceeded`, or
          `url_not_allowed:<reason>` — the SSRF/DNS refusal carries its own suffix, e.g.
          `url_not_allowed:disallowed_host` or `url_not_allowed:dns_resolution_failed`). Never the
          endpoint's response body.
      delivered_at:
        type:
          - string
          - 'null'
        format: date-time
        description: Set once, when `status` first becomes `succeeded`.
unevaluatedProperties: false
```

<a id="webhookdeliverylist"></a>
## WebhookDeliveryList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/WebhookDelivery'
unevaluatedProperties: false
```

<a id="webhookwaitlistrequest"></a>
## WebhookWaitlistRequest

```yaml
type: object
description: Minimal email capture ahead of the full webhook-delivery system (RESOURCE-MODEL §1 DEV-G9).
required:
  - email
properties:
  email:
    type: string
    format: email
additionalProperties: false
```

<a id="cardwaitlistrequest"></a>
## CardWaitlistRequest

```yaml
type: object
description: Write-only waitlist capture. No corresponding read.
required:
  - email
properties:
  email:
    type: string
    format: email
additionalProperties: false
```

<a id="addressbookentry"></a>
## AddressBookEntry

```yaml
description: |
  A saved payment destination, one of seven rail shapes (R16 `layer-ab-add`). Bank-rail fields
  are masked on every read; the rail-specific field present depends on `rail`. `screening`
  carries the latest `/v1/screenings` projection of `address` on `network` and is populated for
  `rail: crypto` only.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - label
      - rail
    properties:
      id:
        type: string
        pattern: ^adr_
      object:
        type: string
        enum:
          - address_book_entry
      label:
        type: string
      rail:
        type: string
        enum:
          - crypto
          - iban
          - ach
          - faster_payments
          - pix
          - spei
          - swift
      network:
        type:
          - string
          - 'null'
        description: 'rail: crypto only.'
      address:
        type:
          - string
          - 'null'
        description: 'rail: crypto only. Case-preserved verbatim — never lowercased.'
      iban_masked:
        type:
          - string
          - 'null'
        description: 'rail: iban only.'
      bic:
        type:
          - string
          - 'null'
        description: 'rail: iban or swift.'
      routing_number_masked:
        type:
          - string
          - 'null'
        description: 'rail: ach only.'
      account_number_masked:
        type:
          - string
          - 'null'
        description: 'rail: ach, faster_payments or swift.'
      sort_code_masked:
        type:
          - string
          - 'null'
        description: 'rail: faster_payments only.'
      pix_key_type:
        type:
          - string
          - 'null'
        description: 'rail: pix only.'
      pix_key_masked:
        type:
          - string
          - 'null'
        description: 'rail: pix only.'
      clabe_masked:
        type:
          - string
          - 'null'
        description: 'rail: spei only.'
      bank_address:
        type:
          - string
          - 'null'
        description: 'rail: swift only.'
      screening:
        description: >-
          rail: crypto only — the latest screening of `address` on `network` by this account, from
          `screenings.create` or `address_book.recheck`. `network` is matched the way
          `screenings.create` reads `chain` (a chain id or name, case-insensitive), so an entry
          saved as `Ethereum` carries a screening recorded under chain `1`. Null only when no such
          screening exists: no `/v1` screening of it yet (checks made in the v1 dashboard are not
          carried over, so null does not mean never checked), or screening does not cover the entry
          — a network such as Tempo, or an address that cannot exist on its network — and its
          `address_book.recheck` answers `409 network_not_supported`, never an Ethereum verdict.
        oneOf:
          - $ref: '#/components/schemas/Screening'
          - type: 'null'
      last_checked_at:
        type:
          - string
          - 'null'
        format: date-time
        description: >-
          `screened_at` of `screening`; null exactly when `screening` is null — a check made in the
          v1 dashboard is not carried over.
      beneficiary_type:
        description: >-
          `self` for the holder's own destination, `third-party` once a beneficiary attestation
          exists.
        oneOf:
          - type: 'null'
          - type: string
            enum:
              - self
              - third-party
      beneficiary_subtype:
        description: >-
          Null until a beneficiary attestation exists. Set to
          `individual`/`legal_entity`/`vasp_customer` by `address_book.beneficiary.update`; set to
          `self_verified`/`self_unverified` by the dashboard's proof-of-control flow (not itself
          exposed on `/v1`).
        oneOf:
          - type: 'null'
          - type: string
            enum:
              - individual
              - legal_entity
              - vasp_customer
              - self_verified
              - self_unverified
      proof_of_control_method:
        description: >-
          Read-only. Null until the destination is verified through the dashboard's proof-of-control
          flow — that verification step is not exposed on `/v1` (out of scope for this operation).
          `micro_deposit` is published because a live v1 writer already uses it, even though today's
          DB CHECK constraint has not been widened to accept it (a separate v1 bug) — publishing it
          now costs nothing, no consumers exist yet.
        oneOf:
          - type: 'null'
          - type: string
            enum:
              - signed_message
              - micro_tx
              - screenshot
              - micro_deposit
              - other
      proof_of_control_verified_at:
        type:
          - string
          - 'null'
        format: date-time
        description: Read-only. Non-null rows are what the address-book hub counts as "verified".
      proof_of_control_artifact_url:
        type:
          - string
          - 'null'
        description: >-
          Read-only. A Supabase Storage object key in a private bucket — not a directly fetchable
          public URL.
      travel_rule_required:
        type: boolean
        description: >-
          True once a transfer through this destination triggered EU TFR / MiCA / FATF R.16
          evaluation.
      travel_rule_last_attested_at:
        type:
          - string
          - 'null'
        format: date-time
        description: >-
          Null until a transfer through this destination has been Travel-Rule-attested. Stamped only
          by the transfer-time attestation (`attestTransfer`, mid-transfer) — never written by any
          `/v1` operation, including `address_book.beneficiary.update`.
      counterparty_name:
        type:
          - string
          - 'null'
        description: >-
          The saved beneficiary's name. Null until a beneficiary attestation exists. Set by
          `address_book.beneficiary.update`. No pattern/length constraint on this READ shape,
          deliberately — so a row already stored before a request-side constraint existed can never
          fail the whole list/get response, the same failure mode finding #5 flagged for
          `proof_of_control_method`. The constraint lives on the request schema
          (`TravelRuleAttestationCreateRequest`) instead.
      counterparty_country:
        type:
          - string
          - 'null'
        description: ISO 3166-1 alpha-2, uppercased on write. Null until a beneficiary attestation exists.
      counterparty_vasp_name:
        type:
          - string
          - 'null'
        description: The counterparty VASP's name. Only set when `beneficiary_subtype` is `vasp_customer`.
      counterparty_vasp_jurisdiction:
        type:
          - string
          - 'null'
        description: >-
          ISO 3166-1 alpha-2, uppercased on write. Only set when `beneficiary_subtype` is
          `vasp_customer`.
      last_used_at:
        type:
          - string
          - 'null'
        format: date-time
        description: >-
          When this entry was last the destination of an order. Set only by `orders.create` with
          this entry's `address_book_id`, for a dashboard-session caller, after the order is created
          (`201`); an order sent with a plain `wallet_address`, or by a business key, never sets it.
          Null means no such order was recorded — orders made before this field existed are not
          backfilled. Sort by it for "recently used".
unevaluatedProperties: false
```

<a id="addressbookentrycreaterequest"></a>
## AddressBookEntryCreateRequest

```yaml
description: >
  One of seven fully-specified rail shapes, a real discriminated union on `rail` — not a flat bag of

  optional fields. Bank-rail details cross the boundary once here; `/v1` never echoes them back
  unmasked

  on a read (the read shape is `AddressBookEntry`).
oneOf:
  - $ref: '#/components/schemas/AddressBookEntryCreateRequestCrypto'
  - $ref: '#/components/schemas/AddressBookEntryCreateRequestIban'
  - $ref: '#/components/schemas/AddressBookEntryCreateRequestAch'
  - $ref: '#/components/schemas/AddressBookEntryCreateRequestFasterPayments'
  - $ref: '#/components/schemas/AddressBookEntryCreateRequestPix'
  - $ref: '#/components/schemas/AddressBookEntryCreateRequestSpei'
  - $ref: '#/components/schemas/AddressBookEntryCreateRequestSwift'
discriminator:
  propertyName: rail
  mapping:
    crypto: '#/components/schemas/AddressBookEntryCreateRequestCrypto'
    iban: '#/components/schemas/AddressBookEntryCreateRequestIban'
    ach: '#/components/schemas/AddressBookEntryCreateRequestAch'
    faster_payments: '#/components/schemas/AddressBookEntryCreateRequestFasterPayments'
    pix: '#/components/schemas/AddressBookEntryCreateRequestPix'
    spei: '#/components/schemas/AddressBookEntryCreateRequestSpei'
    swift: '#/components/schemas/AddressBookEntryCreateRequestSwift'
```

<a id="addressbookentrycreaterequestcrypto"></a>
## AddressBookEntryCreateRequestCrypto

```yaml
type: object
description: 'rail: crypto.'
required:
  - label
  - rail
  - network
  - address
properties:
  label:
    type: string
  rail:
    type: string
    enum:
      - crypto
  network:
    type: string
    description: >-
      Saved as sent; any network can be saved. Screening covers only the networks
      `ScreeningCreateRequest.chain` lists — an entry on any other reads `screening: null` and its
      `address_book.recheck` answers `409 network_not_supported`.
  address:
    type: string
    description: Case-preserved verbatim — never lowercase a BTC, Tron or Solana address.
additionalProperties: false
```

<a id="addressbookentrycreaterequestiban"></a>
## AddressBookEntryCreateRequestIban

```yaml
type: object
description: 'rail: iban.'
required:
  - label
  - rail
  - account_holder
  - iban
  - bic
properties:
  label:
    type: string
  rail:
    type: string
    enum:
      - iban
  iban:
    type: string
  bic:
    type: string
  account_holder:
    type: string
    minLength: 1
    description: >-
      Legal name of the account owner, as the bank holds it (a person's full name or a company's
      registered name). A payment link settles to this destination under this name. Blank after
      trimming is refused. Stored trimmed and not returned on a read.
additionalProperties: false
```

<a id="addressbookentrycreaterequestach"></a>
## AddressBookEntryCreateRequestAch

```yaml
type: object
description: 'rail: ach.'
required:
  - label
  - rail
  - account_holder
  - account_number
  - routing_number
  - account_type
properties:
  label:
    type: string
  rail:
    type: string
    enum:
      - ach
  account_number:
    type: string
  routing_number:
    type: string
  account_type:
    type: string
    enum:
      - checking
      - savings
  account_holder:
    type: string
    minLength: 1
    description: >-
      Legal name of the account owner, as the bank holds it (a person's full name or a company's
      registered name). A payment link settles to this destination under this name. Blank after
      trimming is refused. Stored trimmed and not returned on a read.
additionalProperties: false
```

<a id="addressbookentrycreaterequestfasterpayments"></a>
## AddressBookEntryCreateRequestFasterPayments

```yaml
type: object
description: 'rail: faster_payments.'
required:
  - label
  - rail
  - account_holder
  - sort_code
  - account_number
properties:
  label:
    type: string
  rail:
    type: string
    enum:
      - faster_payments
  sort_code:
    type: string
  account_number:
    type: string
  account_holder:
    type: string
    minLength: 1
    description: >-
      Legal name of the account owner, as the bank holds it (a person's full name or a company's
      registered name). A payment link settles to this destination under this name. Blank after
      trimming is refused. Stored trimmed and not returned on a read.
additionalProperties: false
```

<a id="addressbookentrycreaterequestpix"></a>
## AddressBookEntryCreateRequestPix

```yaml
type: object
description: 'rail: pix.'
required:
  - label
  - rail
  - account_holder
  - pix_key
  - pix_key_type
properties:
  label:
    type: string
  rail:
    type: string
    enum:
      - pix
  pix_key:
    type: string
  pix_key_type:
    type: string
  account_holder:
    type: string
    minLength: 1
    description: >-
      Legal name of the account owner, as the bank holds it (a person's full name or a company's
      registered name). A payment link settles to this destination under this name. Blank after
      trimming is refused. Stored trimmed and not returned on a read.
additionalProperties: false
```

<a id="addressbookentrycreaterequestspei"></a>
## AddressBookEntryCreateRequestSpei

```yaml
type: object
description: 'rail: spei.'
required:
  - label
  - rail
  - account_holder
  - clabe
properties:
  label:
    type: string
  rail:
    type: string
    enum:
      - spei
  clabe:
    type: string
  account_holder:
    type: string
    minLength: 1
    description: >-
      Legal name of the account owner, as the bank holds it (a person's full name or a company's
      registered name). A payment link settles to this destination under this name. Blank after
      trimming is refused. Stored trimmed and not returned on a read.
additionalProperties: false
```

<a id="addressbookentrycreaterequestswift"></a>
## AddressBookEntryCreateRequestSwift

```yaml
type: object
description: 'rail: swift.'
required:
  - label
  - rail
  - account_holder
  - swift_bic
  - account_number
  - bank_address
properties:
  label:
    type: string
  rail:
    type: string
    enum:
      - swift
  swift_bic:
    type: string
  account_number:
    type: string
  bank_address:
    type: string
  account_holder:
    type: string
    minLength: 1
    description: >-
      Legal name of the account owner, as the bank holds it (a person's full name or a company's
      registered name). SWIFT destinations are not a payment-link settlement destination today:
      `activate` answers `422 settlement_rail_unsupported`; use iban, ach, faster_payments, pix or
      spei. Blank after trimming is refused. Stored trimmed and not returned on a read.
additionalProperties: false
```

<a id="addressbookentryupdaterequest"></a>
## AddressBookEntryUpdateRequest

```yaml
type: object
description: >-
  Label only — every rail field is immutable after creation (remove and re-add to change a
  destination).
required:
  - label
properties:
  label:
    type: string
additionalProperties: false
```

<a id="travelruleattestationcreaterequest"></a>
## TravelRuleAttestationCreateRequest

```yaml
type: object
description: >
  Updates the Travel Rule beneficiary/counterparty details for a saved destination — the same fields
  the dashboard's Manage tab writes (`CounterpartyForm`). Sets `beneficiary_type` to `third-party`;
  never writes `travel_rule_last_attested_at` (stamped only by the transfer-time attestation).
  `counterparty_*` are readable on the entry afterward — not write-only.
required:
  - beneficiary_subtype
  - counterparty_name
properties:
  beneficiary_subtype:
    type: string
    enum:
      - individual
      - legal_entity
      - vasp_customer
  counterparty_name:
    type: string
    minLength: 1
    maxLength: 256
  counterparty_country:
    type: string
    pattern: ^[A-Za-z]{2}$
    description: ISO 3166-1 alpha-2, either case (uppercased on write).
  counterparty_vasp_name:
    type: string
    maxLength: 256
    description: >-
      Required when `beneficiary_subtype` is `vasp_customer`; refused blank, exactly like the
      dashboard form.
  counterparty_vasp_jurisdiction:
    type: string
    pattern: ^[A-Za-z]{2}$
    description: >-
      ISO 3166-1 alpha-2, either case (uppercased on write). Only meaningful when
      beneficiary_subtype is vasp_customer.
additionalProperties: false
```

<a id="addressbooklist"></a>
## AddressBookList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/AddressBookEntry'
unevaluatedProperties: false
```

<a id="screening"></a>
## Screening

```yaml
description: |
  A risk signal, never a verdict: `action` is a suggestion, not an authorization, and the
  audit row backing this object stores an address hash, never the address itself.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - address
      - chain
      - risk_score
      - risk_level
      - verdict
      - action
      - cached
      - screened_at
    properties:
      id:
        type: string
        pattern: ^scr_
      object:
        type: string
        enum:
          - screening
      address:
        type: string
        description: Case-preserved verbatim.
      chain:
        type: string
      risk_score:
        type: integer
        minimum: 0
        maximum: 100
      risk_level:
        type: string
        enum:
          - low
          - medium
          - high
          - critical
          - unknown
      verdict:
        type: string
        description: Derived, coarser than `risk_level`. Still a signal, never a block decision.
        enum:
          - clear
          - caution
          - risky
          - no_clear_verdict
          - not_checked
      flags:
        type: array
        items:
          type: string
      details:
        type: object
        description: Free-form, source-shaped detail — never re-typed per source.
        additionalProperties: true
      confidence:
        type:
          - number
          - 'null'
        minimum: 0
        maximum: 1
      coverage:
        type: array
        items:
          type: string
        description: The sources actually checked, so a client can see what was not.
      action:
        type: string
        description: A suggestion the caller may act on — never an authorization by itself.
        enum:
          - allow
          - offer_report
          - block
      provider_version:
        type: string
      cached:
        type: boolean
      screened_at:
        type: string
        format: date-time
unevaluatedProperties: false
```

<a id="screeningcreaterequest"></a>
## ScreeningCreateRequest

```yaml
type: object
required:
  - address
  - chain
properties:
  address:
    type: string
    description: Case-preserved verbatim — never lowercase a BTC, Tron or Solana address.
  chain:
    type: string
    description: >-
      The network to screen on, by chain id or name, case-insensitive: Ethereum (`1`, `eth`), BNB
      Chain (`56`, `bsc`), Polygon (`137`), Arbitrum (`42161`), Optimism (`10`), Base (`8453`),
      Avalanche (`43114`), `bitcoin`, `tron`, `solana`, `xrp` (`XRP Ledger`), `ton`, `cardano`,
      `cosmos`. Any other value — blank, `tempo`, `litecoin` — whatever the address, and an address
      that cannot exist on the named network (an EVM address on `bitcoin`, a Bitcoin address on
      `tron`) are refused `409 network_not_supported`; nothing is ever screened as Ethereum by
      default. An address is judged by the named network's own address format; on `solana` that is
      any base58 string of 32–44 characters.
additionalProperties: false
```

<a id="addresscheckrequest"></a>
## AddressCheckRequest

```yaml
type: object
description: >-
  One destination address and the network it will receive on, checked before the person signs in.
  Sent in the body, never the URL (API-CANON §9).
required:
  - network
  - address
properties:
  network:
    type: string
    minLength: 1
    maxLength: 64
    description: >-
      The network the address will receive on, by chain id or name, case-insensitive — the same
      vocabulary as `ScreeningCreateRequest.chain`: Ethereum (`1`, `eth`), BNB Chain (`56`, `bsc`),
      Polygon (`137`), Arbitrum (`42161`), Optimism (`10`), Base (`8453`), Avalanche (`43114`),
      `bitcoin`, `tron`, `solana`, `xrp`, `ton`, `cardano`, `cosmos`. A network screening does not
      cover is answered `decision: not_screenable`, never screened as Ethereum.
  address:
    type: string
    minLength: 1
    maxLength: 128
    description: >-
      The address exactly as it will be submitted. Case-preserved: a Bitcoin, Tron or Solana address
      is never lowercased. It is not stored and never echoed back.
additionalProperties: false
```

<a id="addresscheck"></a>
## AddressCheck

```yaml
type: object
description: >
  An advisory answer for one destination address, before sign-in. Nothing is stored for the caller,
  no credit is spent, and no score, level or label is published — only the decision. It is never the
  binding check: `POST /v1/orders` screens the destination again server-side and can still refuse it
  (`destination_blocked`), so a client must never present this answer as the order's own screening.
required:
  - object
  - network
  - decision
  - checked_at
properties:
  object:
    type: string
    enum:
      - address_check
  network:
    type: string
    description: The request's `network`, trimmed.
  decision:
    type: string
    enum:
      - allow
      - offer_report
      - block
      - not_screenable
    description: >-
      `allow` — no risk signal above the reporting threshold. `offer_report` — a moderate signal:
      offer the person a report before they continue. `block` — a severe signal or a sanctions
      match; `POST /v1/orders` refuses a destination its own screening blocks
      (`destination_blocked`). `not_screenable` — screening does not cover this network or address
      format (for example Litecoin, or an address that cannot exist on the named network); nothing
      was checked, so it is neither a clean nor a risky answer. The same four decisions the exchange
      widget uses today.
  checked_at:
    type: string
    format: date-time
    description: >-
      When the screening data behind this decision was produced — earlier than now when it was
      served from the screening cache.
additionalProperties: false
```

<a id="screeninglist"></a>
## ScreeningList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Screening'
unevaluatedProperties: false
```

<a id="screeningreport"></a>
## ScreeningReport

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - counterparties
      - evidence_pdf
      - cached
    properties:
      id:
        type: string
        pattern: ^rpt_
      object:
        type: string
        enum:
          - screening_report
      counterparties:
        type: array
        items:
          type: object
          properties:
            address:
              type: string
            label:
              type:
                - string
                - 'null'
            risk_level:
              type: string
              enum:
                - low
                - medium
                - high
                - critical
                - unknown
      evidence_pdf:
        type: object
        required:
          - available
        properties:
          available:
            type: boolean
      cached:
        type: boolean
        description: >-
          True when this report was served from the 30-day cache rather than spending a fresh
          credit.
unevaluatedProperties: false
```

<a id="screeningreportlist"></a>
## ScreeningReportList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/ScreeningReport'
unevaluatedProperties: false
```

<a id="tracerequest"></a>
## TraceRequest

```yaml
type: object
required:
  - hash
  - chain
properties:
  hash:
    type: string
    description: >-
      The origin transaction hash to trace forward from (the incident transfer, e.g. the transaction
      that sent funds to a scammer) — a trace always starts from a specific transaction, never a
      bare address, since an address alone has no single "flow" to follow.
  chain:
    type: string
additionalProperties: false
```

<a id="trace"></a>
## Trace

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - hops
      - counterparties
    properties:
      id:
        type: string
        pattern: ^trc_
      object:
        type: string
        enum:
          - trace
      hops:
        type: array
        items:
          type: object
          properties:
            address:
              type: string
            chain:
              type: string
            amount:
              oneOf:
                - $ref: '#/components/schemas/Money'
                - type: 'null'
            direction:
              type:
                - string
                - 'null'
              enum:
                - in
                - out
                - null
      counterparties:
        type: array
        items:
          type: object
          properties:
            address:
              type: string
            label:
              type:
                - string
                - 'null'
            risk_level:
              type: string
              enum:
                - low
                - medium
                - high
                - critical
                - unknown
unevaluatedProperties: false
```

<a id="chaintransaction"></a>
## ChainTransaction

```yaml
type: object
description: >-
  One on-chain transaction, read by `{chain, hash}` — not a stored, independently addressable
  object, so it carries no `id`.
required:
  - chain
  - hash
  - amounts
  - transfers
  - counterparties
properties:
  chain:
    type: string
  hash:
    type: string
  amounts:
    type: array
    items:
      $ref: '#/components/schemas/Money'
  transfers:
    type: array
    items:
      type: object
      properties:
        from:
          type: string
        to:
          type: string
        amount:
          $ref: '#/components/schemas/Money'
        asset:
          type: string
        direction:
          type:
            - string
            - 'null'
          enum:
            - in
            - out
            - null
  counterparties:
    type: array
    description: A short risk brief on each side of the transaction.
    items:
      type: object
      properties:
        address:
          type: string
        label:
          type:
            - string
            - 'null'
        risk_level:
          type: string
          enum:
            - low
            - medium
            - high
            - critical
            - unknown
additionalProperties: false
```

<a id="credits"></a>
## Credits

```yaml
type: object
required:
  - free_remaining
  - paid_balance
  - total_checks
properties:
  free_remaining:
    type: integer
    minimum: 0
  paid_balance:
    type: integer
    minimum: 0
  total_checks:
    type: integer
    minimum: 0
additionalProperties: false
```

<a id="creditevent"></a>
## CreditEvent

```yaml
type: object
description: One ledger row. Not independently addressable — carries no `id`, only a list.
required:
  - type
  - amount
  - balance_after
  - created_at
properties:
  type:
    type: string
    enum:
      - consume_free
      - consume_paid
      - cache_hit
      - insufficient
      - purchase
      - refund
      - admin_grant
  amount:
    type: integer
    description: Signed credit delta — negative for a consumption, positive for a purchase, refund or grant.
  address:
    type:
      - string
      - 'null'
    description: The screened address this event relates to, when one exists.
  chain:
    type:
      - string
      - 'null'
  balance_after:
    type: integer
  created_at:
    type: string
    format: date-time
additionalProperties: false
```

<a id="crediteventlist"></a>
## CreditEventList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/CreditEvent'
      events_total:
        type: integer
        minimum: 0
        description: >-
          Lifetime, exact row count of every `credit_events` entry for this account, across all
          `type` values — never capped by `limit`/`cursor` pagination and never limited to the page
          currently returned in `data`. Omitted (not `null`, not `0`) on the rare failure of the
          underlying count query — a caller must treat a missing field as "unknown right now", not
          as zero.

          Displayed side by side with `GET /v1/credits`'s own `total_checks` as "N events · M
          reports" — never summed. The two counters are not on the same basis and are not
          reconcilable against each other: `events_total` is an append-only row count (it includes
          the `refund` row itself, alongside the `consume_free`/`consume_paid` row it refunds),
          while `total_checks` is a net counter that is incremented on consume and DECREMENTED,
          clamped at zero, on refund. Summing them double-counts every refunded check, and the two
          drift further apart with every refund an account accumulates.
unevaluatedProperties: false
```

<a id="creditcheckoutrequest"></a>
## CreditCheckoutRequest

```yaml
type: object
required:
  - pack
properties:
  pack:
    type: integer
    enum:
      - 5
      - 10
      - 20
additionalProperties: false
```

<a id="creditcheckout"></a>
## CreditCheckout

```yaml
type: object
required:
  - url
properties:
  url:
    type: string
    format: uri
    description: A Stripe Checkout session url — redirect the holder there to complete the purchase.
unevaluatedProperties: false
```

<a id="apikey"></a>
## ApiKey

```yaml
description: |
  Dashboard session caller only — a key can never mint another key. Blocked on D-11
  (`api_keys` has no owner column an authorized read could key on) until that lands.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - name
      - key_prefix
      - last_four
      - scopes
      - plan
      - status
      - default_swaps_version
    properties:
      id:
        type: string
        pattern: ^key_
      object:
        type: string
        enum:
          - api_key
      name:
        type: string
      key_prefix:
        type: string
        description: '`sk_live_` or `sk_test_`, matching `livemode`.'
      last_four:
        type: string
      default_swaps_version:
        type: string
        format: date
        description: >
          The `Swaps-Version` this key pins requests to when the caller sends no `Swaps-Version`
          header

          (API-CANON §3, RESOURCE-MODEL §0.6). A request that does send the header overrides this
          default

          for that call only; this field is unaffected.
      scopes:
        type: array
        description: >-
          A key created before the `screenings.*` rename may still list the legacy
          `screening.read`/`screening.write`.
        items:
          type: string
          pattern: ^[a-z_]+\.(read|write)$
          description: '`<resource>.<read|write>`.'
      plan:
        type: object
        required:
          - name
          - per_minute
          - per_day
        properties:
          name:
            type: string
            enum:
              - free
              - pro
              - enterprise
          per_minute:
            type: integer
          per_day:
            type: integer
      allowed_ips:
        type: array
        items:
          type: string
      expires_at:
        type:
          - string
          - 'null'
        format: date-time
      last_used_at:
        type:
          - string
          - 'null'
        format: date-time
      revoked_at:
        type:
          - string
          - 'null'
        format: date-time
      status:
        type: string
        enum:
          - active
          - revoked
          - expired
unevaluatedProperties: false
```

<a id="apikeycreaterequest"></a>
## ApiKeyCreateRequest

```yaml
type: object
required:
  - name
  - scopes
  - livemode
properties:
  name:
    type: string
  scopes:
    type: array
    minItems: 1
    description: >-
      `<resource>.<read|write>`. The legacy `screening.read`/`screening.write` are deprecated
      aliases, accepted until the next `Swaps-Version` date and stored as
      `screenings.read`/`screenings.write`. `support.read` and `support.write` belong to dashboard
      sessions only and are refused here with `400 invalid_request`: every support operation is
      session-only, so a key holding one would reach nothing.
    items:
      type: string
      pattern: ^[a-z_]+\.(read|write)$
    x-swaps-deprecated-alias:
      - value: screening.read
        replacement: screenings.read
        accepted_until: next Swaps-Version date
      - value: screening.write
        replacement: screenings.write
        accepted_until: next Swaps-Version date
  livemode:
    type: boolean
  default_swaps_version:
    type: string
    format: date
    description: Optional; defaults to the current `Swaps-Version` when omitted.
  allowed_ips:
    type: array
    items:
      type: string
  expires_at:
    type: string
    format: date-time
additionalProperties: false
```

<a id="apikeycreated"></a>
## ApiKeyCreated

```yaml
description: >-
  Returned once, at creation and again on `roll` — the plaintext secret is never shown or
  retrievable again.
allOf:
  - $ref: '#/components/schemas/ApiKey'
  - type: object
    required:
      - secret
    properties:
      secret:
        type:
          - string
          - 'null'
        description: >
          The full `sk_live_…` / `sk_test_…` value, in cleartext. Present only in the FIRST live
          response to

          `create`/`roll` — stored hashed thereafter, never returned again and never re-derivable. A
          replayed

          Idempotency-Key against the same request gets the same body back with this field redacted
          to null,

          never the cleartext a second time (`AuthenticatedRoute.secretFields`, the same mechanism
          K9

          introduced for `webhook_endpoints.create`/`.rotate_secret`).
unevaluatedProperties: false
```

<a id="apikeylist"></a>
## ApiKeyList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/ApiKey'
unevaluatedProperties: false
```

<a id="apikeyusage"></a>
## ApiKeyUsage

```yaml
type: object
description: Today-only — no trend history exists yet (RESOURCE-MODEL DEV-G3).
required:
  - requests_today
  - ok_today
  - errors_today
properties:
  requests_today:
    type: integer
    minimum: 0
  ok_today:
    type: integer
    minimum: 0
  errors_today:
    type: integer
    minimum: 0
  last_request_at:
    type:
      - string
      - 'null'
    format: date-time
additionalProperties: false
```

<a id="requestlog"></a>
## RequestLog

```yaml
description: >-
  Raw per-call HTTP telemetry for one of the caller's own keys — distinct from the product-lifecycle
  `/v1/events` catalogue.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - request_id
      - operation
      - status_code
      - ok
      - latency_ms
      - ip
    properties:
      id:
        type: string
        pattern: ^req_
      object:
        type: string
        enum:
          - request_log
      request_id:
        type: string
      operation:
        type: string
        description: The `operationId` this call resolved to, e.g. `screenings.create`.
      status_code:
        type: integer
      ok:
        type: boolean
      latency_ms:
        type: integer
        minimum: 0
      ip:
        type: string
      error_code:
        type:
          - string
          - 'null'
unevaluatedProperties: false
```

<a id="requestloglist"></a>
## RequestLogList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/RequestLog'
unevaluatedProperties: false
```

<a id="paymentlinkitem"></a>
## PaymentLinkItem

```yaml
type: object
description: >-
  A line item on a payment link (`payment_link_items`). Quantity and unit price are locked once set;
  `unit` is a late, design-forward addition (RESOURCE-MODEL v2 amendment, R11 PL-G4).
properties:
  product_id:
    type:
      - string
      - 'null'
    pattern: ^prd_
  name:
    type: string
  description:
    type:
      - string
      - 'null'
  quantity:
    type: integer
    minimum: 1
  unit_price:
    $ref: '#/components/schemas/MoneyNonNegative'
  unit:
    type:
      - string
      - 'null'
    enum:
      - item
      - hour
      - day
      - project
      - null
    description: >-
      Optional unit of measure. Design-forward (RESOURCE-MODEL v2 amendment, R11 PL-G4) — not a
      confirmed backend column.
  sort_order:
    type: integer
  metadata:
    type: object
    additionalProperties: true
additionalProperties: false
```

<a id="paymentlink"></a>
## PaymentLink

```yaml
description: >-
  A request for money — the merchant's own view (RESOURCE-MODEL §2.1). Distinct from the
  payer-facing projection at `GET /v1/payment_sessions/{token}`, which never carries these same
  field names verbatim.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - status
      - amount
      - title
      - memo
      - invoice_number
      - reference_id
      - client_id
      - direction
      - expected_payer_type
      - payable_rail_kinds
      - reminder_schedule
      - expires_at
      - payer_email
      - settlement_kind
      - settlement_asset
      - settlement_destination
      - short_code
      - url
      - short_url
      - viewed_at
      - paid_at
      - settled_at
    properties:
      id:
        type: string
        pattern: ^pl_
      object:
        type: string
        enum:
          - payment_link
      status:
        type: string
        enum:
          - draft
          - active
          - viewed
          - processing
          - paid
          - settled
          - expired
          - cancelled
          - refunded
        description: >-
          `refunded` reads "Refunded" to the merchant and "Returned" to the payer on the same
          underlying value (R11 §3) — one enum member, two audience labels.
      title:
        type:
          - string
          - 'null'
      memo:
        type:
          - string
          - 'null'
        description: >-
          Labelled "Memo" on Payment Links merchant screens; the crypto-processing surfaces label
          this same field "Description" (R19 CP-G15) — one field, two display labels, never two
          schema fields.
      invoice_number:
        type:
          - string
          - 'null'
      reference_id:
        type:
          - string
          - 'null'
        readOnly: true
        description: >-
          A short, non-technical, server-minted id in `CP-<n>` form (e.g. `CP-1042`), unique per
          merchant. Set by the server at creation, never accepted as input. Distinct from
          `invoice_number` above, which is an optional, merchant-typed reference that may repeat or
          be null, and from `id` (the `pl_`-prefixed identifier), whose raw form is not meant to be
          shown to a payer or merchant as a reference number. Present on every payment link, not
          only crypto-processing ones.
      amount:
        $ref: '#/components/schemas/Money'
      client_id:
        type:
          - string
          - 'null'
        pattern: ^cli_
      direction:
        type:
          - string
          - 'null'
        readOnly: true
        description: >-
          System-set at activation, never an input (RESOURCE-MODEL §2.1 invariants). Enum values are
          not published in RESOURCE-MODEL; left open rather than invented.
      expected_payer_type:
        type: string
        enum:
          - any
          - business
          - individual
        default: any
      allowed_rails:
        type: array
        items:
          type: string
          x-swaps-open-enum: true
          enum:
            - ach
            - wire
            - fednow
            - sepa
            - faster_payments
            - pix
            - spei
            - crypto_tempo
            - crypto_bridge
            - crypto_relay
        description: >-
          `crypto_relay` (R19 X2) is a payer rail behind the `crypto_relay_rail` flag (off in
          production), never a stored restriction. A `settlement_kind: crypto_only` link offers
          `crypto_tempo`, plus `crypto_relay` where its gate admits it (the same on `payable_rails`
          and in the payer session): that flag and an admitted route (`founder_accepted` per founder
          §52.34, or `proven`; see K13-CRYPTO-RELAY-ROUTES.md). A1-2: open on THIS response field
          only — an unrecognized rail here is an opaque string, never a deserialization failure. The
          same vocabulary on `PaymentLinkCreateRequest`/`PaymentLinkUpdateRequest` stays closed: a
          merchant choosing an unknown rail is a real request error, not a fact to tolerate.
      payable_rails:
        type: array
        readOnly: true
        items:
          type: string
          x-swaps-open-enum: true
          enum:
            - ach
            - wire
            - fednow
            - sepa
            - faster_payments
            - pix
            - spei
            - crypto_tempo
            - crypto_bridge
            - crypto_relay
        description: >-
          The rails the payer is offered now (one exception below), recomputed on every read; not a
          snapshot (RESOURCE-MODEL §2.1 v2 amendment). A later read can differ from an earlier one
          when a rail flag, a Relay route or the Relay cap changes. `crypto_relay` is listed exactly
          when the payer session of this link offers it (the same gate: the `crypto_relay_rail` flag
          for this merchant, the per-invoice cap, an admitted route for the link's Tempo settlement
          network, a USD link). If that gate cannot be read, `crypto_relay` is left out and the read
          still succeeds, where the payer session answers `503 temporarily_unavailable`. Exception:
          an individual (`direction: p2p`) merchant whose Bridge fiat pay-in is not active yet is
          listed with its bank rails, although the payer session withholds them (C4-D23; tracked in
          #3977). Open-enum like `allowed_rails` above (queue item #3560) — the same
          forward-compatibility reasoning applies to a computed field.
      payable_rail_kinds:
        type: object
        readOnly: true
        description: >-
          Computed grouping of `payable_rails` into the three UI kinds (RESOURCE-MODEL §2.1 v2
          amendment; R11 PL-G5).
        properties:
          bank:
            type: boolean
          crypto:
            type: boolean
          card:
            type: boolean
        additionalProperties: false
      accepted_rail_kinds:
        type: array
        items:
          type: string
          enum:
            - bank
            - crypto
            - card
        description: >-
          A `crypto_only` settlement suppresses every fiat rail, so this reads `[crypto]`
          (RESOURCE-MODEL §2.1 v2 amendment; R19 §2.1).
      expires_at:
        type:
          - string
          - 'null'
        format: date-time
      payer_email:
        type:
          - string
          - 'null'
        format: email
      reminder_schedule:
        $ref: '#/components/schemas/ReminderSchedule'
      settlement_kind:
        type:
          - string
          - 'null'
        readOnly: true
        enum:
          - bridge
          - crypto_only
          - null
        description: >-
          System-set at activation, never an input (RESOURCE-MODEL §2.1 invariants; RESOURCE-MODEL
          §2.1 v2 amendment). Null on a draft that has not activated yet.
      settlement_asset:
        readOnly: true
        description: >-
          BL-39 (C4-D27) — the token identity frozen from `settlement_snapshot` at ACTIVATION, never
          a live chain read and never derived from a rate. `null` on a draft (no snapshot yet) or a
          non-Tempo settlement — Bridge bank/crypto destinations have no single payer-facing "token"
          this field describes. Carries no amount of its own.
        oneOf:
          - $ref: '#/components/schemas/SettlementAsset'
          - type: 'null'
      settlement_destination:
        readOnly: true
        description: >-
          BL-50 — the saved `/v1/address_book` entry `create`/`update` set as where this link's
          funds land. `null` until chosen. Read from the SAME reference `activate` freezes into
          `settlement_snapshot` (never cleared by activation), so it keeps reading the live
          destination after the link goes live too — distinct from `settlement_asset` above, which
          is the token identity frozen at that one moment.
        oneOf:
          - $ref: '#/components/schemas/SettlementDestinationView'
          - type: 'null'
      items:
        type: array
        items:
          $ref: '#/components/schemas/PaymentLinkItem'
      short_code:
        type: string
        pattern: ^[0-9bcdfghjkmnpqrstvwxyz]{7}$
        readOnly: true
        description: >-
          C5-SHORT-LINK — the link's short public code (e.g. `7fq3k2c`): 7 characters of lowercase
          Crockford base32 without the vowels `a`/`e` (so also no `i`, `l`, `o`, `u`). Minted by the
          server from a cryptographic random source when the link is created, for every link (draft
          or live, live or test mode), never accepted as input, never derived from ids, amounts,
          e-mails or names, and never changed afterwards. Unique across the project's links. It
          resolves to the payer session through `GET /v1/payment_sessions/by_code/{short_code}`, so
          treat it like `url`: share it with the payer only.
      url:
        type:
          - string
          - 'null'
        format: uri
      short_url:
        type:
          - string
          - 'null'
        format: uri
        readOnly: true
        description: >-
          C5-SHORT-LINK — `<payer host>/p/<short_code>` (e.g. `https://swaps.app/p/7fq3k2c`), on the
          same per-project payer host as `url` and under the same rule: `null` exactly when `url` is
          `null`. Not yet a working payer link: it resolves only once the `/p/:code` page is live on
          that host, and only while the `/v1` API is enabled; share `url` until then.
      viewed_at:
        type:
          - string
          - 'null'
        format: date-time
        readOnly: true
        description: RESOURCE-MODEL §2.1 v2 amendment, R11 PL-G1.
      paid_at:
        type:
          - string
          - 'null'
        format: date-time
        readOnly: true
      settled_at:
        type:
          - string
          - 'null'
        format: date-time
        readOnly: true
      partial_payment:
        type:
          - object
          - 'null'
        readOnly: true
        description: >-
          Set only while `status` is `processing` and a payment arrived short of `amount` (§8.4).
          `received` reports the single MOST RECENT short payment recorded for this link's CURRENT
          attempt — one deposit's figure, not a running total of everything the link has received —
          and is `null` when that deposit's settled amount could not be confirmed (shown to the
          merchant as "needs review"). `null` while `processing` with nothing recorded, and again
          once the link leaves `processing` for a later full payment or a cancellation. A no-funds
          failure on an attempt moves the link to `viewed` and clears this field to `null`; it does
          NOT reopen the link to `processing`. The flag CAN reappear, but only when a LATER attempt
          creates a new transfer and puts the link back into `processing` — if that new attempt also
          under-pays, its own event is what reappears here, never an earlier attempt's superseded
          figure. The key itself may be absent when the underlying signal could not be read — that
          is not the same as `null`.
        properties:
          received:
            description: Null when the settled amount could not be confirmed.
            oneOf:
              - $ref: '#/components/schemas/Money'
              - type: 'null'
          expected:
            $ref: '#/components/schemas/Money'
        required:
          - received
          - expected
        additionalProperties: false
unevaluatedProperties: false
```

<a id="paymentlinklist"></a>
## PaymentLinkList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/PaymentLink'
      summary:
        $ref: '#/components/schemas/PaymentLinkListSummary'
unevaluatedProperties: false
```

<a id="paymentlinklistsummary"></a>
## PaymentLinkListSummary

```yaml
type: object
x-swaps-since: '2026-09-25'
description: >-
  Present only with `?expand=summary` (LIST-SUMMARY-1). Computed server-side over every link
  matching the request's `status_group`, `q`, `client_id` and `settlement_kind` (never
  `cursor`/`limit`), in one grouped query. `by_status_group` uses the `status_group` buckets
  exactly; they overlap (`viewed`/`processing` are both `open` and `needs_attention`), so they do
  not add up to `total`.
required:
  - total
  - by_status_group
  - collected
properties:
  total:
    type: integer
    minimum: 0
  by_status_group:
    type: object
    required:
      - open
      - needs_attention
      - paid
      - ended
    properties:
      open:
        type: integer
        minimum: 0
      needs_attention:
        type: integer
        minimum: 0
      paid:
        type: integer
        minimum: 0
      ended:
        type: integer
        minimum: 0
    additionalProperties: false
  collected:
    type: array
    description: >-
      The amount of every matching `paid` or `settled` link, summed exactly per currency (minor
      units, same `Money` rules as `PaymentLink.amount`), sorted by currency. Empty when none. The
      link's requested amount, not a settlement ledger.
    items:
      $ref: '#/components/schemas/Money'
additionalProperties: false
```

<a id="settlementdestination"></a>
## SettlementDestination

```yaml
type: object
description: >-
  A REQUEST-side settlement destination reference (BL-50) — `create`/`update`'s own input shape,
  always naming a real, owned `/v1/address_book` entry (bank or crypto, RESOURCE-MODEL §2.4);
  `address_book_id` is ownership-checked server-side on every write and again at `activate` — a
  foreign or unknown id answers the identical `404 not_found` either way (RESOURCE-MODEL §0.7),
  never a distinguishing error. See {@link SettlementDestinationView} for the RESPONSE shape, which
  additionally allows `address_book_id: null` (a wallet-via-bridge destination has no address_book
  row) — the two never disagree about the id format, only about whether a client can ever
  legitimately send a null one (it cannot: there is nothing to create or replace with "no
  destination", only `settlement_destination: null` at the top level clears the field, per
  `PaymentLinkUpdateRequest.settlement_destination`'s own three-state convention).
required:
  - address_book_id
properties:
  address_book_id:
    type: string
    pattern: ^adr_
additionalProperties: false
```

<a id="settlementdestinationview"></a>
## SettlementDestinationView

```yaml
type: object
description: >-
  Where an activated link's funds land, as PUBLISHED on `PaymentLink`/`PaymentLinkReceipt` — a
  REFERENCE to something the account already owns, never a raw address or bank detail on the wire
  (BL-50). Usually a saved `/v1/address_book` entry, same `address_book_id` {@link
  SettlementDestination} (the REQUEST shape) accepts. Fixer round (§52.33, PL-TEMPO-BRIDGE-4-T,
  findings #2/#8) — `address_book_id` is `null` for a "settle to my Swaps Wallet" link routed
  through Bridge (`payment_links_tempo_via_bridge`): that destination is the account's OWN wallet,
  resolved server-side, with no `address_book` row behind it, but `provider` (below) still publishes
  the FX-leg disclosure for it exactly like an address-book Tempo destination does. An object rather
  than a bare string so a future destination kind can be added without a breaking change.
required:
  - address_book_id
properties:
  address_book_id:
    type:
      - string
      - 'null'
    pattern: ^adr_
  provider:
    description: >-
      PL-TEMPO-BRIDGE-1-T (§52.33, independent money review — the FX-leg P1); mechanism fixed by
      PL-TEMPO-BRIDGE-2-T (Bridge external accounts are fiat-only — sandbox proof: `400
      invalid_parameters` on `account_type: 'crypto'`). Present ONLY once activation has
      materialized a Bridge COLLECTION account for this destination (a non-USD link's Tempo wallet
      settling through Bridge instead of the non-custodial `crypto_tempo` splitter) — absent for
      every other destination (a plain Tempo splitter address, a Bridge bank rail, or a draft with
      no snapshot yet). `status: 'created'` is the only value this can ever carry: a Bridge REFUSAL
      of the collection account (`settlement_destination_provider_refused`) fails the `activate`
      call itself before anything is persisted, so a link can never be read back with any other
      status here. Carries no exchange rate or fee — that data exists only once a real transfer
      settles; this object reports the destination Bridge holds, never a quote. `last4` is the
      settlement address's own last 4 characters (the collection template's `to_address`), never an
      account id — there is no external account.
    oneOf:
      - type: object
        required:
          - id
          - status
          - network
          - last4
        properties:
          id:
            type: string
            enum:
              - bridge
          status:
            type: string
            enum:
              - created
          network:
            type: string
            enum:
              - tempo
            description: >-
              Bridge's own chain slug for this collection account's destination (`payment_rail:
              'tempo'`) — distinct from `SettlementAsset.network`, which labels the non-custodial
              splitter's token instead.
          last4:
            type: string
            description: The settlement address's own last 4 characters. Never the full address.
        additionalProperties: false
      - type: 'null'
additionalProperties: false
```

<a id="paymentlinkcreaterequest"></a>
## PaymentLinkCreateRequest

```yaml
type: object
description: >-
  Creates a `draft`. Nothing is charged and no rail goes live until `activate` (D-15).
  `reminder_schedule.sent[]` is server-owned — this body has no `reminder_schedule` field, and any
  caller-supplied value on that path is stripped by the router before the create action runs
  (MON-13). `settlement_destination` may be set here or left for a later `update` — either way
  `activate` refuses `400 settlement_destination_required` until one is chosen.
required:
  - amount
properties:
  title:
    type: string
    description: >-
      Optional — a blank title is treated as absent. When absent, it is derived server-side:
      `invoice_number` (as `Invoice <invoice_number>`) if set, else the first line item's name (or,
      when unnamed, the name of the product it resolves to), with `+N more` appended for additional
      items. Only when none of those can supply a value does create answer `400 invalid_request`
      naming `title`. A derived title longer than 200 characters is truncated with an ellipsis
      (L4-10).
  memo:
    type: string
  invoice_number:
    type: string
  amount:
    $ref: '#/components/schemas/MoneyPositive'
  client_id:
    type: string
    pattern: ^cli_
  expected_payer_type:
    type: string
    enum:
      - any
      - business
      - individual
    default: any
  allowed_rails:
    type: array
    items:
      type: string
      enum:
        - ach
        - wire
        - fednow
        - sepa
        - faster_payments
        - pix
        - spei
        - crypto_tempo
        - crypto_bridge
        - crypto_relay
    description: >-
      Rails to offer the payer on this link. Use `accepted_rail_kinds: [crypto]` to request Swaps
      Wallet settlement (USD splitter or non-USD Bridge path); rails here alone do not select
      `settlement_kind`.
  accepted_rail_kinds:
    type: array
    items:
      type: string
      enum:
        - bank
        - crypto
        - card
    description: >-
      Set to `[crypto]` for a link settling to your OWN Swaps Wallet (R19 CP-G2) — there is no
      separate `settlement_kind` input (RESOURCE-MODEL §2.1 invariants: `settlement_kind` is
      system-set, never an input). On a `USD` request this routes the later `activate` call onto the
      Bridge-less `crypto_tempo` splitter branch, gated by `payment_links_crypto_only`. On any OTHER
      currency (§52.33, PL-TEMPO-BRIDGE-4-T, fixer round findings #5/#11) it instead routes
      `activate` onto the SAME Bridge collection path (`payment_rail: tempo` + the account's own
      wallet address) an address-book Tempo destination uses, gated by
      `payment_links_tempo_via_bridge` ALONE — the SAME flag `capabilities.get`'s
      `settlement_currencies[currency].tempo_wallet` reads.
  settlement_destination:
    $ref: '#/components/schemas/SettlementDestination'
    description: >-
      BL-50 — the saved destination `activate` will freeze into `settlement_snapshot`. Naming this
      alongside `accepted_rail_kinds: [crypto]` (a Swaps-Wallet intent) on the SAME create request
      is refused `400 settlement_conflicts_with_destination` — reachable only while this account is
      admitted for that currency (`payment_links_crypto_only` for `USD`,
      `payment_links_tempo_via_bridge` otherwise); when it is not, `accepted_rail_kinds: [crypto]`
      is itself refused first (`409 capability_unavailable` on `USD`, `422
      tempo_via_bridge_not_enabled` otherwise), so the 400 never fires. `update` does not refuse the
      combination: setting a destination there RETIRES an earlier `accepted_rail_kinds: [crypto]`
      intent instead — see `PaymentLinkUpdateRequest.settlement_destination`.
  expires_at:
    type: string
    format: date-time
  payer_email:
    type: string
    format: email
  items:
    type: array
    items:
      $ref: '#/components/schemas/PaymentLinkItem'
additionalProperties: false
```

<a id="paymentlinkupdaterequest"></a>
## PaymentLinkUpdateRequest

```yaml
type: object
description: >-
  Draft-only (RESOURCE-MODEL §2.1) — refused once the link has left `draft`. The reminder cadence
  has its own sub-resource and is not part of this body; `reminder_schedule.sent[]` is server-owned,
  and any caller value on that path is stripped by the router before this action runs (MON-13).
properties:
  title:
    type: string
    description: >-
      A blank value is refused `400 invalid_request` naming `title` — clearing it is not supported;
      omit the key to leave the stored title unchanged (L4-10).
  memo:
    type: string
  invoice_number:
    type: string
  amount:
    $ref: '#/components/schemas/MoneyPositive'
  client_id:
    type: string
    pattern: ^cli_
  expected_payer_type:
    type: string
    enum:
      - any
      - business
      - individual
  allowed_rails:
    type:
      - array
      - 'null'
    items:
      type: string
      enum:
        - ach
        - wire
        - fednow
        - sepa
        - faster_payments
        - pix
        - spei
        - crypto_tempo
        - crypto_bridge
        - crypto_relay
    description: >-
      Accepted as a partial write mid-draft (R11 PL-G6). Three states, and they are distinct: omit
      the field to leave the restriction as it is, send `null` to REMOVE it (the link then offers
      every rail available for its currency), send an array to replace it. An empty array is not a
      way to clear it — it is a `400`, because a link restricted to no rail cannot be paid. A
      restriction that leaves the link with no payable rail at all is refused `400
      allowed_rails_invalid_for_currency` (and `409` at activation), carrying the rails that would
      work in `error.details.offerable_rails`. It narrows what the link can be paid on and never
      widens it: a rail the merchant is not endorsed for stays unavailable whether or not it is
      named here.
  settlement_destination:
    description: >-
      BL-50 — three-state, same convention as `allowed_rails` above: omit to leave the stored
      destination as it is, send `null` to CLEAR it, send `{address_book_id}` to set or replace it.
      An id you do not own answers `404 not_found`, identical to an unknown one (RESOURCE-MODEL
      §0.7). Setting a destination here RETIRES an earlier `accepted_rail_kinds: [crypto]`
      (Swaps-Wallet) intent recorded at create — the response stops reporting `accepted_rail_kinds`
      and the link settles to this destination instead; the newer choice always wins. `400
      settlement_conflicts_with_destination` is refused only by `create`, for naming both in the
      SAME request — `update` never throws it.
    oneOf:
      - $ref: '#/components/schemas/SettlementDestination'
      - type: 'null'
  expires_at:
    type: string
    format: date-time
  payer_email:
    type: string
    format: email
  items:
    type: array
    items:
      $ref: '#/components/schemas/PaymentLinkItem'
additionalProperties: false
```

<a id="paymentlinkactivaterequest"></a>
## PaymentLinkActivateRequest

```yaml
type: object
description: >-
  The money boundary (D-15). `attestation_accepted` is the merchant's own compliance attestation,
  written back as evidence with an ip hash and the caller's auth user id — an agent must never set
  this on the merchant's behalf, which is why this operation is REST-only and excluded from MCP.
required:
  - attestation_accepted
properties:
  attestation_accepted:
    type: boolean
    description: Must be `true`; any other value is refused.
additionalProperties: false
```

<a id="paymentlinkreceipt"></a>
## PaymentLinkReceipt

```yaml
type: object
description: >-
  The merchant's receipt for a collected link (RESOURCE-MODEL §2.1 v2 amendment: "mirrors the payout
  receipt"). Every value is read from the link row and the ONE payment attempt that completed it —
  never from a status string, never estimated. Payment Links has no beneficiary and no
  multi-provider estimate, so the payout-only `beneficiary{}` and `estimates{}` are absent. Shape
  per A1 D-101 (confirmed amounts, masked settlement destination, provider reference). `fee`,
  `deducted_fee` and `net_amount` are the same values `GET /v1/payments/{payment_id}` returns for
  the completing payment, so the two surfaces never disagree.
required:
  - payment_link_id
  - payment_id
  - title
  - invoice_number
  - amount
  - payment_rail
  - amount_received
  - fee
  - deducted_fee
  - net_amount
  - settlement_kind
  - settlement_destination
  - provider_reference
  - onchain_tx_hash
  - paid_at
  - settled_at
  - note
properties:
  payment_link_id:
    type: string
    pattern: ^pl_
  payment_id:
    type: string
    pattern: ^pay_
    description: The payment (attempt) that completed the link — `GET /v1/payments/{id}` returns it.
  title:
    type: string
  invoice_number:
    type:
      - string
      - 'null'
  amount:
    description: What the link asked for, in its own currency — the ask, not an observation.
    allOf:
      - $ref: '#/components/schemas/Money'
  payment_rail:
    type: string
    description: The rail the completing payment used (a `Payment.payment_rail` value).
  recipient_amount:
    description: >-
      What the merchant received, provider-confirmed. OMITTED today, never `null` and never the
      invoice amount: the received figure is recorded on the link's `paid` event (checked against
      the invoice within a 1% tolerance) but is not yet published as a provider-confirmed amount.
      `net_amount` is the server arithmetic, not this confirmation.
    allOf:
      - $ref: '#/components/schemas/Money'
      - type: object
        properties:
          confirmed:
            type: boolean
  amount_received:
    description: >-
      The observed on-chain deposit for a `crypto_tempo` payment, in the token that arrived (same
      value as `Payment.amount_received`). `null` on every other rail.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  provider_reference:
    type:
      - string
      - 'null'
    description: >-
      The provider's reference for a provider-routed payment — the transfer id, or the deposit id
      for a payment collected on a collection account (virtual account); `null` for `crypto_tempo`.
  fee:
    description: >-
      The 1% Swaps fee, payer-borne — same value as `Payment.fee` for `payment_id` (exact server
      arithmetic on `crypto_tempo`; `null` on rails where the payment projection publishes none). It
      is why a `crypto_tempo` `amount_received` exceeds `amount`.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  deducted_fee:
    description: >-
      The Swaps fee deducted from what arrived on a Bridge-routed rail — same value as
      `Payment.deducted_fee` for `payment_id` (the provider receipt's figure); `null` on
      `crypto_tempo`/`crypto_relay`, when the payer paid in another currency than the invoice, and
      while no consistent receipt is recorded.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  net_amount:
    description: >-
      What the merchant receives — same value as `Payment.net_amount` for `payment_id`: the invoice
      amount on `crypto_tempo`/`crypto_relay` once settled; on a Bridge-routed rail the provider
      receipt's figure (what arrived minus `deducted_fee`, in the invoice currency, before
      conversion), `null` without one; `null` before settlement.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  settlement_kind:
    type:
      - string
      - 'null'
    enum:
      - bridge
      - crypto_only
      - null
    description: Same value as `PaymentLink.settlement_kind`.
  settlement_destination:
    description: >-
      Where the funds land, masked as a reference — same value as
      `PaymentLink.settlement_destination`; never a raw address or bank detail.
    oneOf:
      - $ref: '#/components/schemas/SettlementDestinationView'
      - type: 'null'
  onchain_tx_hash:
    type:
      - string
      - 'null'
  paid_at:
    type: string
    format: date-time
  settled_at:
    type:
      - string
      - 'null'
    format: date-time
    description: '`null` while the link is `paid` and not yet `settled`.'
  note:
    type:
      - string
      - 'null'
    description: The link's own memo.
unevaluatedProperties: false
```

<a id="reminderschedule"></a>
## ReminderSchedule

```yaml
type: object
description: >-
  Editable while the link is `draft`, `active` or `viewed` (RESOURCE-MODEL §2.1). R11 PL-G3 flags a
  design screen that disables editing on `viewed` — not ratified as a rule change, so the contract
  keeps `viewed` editable.
properties:
  status:
    type: string
    enum:
      - 'off'
      - 'on'
  offsets_days:
    type: array
    items:
      type: integer
    description: Caller-owned.
  sent:
    type: array
    readOnly: true
    items:
      type: integer
    description: Server-owned (RESOURCE-MODEL §2.1). A caller-supplied value here is ignored, not merged.
  next_reminder_at:
    type:
      - string
      - 'null'
    format: date-time
    readOnly: true
    description: Computed convenience field (RESOURCE-MODEL §2.1 v2 amendment; R11 PL-G17).
unevaluatedProperties: false
```

<a id="reminderscheduleupdaterequest"></a>
## ReminderScheduleUpdateRequest

```yaml
type: object
description: >-
  Sets the caller-owned cadence. `sent[]` and `next_reminder_at` are server-owned and never accepted
  here; use `disable` to turn reminders off rather than sending an empty schedule.
required:
  - offsets_days
properties:
  offsets_days:
    type: array
    items:
      type: integer
additionalProperties: false
```

<a id="client"></a>
## Client

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - display_name
    properties:
      id:
        type: string
        pattern: ^cli_
      object:
        type: string
        enum:
          - client
      display_name:
        type: string
      email:
        type:
          - string
          - 'null'
        format: email
      company:
        type:
          - string
          - 'null'
      country:
        type:
          - string
          - 'null'
        description: >-
          Free text today. R11 PL-G13 is an open ruling on whether this narrows to a closed enum —
          left as free text pending that ruling.
      role:
        type:
          - string
          - 'null'
      notes:
        type:
          - string
          - 'null'
      metadata:
        type: object
        additionalProperties: true
      archived_at:
        type:
          - string
          - 'null'
        format: date-time
        readOnly: true
unevaluatedProperties: false
```

<a id="clientcreaterequest"></a>
## ClientCreateRequest

```yaml
type: object
required:
  - display_name
properties:
  display_name:
    type: string
  email:
    type: string
    format: email
  company:
    type: string
  country:
    type: string
  notes:
    type: string
  metadata:
    type: object
    additionalProperties: true
additionalProperties: false
```

<a id="clientupdaterequest"></a>
## ClientUpdateRequest

```yaml
type: object
description: >-
  `null` clears an optional field to `NULL` on the row (C4-D8, the same convention as
  `allowed_rails: null` in the payment-link Settlement wizard, §51.13); an omitted key leaves the
  stored value unchanged. An empty string (`""`) is not the same as `null` — for `email` it fails
  the format check with `400 invalid_request`; for the other free-text fields it is trimmed and
  treated as `null` (unchanged pre-K3c behaviour). `display_name` stays a plain, non-nullable string
  when sent — it can be changed but never cleared.
properties:
  display_name:
    type: string
  email:
    type:
      - string
      - 'null'
    format: email
    description: '`null` clears the client''s email. An empty string fails the format check (`400`).'
  company:
    type:
      - string
      - 'null'
    description: '`null` clears the client''s company name.'
  country:
    type:
      - string
      - 'null'
    description: '`null` clears the client''s country.'
  notes:
    type:
      - string
      - 'null'
    description: '`null` clears the client''s notes.'
  metadata:
    type: object
    additionalProperties: true
additionalProperties: false
```

<a id="clientlist"></a>
## ClientList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Client'
unevaluatedProperties: false
```

<a id="product"></a>
## Product

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - name
      - unit_price
    properties:
      id:
        type: string
        pattern: ^prd_
      object:
        type: string
        enum:
          - product
      name:
        type: string
      description:
        type:
          - string
          - 'null'
      unit_price:
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
        description: >-
          `null` for a product saved with no fixed price (the merchant fills the amount in
          per-invoice at create time) — `pl_products.unit_price` is a nullable column; K3
          correction, the v1 draft required this and 500'd on a real row.
      currency:
        type: string
        description: >-
          ISO 4217 code or stablecoin symbol, carried alongside `unit_price` (RESOURCE-MODEL §2.1
          lists both `unit_price{}` and `currency` verbatim as sibling fields).
      metadata:
        type: object
        additionalProperties: true
      archived_at:
        type:
          - string
          - 'null'
        format: date-time
        readOnly: true
unevaluatedProperties: false
```

<a id="productcreaterequest"></a>
## ProductCreateRequest

```yaml
type: object
required:
  - name
  - unit_price
  - currency
properties:
  name:
    type: string
  unit_price:
    $ref: '#/components/schemas/MoneyPositive'
  currency:
    type: string
  description:
    type: string
  metadata:
    type: object
    additionalProperties: true
additionalProperties: false
```

<a id="productupdaterequest"></a>
## ProductUpdateRequest

```yaml
type: object
description: >-
  `null` on `description` clears it to `NULL` (C4-D8, same convention as `ClientUpdateRequest` and
  `allowed_rails: null`, §51.13); an omitted key leaves it unchanged. `name`, `unit_price` and
  `currency` are not part of this convention — they identify or price the product, not describe it,
  and stay non-nullable when sent.
properties:
  name:
    type: string
  unit_price:
    $ref: '#/components/schemas/MoneyPositive'
  currency:
    type: string
  description:
    type:
      - string
      - 'null'
    description: >-
      `null` clears the product's description. An empty string is trimmed and treated as `null`
      (unchanged pre-K3c behaviour, no format validator on this field).
  metadata:
    type: object
    additionalProperties: true
additionalProperties: false
```

<a id="productlist"></a>
## ProductList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Product'
unevaluatedProperties: false
```

<a id="settlementasset"></a>
## SettlementAsset

```yaml
type: object
description: >-
  BL-39 (C4-D27, phase-V parity CPD-2/CPD-3) — the settlement token identity, frozen once from
  `settlement_snapshot` at link ACTIVATION and never recomputed per attempt or from a live chain
  read. Shared by `Payment.settlement_asset` and `PaymentLink.settlement_asset` so a
  crypto-processing screen has exactly ONE token name to print for a given link, instead of guessing
  it independently from `deposit_instructions`, a hardcoded literal, or a raw internal currency code
  (the source of CPD-3's three-names-on-one-screen defect). Carries no amount —
  `amount_expected`/`fee`/`amount_received` (`Money`) remain the only figures; this only labels
  their existing numerals with the token actually sent.
required:
  - symbol
  - decimals
  - network
properties:
  symbol:
    type: string
    description: >-
      Canonical display ticker for the frozen network — `USDC.e` on Tempo mainnet, `pathUSD` on
      Moderato.
    example: USDC.e
  decimals:
    type: integer
    minimum: 0
    maximum: 18
    example: 6
  network:
    type: string
    enum:
      - tempo
    description: >-
      The settlement rail, not the concrete Tempo network — Moderato vs Mainnet is
      `DepositInstructions.chain_id`, never re-derived from this label.
additionalProperties: false
```

<a id="paymentstatus"></a>
## PaymentStatus

```yaml
type: string
description: >-
  Cross-product pay-in lifecycle (RESOURCE-MODEL §2.1 v2 amendment, 12 states). `processing` is
  still planned (Bridge-rail only, R19 X4). `underpaid` / `overpaid` / `unmatched` are AVAILABLE
  (CP-T3, §52.37 item 4 — RESOURCE-MODEL CP-G5's producer): the Tempo watcher
  (`crypto_tempo`/`crypto_relay`) writes them directly onto `payment_link_attempts.status` — a
  deposit short of the frozen total is `underpaid` (stays live: a later top-up can still complete
  it, settling `paid` with the SUM of every leg received, never just the last one); a match ABOVE
  the total settles the merchant's exact invoice as before and is `overpaid`, `surplus` published
  (paid to the Swaps fee wallet together with the fee, never auto-refunded — see `Payment.surplus`);
  a deposit to a closed, unpaid payment's address (expired or abandoned) makes that payment
  `unmatched`, `amount_received` = what was observed on it; a merchant cancel of an `underpaid`
  payment makes it `unmatched` too, never `abandoned` — the money stays held and its running total
  stays `amount_received`; `Payment.unmatched_reason` names which of these two made it
  (`deposit_after_close` / `partial_before_cancel`); a further deposit to an `unmatched` or
  already-paid payment's address never changes its status and is recorded for support, not published
  on `/v1`; a leg in a different accepted token than an `underpaid` payment's running total is never
  summed into it and is recorded for support; a deposit matching no payment at all is recorded for
  support. Events: `payment.underpaid` / `payment.overpaid` are live on TWO independent producers —
  the Bridge pay-in reducer (below) AND, as of CP-T3, the Tempo watcher, each firing at most once
  per payment on its own rail. The Bridge pay-in reducer emits one per payment when Bridge reports
  the received amount in the link's own currency and it falls outside ±1 % of the link amount;
  `data.object` carries both figures as `amount_received` and `amount_expected` (`Money`, link
  currency for the Bridge producer; the observed Tempo stablecoin for the watcher's own producer —
  crypto_tempo has no FX leg to convert either figure into). No verdict is published for a pay-in in
  another currency or on an FX-estimated figure. On the Bridge rails ONLY, `payment.paid` (and
  `payment.settled`) also fire for a short payment and come first, and `data.object.status` keeps
  reading `paid`/`settled` while the link stays `processing`: a `payment.underpaid` on the same
  `pay_` id overrides them — do not fulfil on `payment.paid` alone. On crypto_tempo/crypto_relay
  this never happens: an underpaid deposit never reaches `funds_received` at all until it is topped
  up. `payment.marked_sent` is live — the payer's non-authoritative "I have sent it" on /pay,
  `status` still `awaiting`. Still catalogued, with the missing observation: `payment.detected` —
  Bridge delivers no pre-arrival state on the rails Payment links use (`funds_scheduled` is
  ACH-only, collection accounts are GBP/EUR) and the Tempo watcher settles on first sight;
  `payment.processing` — Bridge's `payment_submitted` follows `funds_received` (already `paid`) and
  `in_review` is only recorded on the link timeline, so no pre-paid processing step is observed.
  `payment.unmatched` is live (CP-T3, crypto_tempo/crypto_relay) once per payment, ONLY for a
  deposit to a closed, unpaid payment's address or a merchant cancel of an `underpaid` payment, its
  `data.object.unmatched_reason` naming which; a further deposit to an unmatched or paid payment and
  a deposit matching no payment are recorded for support and never published on `/v1`; a Bridge
  collection-account deposit with no match is still held in the operator ledger with no Payment to
  carry it (that half stays unproduced). A1-2 fixer round 1 (finding #2): this is the CLOSED,
  request-side form — `payments.list`'s `status` query filter uses it directly, and an unrecognized
  value there is a real `400`. `PaymentStatusOpen` (below) is the exact same list, response-side
  only (`Payment.status`, `PaymentAttemptView.status`, `PaymentLink.latest_attempt_status`); round 1
  marked this schema directly instead, which leaked the open behaviour onto the query filter too
  (CONVENTIONS.md: mark the specific field, never the vocabulary in general).
enum:
  - awaiting
  - detected
  - processing
  - paid
  - settled
  - underpaid
  - overpaid
  - unmatched
  - expired
  - returned
  - failed
  - abandoned
```

<a id="paymentstatusopen"></a>
## PaymentStatusOpen

```yaml
type: string
x-swaps-open-enum: true
description: >-
  The exact same 12-state lifecycle as `PaymentStatus` above, kept as its own schema so the
  open-enum marker never reaches the `payments.list` `status` query filter (A1-2 fixer round 1,
  finding #2). Response-only — an unrecognized member here is an opaque string, never a
  deserialization failure. See `PaymentStatus` above for the state descriptions.
enum:
  - awaiting
  - detected
  - processing
  - paid
  - settled
  - underpaid
  - overpaid
  - unmatched
  - expired
  - returned
  - failed
  - abandoned
```

<a id="paymentunmatchedreason"></a>
## PaymentUnmatchedReason

```yaml
type: string
x-swaps-open-enum: true
description: >-
  Why a payment is `unmatched` (CP-T4-T, crypto_tempo/crypto_relay): one code per way a payment
  becomes unmatched. `partial_before_cancel` — the payer had already sent part of the amount (the
  payment was `underpaid`) when the merchant cancelled the link; that partial amount is
  `amount_received`. `deposit_after_close` — money was first observed at the payment's settlement
  address after the payment had closed without being paid (for example it expired, or the link was
  cancelled before any money was observed); what arrived is `amount_received`. It may have been sent
  shortly before the close: the watcher polls, and a `crypto_relay` payment can still be bridging —
  this is the observation time, not the chain time. Either way the funds are held: nothing is
  released or refunded automatically, support handles them (CP-G6). Response-only and open: a value
  added within `/v1` is additive — treat one you do not recognise like `null` (neutral copy).
enum:
  - partial_before_cancel
  - deposit_after_close
```

<a id="paymentsessionamountverdict"></a>
## PaymentSessionAmountVerdict

```yaml
type: string
x-swaps-open-enum: true
description: >-
  PAY-VERDICT-2-T — the amount verdict a payer session publishes (`PaymentSession.amount_verdict`).
  `exact`: the total due arrived, no more and no less. `overpaid`: more than the total arrived; the
  merchant still received exactly the invoice and the surplus is paid to the Swaps fee wallet
  together with the fee (see `Payment.surplus`). `underpaid`: less than the total arrived and the
  request is not complete. `unmatched`: money arrived on a payment that had already closed without
  being paid (see `Payment.unmatched_reason`) and is held. Response-only and open: a value added
  within `/v1` is additive — treat one you do not recognise like `null`.
enum:
  - exact
  - underpaid
  - overpaid
  - unmatched
```

<a id="paymentscreening"></a>
## PaymentScreening

```yaml
type: object
description: >-
  RESOURCE-MODEL §2.1 v2 amendment. `not_screened` is a legitimate value, not an absent field (R19
  CP-G13) — a payment that exists but was never run through the guard action reads `not_screened`,
  distinct from the field being missing.
properties:
  status:
    type: string
    enum:
      - not_screened
      - screened
  level:
    type:
      - string
      - 'null'
    enum:
      - low
      - medium
      - high
      - critical
      - unknown
      - null
  flags:
    type: array
    items:
      type: string
unevaluatedProperties: false
```

<a id="payment"></a>
## Payment

```yaml
description: >-
  A cross-product pay-in — a payment-link attempt or a crypto-processing attempt (RESOURCE-MODEL
  §2.1 v2 amendment, promoted to a top-level resource; R19 §2.2). No create and no refund: only a
  payer session creates one, and Swaps never initiates a refund (RESOURCE-MODEL §0.10).
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - status
      - payment_link_id
      - subscription_id
      - source_currency
      - source_chain
      - source_asset
      - source_address
      - deposit_address
      - deposit_instructions
      - bank_deposit_instructions
      - payer_type
      - payer_marked_sent_at
      - return_reason
      - settlement_tx_hash
      - onchain_tx_hash
      - settlement_asset
      - amount_expected
      - amount_received
      - fee
      - deducted_fee
      - net_amount
      - surplus
      - amount_missing
      - unmatched_reason
      - screening
      - expires_at
    properties:
      id:
        type: string
        pattern: ^pay_
      object:
        type: string
        enum:
          - payment
      payment_link_id:
        type:
          - string
          - 'null'
        pattern: ^pl_
      subscription_id:
        type:
          - string
          - 'null'
        pattern: ^sub_
        description: >-
          Set when this pay-in settles a subscription invoice rather than a standalone link
          (RESOURCE-MODEL §2.1 v2 amendment — `/v1/payments` filters on `payment_link_id`,
          `subscription_id`, `settlement`).
      status:
        $ref: '#/components/schemas/PaymentStatusOpen'
      payment_rail:
        type: string
        enum:
          - ach
          - wire
          - fednow
          - sepa
          - faster_payments
          - pix
          - spei
          - crypto_tempo
          - crypto_bridge
          - crypto_relay
        description: >-
          `crypto_relay` (R19 X2): the payer sends USDC on another EVM network to a Relay deposit
          address whose recipient is this attempt's splitter; live behind the `crypto_relay_rail`
          flag, off in production.
      source_currency:
        type:
          - string
          - 'null'
      source_chain:
        type:
          - string
          - 'null'
        enum:
          - tempo
          - base
          - ethereum
          - polygon
          - arbitrum
          - optimism
          - solana
          - null
        description: >-
          The one chain field (CMP-8) — the v2 crypto-extension's `source_network` duplicate is
          removed; this carries the `payment_sessions.network` vocabulary for every crypto rail.
      source_asset:
        type:
          - string
          - 'null'
      source_address:
        type:
          - string
          - 'null'
        description: Masked (RESOURCE-MODEL §2.1 v2 amendment).
      deposit_address:
        type:
          - string
          - 'null'
        description: >-
          The splitter address, case-preserved verbatim — never lowercased (AGENTS.md address-case
          rule).
      deposit_instructions:
        description: >-
          Where to send crypto for this attempt (MON-7, CMP-5) — present only for a crypto rail;
          null on a bank rail (`bank_deposit_instructions` below is set instead). An `underpaid`
          `crypto_tempo` payment with a recorded running total names only the token that total
          counts, or is `null` when that token cannot be told apart.
        oneOf:
          - $ref: '#/components/schemas/DepositInstructions'
          - type: 'null'
      bank_deposit_instructions:
        description: >-
          The Bridge-issued bank transfer target for this attempt (K3b) — present only for one of
          the seven bank rails (`ach`, `wire`, `fednow`, `sepa`, `faster_payments`, `pix`, `spei`);
          null on a crypto rail (`deposit_instructions` above is set instead). Exactly one of the
          two is ever non-null. See `BankDepositInstructions.holder_kind` for who the beneficiary
          actually is on this attempt.
        oneOf:
          - $ref: '#/components/schemas/BankDepositInstructions'
          - type: 'null'
      payer_type:
        type:
          - string
          - 'null'
        enum:
          - business
          - individual
          - null
      payer_marked_sent_at:
        type:
          - string
          - 'null'
        format: date-time
        description: A payer self-report, never authoritative on its own.
      return_reason:
        type:
          - string
          - 'null'
        description: Raw code from the returns dictionary (R11 §3); the client renders the display label.
      settlement_tx_hash:
        type:
          - string
          - 'null'
      onchain_tx_hash:
        type:
          - string
          - 'null'
        description: Crypto rails only (RESOURCE-MODEL §2.1 v2 amendment; R11 PL-G7).
      settlement_asset:
        readOnly: true
        description: >-
          BL-39 (C4-D27) — this attempt's parent link's token identity, frozen from
          `settlement_snapshot` at ACTIVATION (never a live chain read, never derived from a rate,
          never recomputed per attempt). `null` for anything that is not a Tempo crypto-only
          settlement.
        oneOf:
          - $ref: '#/components/schemas/SettlementAsset'
          - type: 'null'
      amount_expected:
        description: >-
          The ask — what this attempt requires the payer to send, fixed at creation and unaffected
          by what has actually arrived (MON-8; RESOURCE-MODEL §2.1 v2 amendment; R11 PL-G2).
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
      amount_received:
        description: >-
          The observation — what has actually been detected on-chain or at the rail so far. Null
          before `status=detected`; once populated it never goes back to null (MON-8). On
          `unmatched` (crypto_tempo/crypto_relay): everything observed up to the moment the payment
          became unmatched; later deposits to its address are recorded for support only.
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
      fee:
        description: >-
          The Swaps fee on `crypto_tempo`/`crypto_relay`, payer-borne on top of the invoice, at the
          settlement token's scale (the deposit instructions' figure; `amount_expected` includes
          it); `null` on every other rail, Bridge-routed rails included — the fee those rails deduct
          from what arrives is `deducted_fee`.
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
      deducted_fee:
        description: >-
          The Swaps fee deducted from what arrived on a Bridge-routed rail (bank rails,
          `crypto_bridge`) — the provider receipt's developer fee, recorded when the payment
          settled, in the invoice currency, rounded up to the currency's scale; the merchant bears
          it (`net_amount` is what arrived minus it). `null` before the payment is settled (and
          again if it is later returned); on `crypto_tempo`/`crypto_relay` (their fee is `fee`, paid
          on top); when the payer paid in another currency than the invoice (a receipt is never
          converted — the USDC source of `crypto_bridge` included); and while no consistent receipt
          is recorded (none sent, terms that do not add up, a provider exchange or gas fee on top).
          The configured rate is `Capabilities.deducted_fee`.
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
      net_amount:
        description: >-
          What the merchant receives, once the payment is settled; `null` before. On
          `crypto_tempo`/`crypto_relay` the invoice amount (the fee is paid on top). On a
          Bridge-routed rail only the provider receipt's figure — what arrived minus `deducted_fee`,
          in the invoice currency, before any conversion to the settlement asset, rounded down to
          the currency's scale — and `null` whenever `deducted_fee` is `null` for a receipt reason;
          never copied from the invoice there.
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
      surplus:
        description: >-
          Present only when `status=overpaid` (RESOURCE-MODEL §2.1 v2 amendment; live, CP-T3/CP-R4 —
          the Tempo watcher's own producer, crypto_tempo/crypto_relay only). `amount_received` minus
          the frozen total the deposit instructions quoted, denominated in the observed stablecoin.
          Paid to the Swaps fee wallet together with the fee when the payment settles; returned to
          the sending address, minus the network fee, only on request through support (manual, from
          the fee wallet; in force after the legal sign-off on CP-G6, founder §52.60 54A); never
          auto-refunded. The merchant's own release is untouched: they still receive exactly their
          invoice regardless of this figure.
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
      amount_missing:
        description: >-
          Present only when `status=underpaid` (live, CP-T3/CP-R4 — crypto_tempo/crypto_relay only):
          the frozen total minus everything observed toward this attempt SO FAR (every deposit leg
          summed, not just the latest one). A later top-up that completes the payment clears this
          back to `null` and settles `paid` with the sum. `null` rather than a zero or negative
          figure, or one across two token scales.
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
      unmatched_reason:
        description: >-
          Present only when `status=unmatched` (CP-T4-T): which of the two paths made the payment
          unmatched — see `PaymentUnmatchedReason`. `null` on every other status, and on a payment
          that became `unmatched` before this field existed (no reason was recorded: show neutral
          copy, never a guessed reason).
        oneOf:
          - $ref: '#/components/schemas/PaymentUnmatchedReason'
          - type: 'null'
      screening:
        $ref: '#/components/schemas/PaymentScreening'
      expires_at:
        type:
          - string
          - 'null'
        format: date-time
        description: >-
          `crypto_relay`: the send-by time of its Relay quote (the `quote_expires_at` of its deposit
          instructions). `null` on every other rail today.
unevaluatedProperties: false
```

<a id="paymentlist"></a>
## PaymentList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Payment'
      summary:
        $ref: '#/components/schemas/PaymentListSummary'
unevaluatedProperties: false
```

<a id="paymentlistsummary"></a>
## PaymentListSummary

```yaml
type: object
x-swaps-since: '2026-09-25'
description: >-
  Present only with `?expand=summary` (LIST-SUMMARY-1). Computed over every pay-in matching the
  request's `payment_link_id`, `subscription_id`, `settlement` and `status` (never
  `cursor`/`limit`). `by_status` has one key per `PaymentStatus` value; a status this version does
  not know counts in `total` only.
required:
  - total
  - needs_attention
  - by_status
properties:
  total:
    type: integer
    minimum: 0
  needs_attention:
    type: integer
    minimum: 0
    description: >-
      `underpaid` + `overpaid` + `unmatched`, server-computed over the same rows the list already
      loaded (C-2, §52.37 item 4). Live for `crypto_tempo`/`crypto_relay` pay-ins (CP-T3 — the Tempo
      watcher is the producer of all three statuses; `unmatched` can also come from a merchant
      cancel of an `underpaid` payment); a Bridge-rail short/over payment is still visible only
      through the `payment.underpaid`/`.overpaid` EVENTS (its `Payment.status` stays
      `paid`/`settled` per `PaymentStatus`'s own note), so a nonzero count here is not the complete
      set of every pay-in a merchant might want to review — subscribe to those events too.
  by_status:
    type: object
    required:
      - awaiting
      - detected
      - processing
      - paid
      - settled
      - underpaid
      - overpaid
      - unmatched
      - expired
      - returned
      - failed
      - abandoned
    properties:
      awaiting:
        type: integer
        minimum: 0
      detected:
        type: integer
        minimum: 0
      processing:
        type: integer
        minimum: 0
      paid:
        type: integer
        minimum: 0
      settled:
        type: integer
        minimum: 0
      underpaid:
        type: integer
        minimum: 0
      overpaid:
        type: integer
        minimum: 0
      unmatched:
        type: integer
        minimum: 0
      expired:
        type: integer
        minimum: 0
      returned:
        type: integer
        minimum: 0
      failed:
        type: integer
        minimum: 0
      abandoned:
        type: integer
        minimum: 0
    additionalProperties: false
additionalProperties: false
```

<a id="paymenteventlist"></a>
## PaymentEventList

```yaml
description: >
  A view of `/v1/events` scoped to one payment (RESOURCE-MODEL §2.1 — mirrors
  `PaymentLinkEventList`/ `PayoutEventList` exactly, BL-33). Items share the global event envelope:
  `type` is one of the `payment.*` event names in RESOURCE-MODEL §3 (`payment.created`, `.awaiting`,
  `.paid`, `.settled`, `.expired`), and `data.object` carries this payment's own allowlisted
  projection (`buildOutboxPaymentAttemptEventData` — no `deposit_instructions`, no payer PII, no
  Bridge transfer id).
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Event'
unevaluatedProperties: false
```

<a id="paymentsessionpayment"></a>
## PaymentSessionPayment

```yaml
type: object
description: >-
  The payer's projection of a payment attempt (SEC-2) — returned by every `payment_sessions/*`
  operation that starts or reads a payment. Never carries `screening`, `source_address`, or any
  merchant-only field; the full object is served only at `/v1/payments*` as `Payment`.
required:
  - id
  - object
  - status
  - created_at
properties:
  id:
    type: string
    pattern: ^pay_
  object:
    type: string
    enum:
      - payment
  status:
    $ref: '#/components/schemas/PaymentStatusOpen'
  payment_rail:
    type: string
    enum:
      - ach
      - wire
      - fednow
      - sepa
      - faster_payments
      - pix
      - spei
      - crypto_tempo
      - crypto_bridge
      - crypto_relay
    description: >-
      `crypto_relay` (R19 X2): the payer sends USDC on another EVM network to a Relay deposit
      address whose recipient is this attempt's splitter; live behind the `crypto_relay_rail` flag,
      off in production.
  source_chain:
    type:
      - string
      - 'null'
    enum:
      - tempo
      - base
      - ethereum
      - polygon
      - arbitrum
      - optimism
      - solana
      - null
  stablecoin:
    type:
      - string
      - 'null'
    enum:
      - USDC.e
      - pathUSD
      - USDT0
      - USD1
      - cUSD
      - USDC
      - null
    description: >-
      The stablecoin the payer sends. `USDC` (native Circle USDC on `source_chain`) on a
      `crypto_relay` attempt; `null` on every other rail today.
  source_asset:
    type:
      - string
      - 'null'
  deposit_instructions:
    description: >-
      Where to send crypto for this attempt (MON-7, CMP-5) — present only for a crypto rail; null on
      a bank rail (`bank_deposit_instructions` below is set instead). An `underpaid` `crypto_tempo`
      payment with a recorded running total names only the token that total counts (as
      `PaymentSession.underpaid_payment` does), or is `null` when that token cannot be told apart.
      `null` on every rail while the link takes no new money for its settlement destination
      (`PaymentSession.accepting_payments: false`; `payment_sessions.payments.get`).
    oneOf:
      - $ref: '#/components/schemas/DepositInstructions'
      - type: 'null'
  bank_deposit_instructions:
    description: >-
      The Bridge-issued bank transfer target for this attempt (K3b) — present only for one of the
      seven bank rails; null on a crypto rail (`deposit_instructions` above is set instead). At most
      one of the two is ever non-null, and both are `null` while the link takes no new money for its
      settlement destination (`PaymentSession.accepting_payments: false`). See
      `BankDepositInstructions.holder_kind` for who the beneficiary actually is.
    oneOf:
      - $ref: '#/components/schemas/BankDepositInstructions'
      - type: 'null'
  amount_expected:
    description: The ask — what this attempt requires the payer to send (MON-8).
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  amount_received:
    description: >-
      The observation — what has actually been detected so far. Null before `status=detected`
      (MON-8).
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  surplus:
    description: >-
      Same value and gating as `Payment.surplus`: present only when `status=overpaid`
      (crypto_tempo/crypto_relay), `amount_received` minus the frozen total, in the observed
      stablecoin. Paid to the Swaps fee wallet together with the fee when the payment settles;
      returned to the sending address, minus the network fee, only on request through support
      (manual, from the fee wallet; in force after the legal sign-off on CP-G6, founder §52.60 54A);
      never auto-refunded.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  amount_missing:
    description: >-
      Same value and gating as `Payment.amount_missing`: present only when `status=underpaid`
      (crypto_tempo/crypto_relay), the frozen total minus every deposit leg observed so far. The
      same figure as `PaymentSession.underpaid_payment.amount_missing` for this payment.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  unmatched_reason:
    description: >-
      Same value and gating as `Payment.unmatched_reason`: present only when `status=unmatched`,
      which of the two paths made it (`PaymentUnmatchedReason`); `null` otherwise and on a payment
      made unmatched before the field existed.
    oneOf:
      - $ref: '#/components/schemas/PaymentUnmatchedReason'
      - type: 'null'
  fee:
    description: >-
      The Swaps fee on `crypto_tempo`/`crypto_relay`, payer-borne on top of the invoice, at the
      settlement token's scale (the deposit instructions' figure; `amount_expected` includes it);
      `null` on every other rail, Bridge-routed rails included.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  expires_at:
    type:
      - string
      - 'null'
    format: date-time
    description: >-
      `crypto_relay`: the send-by time of its Relay quote (the `quote_expires_at` of its deposit
      instructions). `null` on every other rail today.
  watcher_state:
    $ref: '#/components/schemas/PaymentWatcherState'
  watcher_reason:
    $ref: '#/components/schemas/PaymentWatcherReason'
  settlement_tx_hash:
    type:
      - string
      - 'null'
  payer_marked_sent_at:
    type:
      - string
      - 'null'
    format: date-time
    description: >-
      The payer's own non-authoritative "I've sent it" self-report (`POST .../mark_sent`) — never
      settlement, `status` stays the sole source of truth (RESOURCE-MODEL §3,
      `authoritative:false`).
  created_at:
    type: string
    format: date-time
unevaluatedProperties: false
```

<a id="paymentlinkeventlist"></a>
## PaymentLinkEventList

```yaml
description: >-
  A view of `/v1/events` scoped to one link (RESOURCE-MODEL §2.1, proposed). Shares the global event
  envelope (CMP-7) — no bespoke per-link event shape.
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Event'
unevaluatedProperties: false
```

<a id="subscription"></a>
## Subscription

```yaml
description: >-
  Version 1 scheduled invoices — a crypto invoice reissued on a schedule, never an authorised pull
  (RESOURCE-MODEL §2.6; R19 §2.3). Dark-flag (PRD-CP-001 slice X5) — gated behind
  `api_v1.subscriptions`, off in production pending legal sign-off (G-8, MiCA/crypto-processing).
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - status
      - kind
      - name
      - amount
      - interval
    properties:
      id:
        type: string
        pattern: ^sub_
      object:
        type: string
        enum:
          - subscription
      kind:
        type: string
        enum:
          - scheduled_invoices
          - authorised_pull
        description: >-
          Only `scheduled_invoices` is buildable for v1. `authorised_pull` names the v2 shape
          (`SwapsSubscription` contract) so the collision cannot be claimed by accident — it stays
          behind the founder-gated third-party audit (PRD §11.6) and has no producer.
      status:
        type: string
        enum:
          - active
          - paused
          - cancelled
        description: >-
          Stored subscription lifecycle; only merchant actions change it. An overdue invoice does
          not pause or cancel the subscription.
      overdue:
        type: boolean
        readOnly: true
        description: >-
          NB-2 — derived: this subscription has at least one invoice that is `open` and past its
          `due_at`. Never stored, computed fresh from the invoice table at read time (one query per
          page, never N+1) — the same zero-grace rule `SubscriptionInvoice.overdue` uses. `status`
          itself never flips to reflect a missed payment (D-K13-6); this is the field that says so
          instead. Present on every `/v1` read of this object (`list`, `get`, `create`, `pause`,
          `resume`, `cancel`) and ABSENT from `subscription.*` event and webhook payloads, which
          describe the transition that happened rather than a live invoice poll — read
          `subscriptions.get` for the current value.
      name:
        type: string
      amount:
        $ref: '#/components/schemas/Money'
      interval:
        type: string
        enum:
          - monthly
          - quarterly
          - yearly
      first_due_at:
        type: string
        format: date-time
      next_due_at:
        type:
          - string
          - 'null'
        format: date-time
      client_id:
        type:
          - string
          - 'null'
        pattern: ^cli_
      settlement:
        type: object
        additionalProperties: true
        description: >-
          The merchant's settlement destination — always the Swaps Wallet for v1 (RESOURCE-MODEL
          §2.6). Sub-shape not further enumerated there.
      reference:
        type: string
        description: '`SUB-00nn` (RESOURCE-MODEL §2.6).'
      paused_at:
        type:
          - string
          - 'null'
        format: date-time
        readOnly: true
        description: >-
          C-55 (§52.37 item 4) — the instant `pause` set it; `resume` clears it back to `null`.
          `null` on an `active` or `cancelled` subscription, or one never paused. Present on every
          `/v1` read (`list`, `get`, `create`, `pause`, `resume`, `cancel`) — the same
          producer/consumer pairing as `overdue` above.
      reminder_schedule:
        $ref: '#/components/schemas/ReminderSchedule'
        description: >-
          Always off in v1: empty offsets_days and sent, and null next_reminder_at. The DB has an
          inert placeholder column, but the public create request accepts no reminder_schedule and
          reads do not expose a stored custom schedule. No crypto subscription reminders are
          delivered.
unevaluatedProperties: false
```

<a id="subscriptioncreaterequest"></a>
## SubscriptionCreateRequest

```yaml
type: object
description: >-
  Dark-flag (X5). `attestation_accepted` mirrors the payment-link activation gate — R19 §1 shows the
  creation wizard behind the same attestation checkbox.
required:
  - name
  - amount
  - interval
  - client_id
  - attestation_accepted
properties:
  name:
    type: string
  amount:
    $ref: '#/components/schemas/MoneyPositive'
  interval:
    type: string
    enum:
      - monthly
      - quarterly
      - yearly
  first_due_at:
    type: string
    format: date-time
  client_id:
    type: string
    pattern: ^cli_
  attestation_accepted:
    type: boolean
additionalProperties: false
```

<a id="subscriptionlist"></a>
## SubscriptionList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Subscription'
      summary:
        $ref: '#/components/schemas/SubscriptionListSummary'
unevaluatedProperties: false
```

<a id="subscriptionlistsummary"></a>
## SubscriptionListSummary

```yaml
type: object
x-swaps-since: '2026-09-25'
description: >-
  Present only with `?expand=summary` (LIST-SUMMARY-1). Computed over every subscription of this
  account matching the request's `status` (never `cursor`/`limit`), in one grouped query.
required:
  - total
  - active
  - paused
  - next_due
properties:
  total:
    type: integer
    minimum: 0
  active:
    type: integer
    minimum: 0
  paused:
    type: integer
    minimum: 0
  next_due:
    description: >-
      The earliest `next_due_at` among the matching `active` subscriptions (ties broken by id);
      `null` when none is active or none has a next due date.
    oneOf:
      - type: 'null'
      - type: object
        required:
          - at
          - subscription_id
        properties:
          at:
            type: string
            format: date-time
          subscription_id:
            type: string
            pattern: ^sub_
        additionalProperties: false
additionalProperties: false
```

<a id="subscriptioninvoice"></a>
## SubscriptionInvoice

```yaml
description: >-
  One child invoice of a subscription — issued on its due date, never earlier (RESOURCE-MODEL §2.6;
  R19 §2.3). No create: the emitter is a cron, not a caller.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - subscription_id
      - sequence
      - due_at
      - status
    properties:
      id:
        type: string
        pattern: ^inv_
      object:
        type: string
        enum:
          - subscription_invoice
      subscription_id:
        type: string
        pattern: ^sub_
      sequence:
        type: integer
        description: 1, 2, 3… (Invoice 1, Invoice 2…).
      due_at:
        type: string
        format: date-time
      issued_at:
        type:
          - string
          - 'null'
        format: date-time
        description: Set on the due date, never earlier.
      payment_link_id:
        type:
          - string
          - 'null'
        pattern: ^pl_
        description: Nullable until issued.
      status:
        type: string
        enum:
          - not_issued
          - open
          - paid
      overdue:
        type: boolean
        readOnly: true
        description: >-
          Derived: past due and unpaid. Never stored (RESOURCE-MODEL §2.6) — a clock skew cannot
          desync it from the invoice's real status.
unevaluatedProperties: false
```

<a id="subscriptioninvoicelist"></a>
## SubscriptionInvoiceList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/SubscriptionInvoice'
unevaluatedProperties: false
```

<a id="paymentsession"></a>
## PaymentSession

```yaml
type: object
description: >-
  The payer's projection of a payment link (RESOURCE-MODEL §2.1; §2.1 invariants). Never carries
  `merchant_user_id`, `settlement_snapshot`, `bridge_*`, `payer_ip_hash`, the merchant's login
  e-mail, a payment id (not a secret from a link holder, see `pending_payment`), or any payer's data
  — the projection is the schema, not a filter applied afterwards. The one set of deposit
  instructions it carries is `pending_payment` (amended 2026-09-29, PAY-WAIT-1-T): the receiving
  side of the link's live payment, never the payer's side, never another payment's; for a live
  `crypto_tempo` payment that arrived short it is `underpaid_payment` instead (PAY-WAIT-2-T,
  2026-09-29), and at most one of the two is non-null. `client_display_name` (§52 C4-D5) is the one
  exception to "merchant-internal stays withheld": it is the payer's OWN display name as the
  merchant's client of record, not merchant data. `merchant_contact` (L4-9) is a second, narrower
  exception: an EXPLICIT opt-in the merchant sets for this purpose (`Account.support_contact`) —
  never the login e-mail, never derived from KYC/Bridge/settlement data, `null` until set — SEC-2
  governs what is withheld from the payer and is amended to say so explicitly.
required:
  - merchant_display_name
  - amount
  - status
  - pending_payment
  - underpaid_payment
  - unmatched_reason
properties:
  merchant_display_name:
    type: string
  client_display_name:
    type:
      - string
      - 'null'
    description: >-
      The payer's own name as saved on the merchant's client record for this link (§52 C4-D5) — e.g.
      "Marin & Co." rendered as "Website deposit · Marin & Co." `null` when the link has no client
      of record attached. Distinct from `merchant_display_name` (whose money this is) and from
      `title` (the merchant's free-text label for the link).
  title:
    type:
      - string
      - 'null'
  memo:
    type:
      - string
      - 'null'
    description: Rendered as "Description" on the payer surfaces (R19 CP-G15).
  invoice_number:
    type:
      - string
      - 'null'
  items:
    type: array
    items:
      $ref: '#/components/schemas/PaymentLinkItem'
  amount:
    $ref: '#/components/schemas/Money'
  expected_payer_type:
    type: string
    enum:
      - any
      - business
      - individual
  rails:
    type: array
    description: >-
      Exactly the rails `POST .../payments` will actually accept for this link today (K3b: bank
      rails are back, now that `bank_deposit_instructions` exists — the P1-4b gap this note used to
      describe is closed). `crypto_relay` is listed only while the `crypto_relay_rail` flag admits
      the merchant (off in production), the link settles on Tempo mainnet in USD, the invoice is
      within the rail cap and at least one source network has an admitted Relay route
      (`founder_accepted` per founder §52.34, or `proven`; see K13-CRYPTO-RELAY-ROUTES.md); its
      `networks` names those networks.
    items:
      type: object
      properties:
        rail:
          type: string
          enum:
            - ach
            - wire
            - fednow
            - sepa
            - faster_payments
            - pix
            - spei
            - crypto_tempo
            - crypto_bridge
            - crypto_relay
        eta_seconds:
          type:
            - integer
            - 'null'
          description: >-
            Added in the v2 amendment. Previously nulled before reaching the client for want of
            localized copy (R11 PL-G9) — now published as a raw value.
        networks:
          type: array
          description: >-
            `crypto_relay` only — the source networks with an admitted Relay route
            (`founder_accepted` per founder §52.34, or `proven`; see K13-CRYPTO-RELAY-ROUTES.md)
            into Tempo USDC.e. `POST .../payments` refuses any other `network` for this rail.
          items:
            type: string
            enum:
              - base
              - ethereum
              - polygon
              - arbitrum
              - optimism
  unavailable_rails:
    type: array
    description: >-
      PL-UNAVAIL-RAILS — rails this link could offer in principle (its currency's rails, the
      merchant's enabled rails, narrowed by `allowed_rails`, plus both crypto rails) that THIS
      session may not pick, so a payer page can show them muted with an honest reason instead of
      hiding them. Computed from the same eligibility inputs as `rails[]` and never overlaps it;
      `POST .../payments` refuses every rail listed here with `rail_not_allowed`. Payer-dependent
      refusals (the individual-payer caps, which depend on the declared `payer_type`) are not
      listed: those rails stay in `rails[]` and are refused at payment time.
    items:
      $ref: '#/components/schemas/PaymentSessionUnavailableRail'
  status:
    type: string
    enum:
      - active
      - viewed
      - processing
      - paid
      - settled
      - expired
      - cancelled
      - refunded
    description: >-
      The live link status published verbatim, plus `refunded` (v2 amendment). `link_not_payable`,
      `link_expired` and `not_found` are errors, never states.
  accepting_payments:
    type: boolean
    description: >-
      Wallet phase 0 item 0.7 — whether this link takes a payment now: `true` exactly when `POST
      .../payments` would get past the link's own gates (it is not past `expires_at`, its `status`
      is `active`, `viewed` or `processing`, and Swaps routes new money to its settlement
      destination); `false` otherwise, including when Swaps could not check the destination (fail
      closed). A yes/no only: it never says why, and no other field does either — read `status` for
      a paid, cancelled or expired link; for an open link, `false` means show that it is not taking
      payments now. While it is `false`, `pending_payment` and `underpaid_payment` are `null`; while
      the destination is the reason, `GET .../payments/{payment_id}` carries no instructions either.
      Payer-dependent refusals (a rail, the individual-payer caps) do not change it. Present on
      every response from WALLET-INBOUND-2 (wallet phase 0 item 0.7, PR-2) on; it becomes required
      in a follow-up once production api-v1 is verified to send it. Until then a client must read a
      missing key as unknown — never as `true` — and keep relying on `status` and on `POST
      .../payments` refusing with `link_not_payable`.
  expires_at:
    type:
      - string
      - 'null'
    format: date-time
  latest_attempt_status:
    oneOf:
      - $ref: '#/components/schemas/PaymentStatusOpen'
      - type: 'null'
  amount_verdict:
    description: >-
      PAY-VERDICT-2-T (#3922) — ONE amount verdict for this link, from the payment that settled it
      or holds its money, never from the newest payment: a newer payment on another rail never
      shadows it (`latest_attempt_status` is unchanged and still reads the newest payment). Swaps
      derives it from the amounts it recorded itself; never compute one from `amount_received` and
      `amount_expected`. crypto_tempo / crypto_relay: `exact` when the watcher observed exactly the
      total due (the invoice plus the 1% fee) and no other payment of this link holds money;
      `overpaid` when it observed more (stored together with the paid state, never a second write);
      `underpaid` while the running total is short; `unmatched` for money that reached a closed,
      unpaid payment. Bridge rails: `underpaid` only while Swaps holds a short payment open with its
      received amount recorded for that payment; a Bridge payment Swaps settled as paid reads
      `null`, never `exact`: the only received figure kept for it is an audit note compared without
      the 1% tolerance and possibly converted at an FX rate, not a verdict (its `payment.overpaid` /
      `payment.underpaid` events stay the Bridge signal). `null` whenever no verdict is provable:
      nothing has arrived; the observed amount was not recorded, or is still being recorded; more
      than one payment of the link completed; with no completed payment, payments that hold money
      under different verdicts; `exact` while any other money for this link is held (on another
      payment, on another payment's Relay leg, or a further deposit to this one). A settling
      payment's `overpaid` / `underpaid` is published even when another payment also holds money;
      only `exact` requires that none does. `null` never means `exact`.
    oneOf:
      - $ref: '#/components/schemas/PaymentSessionAmountVerdict'
      - type: 'null'
  unmatched_reason:
    description: >-
      Why held money is held, when amount_verdict is unmatched: the server's own classification.
      Clients draw it; they never derive order from other fields. PAY-HELD-REASON-1-T: the
      `unmatched_reason` of the payments behind that verdict (`PaymentUnmatchedReason`, the same
      value `GET .../payments/{payment_id}` publishes for such a payment): `partial_before_cancel`
      when part of the payment arrived before the request was cancelled, `deposit_after_close` when
      money was first observed after the payment closed unpaid. `null` whenever `amount_verdict` is
      not `unmatched`; when a payment behind it became `unmatched` before reasons were recorded;
      when the payments behind it carry different reasons; when the reason is
      `partial_before_cancel` but other money for this link is held beside the payment (for example
      a top-up sent after the cancel); and for a stored value this contract does not know (withheld
      and reported, never published). Show neutral copy for `null` and for a value you do not
      recognise. Only the code: never an amount, a payment id or a time. Always present, never
      omitted.
    oneOf:
      - $ref: '#/components/schemas/PaymentUnmatchedReason'
      - type: 'null'
  pending_payment:
    type:
      - object
      - 'null'
    description: >-
      PAY-WAIT-1-T (founder P1, 2026-09-29) — where to send the money while this link's live payment
      waits for it, so a payer who reopens the link in another browser, an in-app browser or a
      private window still sees the payment method they chose until its status changes. Present only
      while `status` is `processing`, the link has not expired, and its latest payment is
      `awaiting`, `detected` or `processing` with deposit instructions that are still usable (a
      `crypto_relay` address only until its send-by time and only while Relay has seen nothing; a
      memo-matched bank slip only with its reference). `null` in every other case: no payment, a
      failed, abandoned, expired or returned payment, a verdict (`underpaid`, `overpaid`,
      `unmatched`), a deposit Swaps already observed on that payment and is still recording
      (PAY-VERDICT-2-T), another payment's deposit still being recorded (a deposit Swaps observed on
      another open payment of this link may complete it, so the payer is never asked for money while
      it is being recorded; PAY-WAIT-3-T), money already on another payment of this link
      (`underpaid`, `unmatched`, completed, or a `crypto_relay` payment whose Relay leg saw funds),
      a paid, closed or expired link. Every payment link is a single-payment request (the first
      completed payment closes it to new payments; a subscription issues one link per invoice), so
      these are the receiving instructions of the ONE request the link is for: Swaps' or the
      provider's receiving account, address and reference plus the link's own amount, exactly what
      `GET .../payments/{payment_id}` shows for that payment. An exact, on-time payment with them
      completes that same request, whoever sends it. `crypto_relay`: send exactly
      `deposit_instructions.amount` on `deposit_instructions.chain` before `expires_at`; any Relay
      refund goes to the refund address given when this payment was started, which is not shown
      here. Never carries the payment id, the payer's e-mail, name, type, state, IP, source or
      refund address, or any provider id. The id is not a secret from a link holder, though: the
      same-rail `POST .../payments` replay returns it on every rail except `crypto_relay`, so the
      link token is the only capability for `mark_sent` and `receipt_email`. Always present (`null`
      or the object), never omitted. A short `crypto_tempo` payment the payer can top up reads
      `null` here and is described by `underpaid_payment`.
    required:
      - rail
      - status
      - deposit_instructions
      - bank_deposit_instructions
      - expires_at
      - created_at
      - payer_marked_sent
    properties:
      rail:
        type: string
        enum:
          - ach
          - wire
          - fednow
          - sepa
          - faster_payments
          - pix
          - spei
          - crypto_tempo
          - crypto_bridge
          - crypto_relay
      status:
        type: string
        enum:
          - awaiting
          - detected
          - processing
        description: The live payment's `PaymentStatus`; only these three ever appear here.
      deposit_instructions:
        description: >-
          A crypto rail's receiving instructions, the same projection as
          `PaymentSessionPayment.deposit_instructions` (`crypto_tempo`: the splitter address, chain
          id, token and the total including the 1% Swaps fee; `crypto_relay`: the Relay deposit
          address, source network and exact amount, never the refund address; `crypto_bridge`: the
          provider deposit address and estimated amount). `null` on a bank rail. `crypto_relay`:
          send exactly `amount` on `chain` before `expires_at`; any Relay refund goes to the refund
          address given when this payment was started, which is not shown here.
        oneOf:
          - $ref: '#/components/schemas/DepositInstructions'
          - type: 'null'
      bank_deposit_instructions:
        description: >-
          A bank rail's receiving account and the reference the payer must use, the same projection
          as `PaymentSessionPayment.bank_deposit_instructions`. `null` on a crypto rail. Exactly one
          of the two is non-null.
        oneOf:
          - $ref: '#/components/schemas/BankDepositInstructions'
          - type: 'null'
      expires_at:
        type:
          - string
          - 'null'
        format: date-time
        description: >-
          `crypto_relay`: the send-by time of its Relay quote (`pending_payment` is `null` once it
          passes). `null` on every other rail, whose instructions do not expire on their own.
      created_at:
        type: string
        format: date-time
        description: When the payer chose this payment method.
      payer_marked_sent:
        type: boolean
        description: >-
          `true` once `POST .../mark_sent` recorded it: the payer reported sending; show the
          instructions as reference, not as a call to pay. Only the flag, never when.
    additionalProperties: false
  underpaid_payment:
    type:
      - object
      - 'null'
    description: >-
      PAY-WAIT-2-T (founder ruling §52.53 2A, 2026-09-29) — how much is still owed and where to send
      it when this link's live `crypto_tempo` payment arrived short, so the payer can top up the
      same payment from any browser. Present only while `status` is `processing`, the link has not
      expired, its latest payment is a `crypto_tempo` payment in `underpaid` whose running total
      Swaps recorded, no deposit to it is still being recorded, no other payment's deposit is being
      recorded (a deposit Swaps observed on another open payment of this link may complete it;
      PAY-WAIT-3-T), and no other payment of this link holds money (`underpaid`, `unmatched`,
      completed, or a `crypto_relay` leg that saw funds). `null` in every other case, including:
      `crypto_relay` (its Relay deposit address is issued per quote, so there is no top-up path;
      `amount_verdict` and the payment's own read say it is short), a bank rail or `crypto_bridge`,
      a cancelled link (its short payment becomes `unmatched`), a paid, closed or expired link, and
      a recorded total from which Swaps cannot prove one positive remainder in one token (withheld
      and reported, never published as a guess). While this is set `pending_payment` is `null`. A
      top-up that reaches the total settles the payment `paid` with the sum. Never carries the
      payment id or any payer data (e-mail, name, type, state, IP, source address).
    required:
      - rail
      - amount_missing
      - amount_received
      - deposit_instructions
      - created_at
    properties:
      rail:
        type: string
        enum:
          - crypto_tempo
      amount_missing:
        description: >-
          What is still owed: the total due (the invoice plus the 1% Swaps fee, the figure the
          payment is judged against) minus `amount_received`, exact, in the same token and scale as
          `amount_received`; always greater than zero. The same figure `GET
          .../payments/{payment_id}` publishes as `amount_missing` for this payment, and
          `deposit_instructions.amount` here.
        allOf:
          - $ref: '#/components/schemas/Money'
      amount_received:
        description: >-
          The running total Swaps recorded for this payment, in the token of its first counted
          deposit. A deposit in another accepted token is never added to it (it is held for
          support), so it is not in this figure.
        allOf:
          - $ref: '#/components/schemas/Money'
      deposit_instructions:
        description: >-
          What to send now: the same receiving address and chain id as while the payment was
          awaiting, narrowed to the ONE token the running total counts (`asset` and `token_address`
          name that token only; a top-up in any other token is not added to the total). `amount`
          equals `amount_missing` and is exact (`amount_is_estimate: false`); `amount_out`,
          `bridge_fee` and `gross_up`, which describe the whole payment, are absent.
        allOf:
          - $ref: '#/components/schemas/DepositInstructions'
      created_at:
        type: string
        format: date-time
        description: When the payer chose this payment method.
    additionalProperties: false
  network:
    type:
      - string
      - 'null'
    enum:
      - tempo
      - base
      - ethereum
      - polygon
      - arbitrum
      - optimism
      - solana
      - null
    description: >-
      The source network of this link's latest `crypto_relay` payment (CP-X2). Present only once a
      `crypto_relay` payment exists, which only the `crypto_relay_rail` flag (off in production)
      allows; absent otherwise. `solana` stays dark: `POST .../payments` answers `409
      network_not_supported` for it.
  stablecoin:
    type:
      - string
      - 'null'
    enum:
      - USDC.e
      - pathUSD
      - USDT0
      - USD1
      - cUSD
      - USDC
      - null
    description: >-
      The stablecoin the payer sends on `network` — `USDC` for `crypto_relay`. Present exactly when
      `network` is.
  deposit_instructions_by_chain:
    type: array
    description: >-
      The Relay deposit for `network` (CP-X2; R19 §2.4; MON-7, CMP-5), present exactly when
      `network` is. Send `amount` of `asset` on `chain` to `address` (case-preserved verbatim) —
      never to the splitter — before `quote_expires_at`. The quote is a strict exact-output order:
      `amount_is_estimate` is false, the splitter receives exactly `amount_out` (the invoice plus
      the 1% Swaps fee), `bridge_fee` is Relay's part of `amount`, and `provider: relay` states that
      a third party moves the funds. Relay refunds a failed or mismatched deposit only to the refund
      address the payer gave; Swaps holds no key on this path. An empty array means the address is
      not payable (the payment or link is closed, Relay has already seen a transfer, or the send-by
      time passed) or the stored quote could not be read; `watcher_state` says which. It is also
      empty while another payment of the link holds money or has a deposit being recorded;
      `watcher_state` then stays `address_issued` and `pending_payment` is null.
    items:
      $ref: '#/components/schemas/DepositInstructions'
  one_tap:
    type: object
    properties:
      available:
        type: boolean
  watcher_state:
    $ref: '#/components/schemas/PaymentWatcherState'
  watcher_reason:
    $ref: '#/components/schemas/PaymentWatcherReason'
  merchant_contact:
    type:
      - object
      - 'null'
    description: >-
      L4-9 — display-only, so a terminal screen's "contact the merchant" line can be actionable.
      Populated ONLY from the merchant's own opt-in `Account.support_contact`; `null` when the
      merchant never set one. NEVER the merchant's login e-mail, a KYC field, a Bridge record or any
      settlement/id data.
    properties:
      email:
        type: string
        format: email
        maxLength: 254
      url:
        type: string
        format: uri
        pattern: ^https://
        maxLength: 512
    additionalProperties: false
unevaluatedProperties: false
```

<a id="paymentsessioncoderesolution"></a>
## PaymentSessionCodeResolution

```yaml
type: object
description: >-
  C5-SHORT-LINK — what a payment link's `short_code` resolves to: the payer session token and the
  payer page `url`. Carries nothing else; read the session itself with `GET
  /v1/payment_sessions/{token}`.
required:
  - token
  - url
properties:
  token:
    type: string
    pattern: ^plk_
    description: 'The plk_ capability token — a secret, never an id: never logged, never echoed.'
  url:
    type: string
    format: uri
    pattern: ^https://
    description: The payer page for `token` on this project's payer host (the same value as `PaymentLink.url`).
additionalProperties: false
```

<a id="paymentsessionunavailablerail"></a>
## PaymentSessionUnavailableRail

```yaml
type: object
required:
  - rail
  - reason_code
properties:
  rail:
    type: string
    enum:
      - ach
      - wire
      - fednow
      - sepa
      - faster_payments
      - pix
      - spei
      - crypto_tempo
      - crypto_bridge
      - crypto_relay
  reason_code:
    type: string
    enum:
      - merchant_fiat_payin_pending
      - settlement_requires_crypto
      - merchant_individual_rail_blocked
      - merchant_rail_not_enabled
      - link_currency_unsupported
      - amount_below_rail_minimum
      - rail_disabled
      - rail_temporarily_unavailable
      - rail_not_offered
      - settlement_kind_mismatch
      - merchant_not_enrolled
      - amount_above_rail_maximum
    description: >-
      Closed set. `merchant_fiat_payin_pending`: the merchant is an individual whose bank pay-in is
      not active yet (the same `details.reason` `POST .../payments` returns).
      `settlement_requires_crypto`: the link settles to a bank account and there is no bank-to-bank
      route, so only a crypto pay-in can settle it; it takes precedence over
      `merchant_fiat_payin_pending`. `merchant_individual_rail_blocked`: this rail is never payable
      to an individual merchant (pix, faster_payments). `merchant_rail_not_enabled`: the merchant's
      account is not enabled for this rail (for `pix`: the merchant's Pix deposit approval at Bridge
      is not in place yet). `link_currency_unsupported`: the rail cannot settle an invoice in this
      currency (`crypto_tempo` is USD only). `amount_below_rail_minimum`: the invoice is under the
      rail's minimum. `rail_disabled`: Swaps has switched the rail off for this link (an operator
      switch, not a merchant choice). `settlement_kind_mismatch`: the link's settlement destination
      cannot receive this crypto rail (`crypto_tempo` needs a Tempo address; `crypto_bridge` needs a
      Tempo address or a supported bank off-ramp account, so a link settling to an Ethereum address
      gets neither; `crypto_bridge` is also never offered on a link that settles to the merchant's
      own Swaps Wallet, `crypto_only`). `merchant_not_enrolled`: Swaps has not yet opened
      `crypto_bridge` for this merchant (a Swaps rollout gate, not a merchant setting, so do not
      word it as the merchant's choice). `crypto_tempo` and `crypto_bridge` are always in exactly
      one of `rails[]` and this list; `crypto_relay` is in exactly one of them only while the
      `crypto_relay_rail` flag admits the merchant, and in neither otherwise. For `crypto_relay`,
      `rail_not_offered` means no source network has an admitted Relay route (`founder_accepted` per
      founder §52.34, or `proven`) into this link's settlement network, and
      `amount_above_rail_maximum` means the invoice is above the rail's per-payment cap.
      `rail_temporarily_unavailable`: eligibility could not be verified right now (for example a
      missing FX rate), so the rail fails closed. `rail_not_offered`: no more specific reason
      applies. New values are added only in a documented contract change.
additionalProperties: false
```

<a id="paymentsessionselectrailrequest"></a>
## PaymentSessionSelectRailRequest

```yaml
type: object
description: >-
  Selects a rail and, for a crypto rail, the network and stablecoin (RESOURCE-MODEL §2.1 v2
  amendment: `select_rail` extended with network + stablecoin). The payer's consent checkbox is a
  client-side gate, not a field here. `payer_type` is required on every rail — it is the §5.1
  payer-gate declaration (business vs. individual); the three USDC-stablecoin rails (`crypto_tempo`,
  `crypto_bridge`, `crypto_relay`) still take the field but carry no fiat ceiling, so it is accepted
  and ignored for the cap check on those. `source_chain`/ `source_asset` are required, and
  `source_address` optional, when `rail` is `crypto_bridge` (K3 correction to the v1 draft of this
  schema, which omitted every payer-gate and cross-chain field the handler actually requires).
  `network` and `refund_address` are required when `rail` is `crypto_relay` (CP-X2, behind the
  `crypto_relay_rail` flag): one open payment per link — the same network and refund address get it
  back, anything else is `409 payment_in_progress` until Relay can no longer fill it and either saw
  no transfer or refunded it with the refund transaction recorded. Refusals: `400
  refund_address_required`; `409 network_not_supported` for a network outside the five Relay
  sources; `503 relay_route_unavailable` when the network has no admitted route (`founder_accepted`
  per founder §52.34, or `proven`), Relay cannot quote, the Tempo leg is down or the link closes too
  soon (`details.reason`, `details.network`); `502 relay_quote_shortfall` when Relay's quote does
  not deliver exactly the amount due to the splitter. Every refusal records nothing.
required:
  - rail
  - payer_type
properties:
  rail:
    type: string
    enum:
      - ach
      - wire
      - fednow
      - sepa
      - faster_payments
      - pix
      - spei
      - crypto_tempo
      - crypto_bridge
      - crypto_relay
  payer_type:
    type: string
    enum:
      - business
      - individual
    description: >-
      The payer's own declaration (§5.1) — decides which fiat-rail cap applies. Not the merchant's
      `expected_payer_type` restriction.
  payer_state:
    type: string
    description: >-
      Two-letter US state code — only consulted for an individual payer on a US ACH/wire/FedNow rail
      against an individual (p2p) merchant's US-residency gate.
  payer_email:
    type: string
    format: email
    description: >-
      Optional receipt email set at select-rail time — a payer may also set or correct it afterward
      via `POST .../receipt_email`.
  network:
    type: string
    enum:
      - tempo
      - base
      - ethereum
      - polygon
      - arbitrum
      - optimism
      - solana
    description: >-
      Required when `rail` is `crypto_relay`: the network the payer sends USDC from (`base`,
      `ethereum`, `polygon`, `arbitrum`, `optimism`; `tempo` and `solana` answer `409
      network_not_supported`). Accepted and ignored on the other rails today.
  stablecoin:
    type: string
    enum:
      - USDC.e
      - pathUSD
      - USDT0
      - USD1
      - cUSD
      - USDC
    description: >-
      Optional. `crypto_relay` accepts only `USDC` (anything else is `400 invalid_request`, `param:
      stablecoin`); accepted and ignored on the other rails today.
  refund_address:
    type: string
    pattern: ^0x[0-9a-fA-F]{40}$
    description: >-
      Required when `rail` is `crypto_relay`: the payer's own address on `network`, kept
      case-verbatim. Relay returns a failed, short or excess deposit only here. Never a Swaps
      address — the Swaps fee wallet, the merchant's settlement address and the zero address are
      refused (`400 invalid_request`, `param: refund_address`).
  source_chain:
    type: string
    enum:
      - base
      - ethereum
      - polygon
      - arbitrum
      - optimism
    description: >-
      Required when `rail` is `crypto_bridge` — the payer's own chain, distinct from `network` (the
      merchant's settlement network).
  source_address:
    type: string
    description: >-
      Optional when `rail` is `crypto_bridge` — the payer's sending wallet, kept case-verbatim.
      Bridge accepts a deposit from any sender when omitted.
  source_asset:
    type: string
    enum:
      - USDC
    description: Required when `rail` is `crypto_bridge` — Phase 1 accepts USDC only.
additionalProperties: false
```

<a id="paymentwatcherstate"></a>
## PaymentWatcherState

```yaml
type:
  - string
  - 'null'
enum:
  - address_issued
  - source_seen
  - confirming
  - matching
  - releasing
  - delivered
  - recovering
  - recovery_required
  - bridge_failed
  - expired
  - null
description: >-
  Narrator sub-state (R19 CP-G11), produced for `crypto_relay` payments (CP-X2, behind
  `crypto_relay_rail`); `null` on every other rail today. From the Relay deposit intent and the
  payment: `address_issued` (deposit address issued, nothing seen), `source_seen` (the payer's
  transfer seen on the source network), `confirming` (Relay is bridging it), `matching` (Relay
  reported the fill on Tempo; the splitter watcher is matching it), `delivered` (the splitter
  watcher observed the deposit and released it — the only state that goes with `paid`). `releasing`
  has no observable producer yet. Problem states always carry `watcher_reason` and never mean paid:
  `recovering` / `recovery_required` (a deposit on the wrong network, recovered through Relay or
  needing support; `recovery_required` with `payment_closed`: funds reached or are on their way to
  the splitter of a payment that is already closed, so support must return them), `bridge_failed`
  (Relay refunded the order to the payer's refund address, or failed it; a failed order may return
  nothing and needs support), `expired` (the send-by time passed with nothing seen — do not send to
  this address). A transfer Relay saw before the send-by time stays `confirming`.
```

<a id="paymentwatcherreason"></a>
## PaymentWatcherReason

```yaml
type:
  - string
  - 'null'
enum:
  - wrong_network_deposit
  - provider_refund_or_failure
  - quote_expired
  - bridge_delivered_less
  - payment_closed
  - null
description: >-
  Why `watcher_state` is a problem state; `null` otherwise. `bridge_delivered_less` goes with
  `matching` when Relay reported delivering less than the amount due: the splitter holds the funds
  and releases nothing until the full amount is there (the underpaid rule). `payment_closed` goes
  with `recovery_required`: the payment was closed (link expired, cancelled or paid another way)
  while its funds were in flight.
```

<a id="paymentsessionreceiptemailrequest"></a>
## PaymentSessionReceiptEmailRequest

```yaml
type: object
description: >-
  Attaches a receipt email to one payment attempt on this session. `attempt_id` is required (K3
  correction — the v1 draft of this schema omitted it, but the handler scopes the write to one
  attempt id, not "the link"; a payer with more than one live attempt — the product allows up to
  five — would otherwise have no way to say which one).
required:
  - attempt_id
  - email
properties:
  attempt_id:
    type: string
    pattern: ^pay_
  email:
    type: string
    format: email
additionalProperties: false
```

<a id="paymentsessionmarksentrequest"></a>
## PaymentSessionMarkSentRequest

```yaml
type: object
description: >-
  Identifies which payment attempt the payer is confirming they sent (K3 addition — `mark_sent` had
  no request body in the v1 draft at all; the handler scopes the CAS update to one attempt id).
required:
  - attempt_id
properties:
  attempt_id:
    type: string
    pattern: ^pay_
additionalProperties: false
```

<a id="paymentsessionpaywithwalletresponse"></a>
## PaymentSessionPayWithWalletResponse

```yaml
type: object
description: >-
  Unsigned steps only (RESOURCE-MODEL §0.10, §2.1 v2 amendment `pay_with_wallet`). Mirrors the shape
  already established for the wallet's own unsigned operations (`send_intents.send_instructions`,
  `conversions.steps[]`, RESOURCE-MODEL §2.3) — nothing here can sign or move funds; the holder's
  passkey is the only signer.
properties:
  steps:
    type: array
    items:
      type: object
      properties:
        kind:
          type: string
          description: E.g. `transfer` — a single unsigned call to the splitter address.
        call:
          type: object
          properties:
            to:
              type: string
            data:
              type: string
            value:
              type: string
  request_id:
    type: string
  expires_at:
    type: string
    format: date-time
unevaluatedProperties: false
```

<a id="payoutstatus"></a>
## PayoutStatus

```yaml
type: string
x-swaps-open-enum: true
description: >
  The 11 payout states (RESOURCE-MODEL §2.2). `paid` is not terminal — the ladder's final rung stays
  active until `settled`. `settled` is the recipient paid the invoice exactly, or above it by at
  most 0.05 % of the invoice plus 10 minor units (25 for MXN) — the funding buffer's over-delivery;
  `settled_amount` is what was actually paid. `paid_with_shortfall` is terminal: it produces no
  receipt, no success notification, and never becomes `settled`. A1-2: open — an unrecognized member
  is an opaque string, never a deserialization failure.
enum:
  - draft
  - awaiting_funds
  - funds_received
  - processing
  - paid
  - paid_with_shortfall
  - settled
  - failed
  - returned
  - expired
  - cancelled
```

<a id="payoutfailurecode"></a>
## PayoutFailureCode

```yaml
type: string
description: >
  Bridge terminal-state failure code (RESOURCE-MODEL §2.2, v2 amendments — "`failure_code` as a
  published enum"). The exhaustive list of Bridge terminal codes is not present in the files this
  fragment was generated from; only `returned_no_account` is directly evidenced
  (`docs/api/consumers/R12-pay-invoice.md` §1 `pi-returned`, §4). Left open as a free string rather
  than a closed, invented enum — see `x-swaps-enum-source`.
x-swaps-enum-source: RESOURCE-MODEL §2.2 D-77
example: returned_no_account
```

<a id="payoutcapabilitysnapshot"></a>
## PayoutCapabilitySnapshot

```yaml
type: object
description: >
  **v1 reality (K4c, §52 C4-D12):** `eta_seconds`/`minimum` are resolved from the LIVE corridor
  catalog at read time, keyed by the payout's own stored `capability_id`; `version` is the
  capability-contract version stamped at creation, not re-resolved — both are presentation-only
  facts (no SLA, no legal claim) that are safe to re-derive live.
  `sender_display`/`legal_entity_name` are the OPPOSITE: FROZEN at payout creation (or replacement —
  a `payouts.replace` draft gets its own fresh resolution too, never a copy of the payout it
  replaces) and read back from the row, never re-derived from the live catalog. A payout created on
  or after this snapshot's migration always carries one — `sender_display` is set even when the
  resolver could not identify a specific sender (the literal value `unknown`, never omitted in that
  case). A payout created BEFORE the migration carries no snapshot at all: `sender_display` is
  omitted entirely and `legal_entity_name` is `null`, exactly as this endpoint behaved before the
  freeze existed — nothing proves what that payout was actually sent as, so nothing is invented for
  it. The freeze exists precisely because the catalog's answer for a corridor can change for reasons
  that have nothing to do with one specific payout (e.g. Bridge moved `eur_sepa` from `bridge` to
  `customer` on 2026-09-02, D-111): a live-resolved value here would let a payout created before
  that date wrongly claim "customer" on a statement it never actually carried that name on.
  Resolution reads the provider's account record AS CACHED AT THE MOMENT OF FREEZE — not a live
  probe — so a rename on Bridge's side after creation never changes an already-frozen payout's
  answer; a client rendering `bridge` should pair it with a tip that the statement name changes once
  the account is renamed. A client that wants the corridor's CURRENT terms (for a payout not yet
  created) calls `GET /v1/capabilities?product=payouts` instead. Consumer contract: prefer this
  snapshot first and fall back to a live capability read ONLY when it is absent (a legacy pre-freeze
  payout) — never the reverse.
required:
  - eta_seconds
  - minimum
  - version
properties:
  sender_display:
    type: string
    description: >
      How the sender appeared on the recipient's bank statement, frozen at the moment this payout
      was created. Omitted only for a payout created before the creation-time freeze existed (no
      column value to publish) — present, and possibly `unknown`, on every payout created since.
    enum:
      - bridge
      - customer
      - payment_partner
      - unknown
  legal_entity_name:
    type:
      - string
      - 'null'
    description: >
      The specific legal-entity/account-holder name frozen alongside `sender_display`, verbatim from
      the customer's Bridge virtual account record AS CACHED AT THE MOMENT OF FREEZE — populated
      ONLY for `eur_sepa` (the one corridor whose `customer` category genuinely reflects the
      receiving-account record; `usd_wire` also defaults to `customer` in the catalog, but that is
      an ACTIVE Bridge configuration Swaps applies at funding time, not an account fact, so it never
      gets this per-account refinement and always reads `null` here), and only when a Bridge
      customer id is already resolvable for the payer and every activated account THAT Bridge
      customer holds for EUR agrees on the name on file. Null in every other case: `sender_display`
      itself omitted (a pre-freeze payout), `sender_display` is
      `bridge`/`payment_partner`/`unknown`, the corridor is not `eur_sepa`, no Bridge customer id
      was resolvable yet, no activated account existed to resolve against, or more than one
      activated account disagreed on the name — the resolver never guesses between them.
      `packages/config/payoutCapabilities.ts` itself still carries no legal-entity-name field
      (PI-G6's catalog half remains open); this field is a per-PAYOUT fact sourced from the account
      record, not a per-corridor catalog constant.
  eta_seconds:
    type: integer
    minimum: 0
    description: >
      Typical time to arrival for this corridor, in seconds. Presentation-only — it carries no
      provider SLA and must be labelled accordingly wherever it is shown.
  minimum:
    $ref: '#/components/schemas/Money'
  version:
    type: string
    description: Capability-contract version this snapshot was resolved from.
additionalProperties: false
```

<a id="payoutbeneficiary"></a>
## PayoutBeneficiary

```yaml
type: object
description: >
  The masked beneficiary projection returned on `payouts` and `receipt`. Raw bank details cross the
  boundary once, at `POST /v1/payouts/{id}/beneficiary`, and are held only by the provider from then
  on — this is the only shape ever read back (RESOURCE-MODEL §2.2 invariants). Note the output field
  is `beneficiary_owner_type`, not the request's `account_owner_type` — the two are named
  differently on purpose.
required:
  - account_holder
  - account_tail
  - bank_country
  - beneficiary_owner_type
properties:
  account_holder:
    type: string
    description: The bank account holder's name, as supplied on the beneficiary request.
  account_tail:
    type: string
    description: The last few digits of the destination account or rail identifier, masked.
    example: ••••4821
  bank_country:
    type: string
    description: ISO country code of the destination bank.
    example: DE
  beneficiary_owner_type:
    type: string
    enum:
      - individual
      - business
  bank_name:
    type: string
    description: >
      Name of the destination bank, as supplied on the beneficiary request. Present only when one
      was supplied (US bank rails `ach` and `wire`); absent, never empty, otherwise.
additionalProperties: false
```

<a id="payoutbeneficiaryaddress"></a>
## PayoutBeneficiaryAddress

```yaml
type: object
description: >
  The beneficiary's address (`docs/api/consumers/R12-pay-invoice.md` §2 PI-G11). `state_region` is
  collected only for US rails, and is OPTIONAL there as well as everywhere else — the `required`
  list below omits it; a US beneficiary with no state supplied is not rejected for that alone.
  (Corrected 2026-09-07: the schema, the route's own `optionalAddress` and the approved
  `pi-recipient-usd` screen already agreed on this; this description previously said "required" and
  was the one outlier — confirm with Bridge whether that was ever actually enforced before
  tightening either side.)
required:
  - street
  - city
  - postal_code
properties:
  street:
    type: string
  city:
    type: string
  postal_code:
    type: string
  state_region:
    type:
      - string
      - 'null'
    description: US rails only; null for every other rail.
additionalProperties: false
```

<a id="payoutbeneficiaryiban"></a>
## PayoutBeneficiaryIban

```yaml
type: object
description: 'IBAN rail beneficiary (`rail: sepa`).'
required:
  - rail
  - account_owner_type
  - account_holder
  - address
  - iban
  - bic
properties:
  rail:
    type: string
    enum:
      - sepa
  account_owner_type:
    type: string
    enum:
      - individual
      - business
    description: Who owns the destination bank account.
  account_holder:
    type: string
    description: Full name on the destination bank account.
  address:
    $ref: '#/components/schemas/PayoutBeneficiaryAddress'
  iban:
    type: string
  bic:
    type: string
additionalProperties: false
```

<a id="payoutbeneficiaryusbank"></a>
## PayoutBeneficiaryUsBank

```yaml
type: object
description: 'US account+routing rail beneficiary (`rail: ach` or `rail: wire`).'
required:
  - rail
  - account_owner_type
  - account_holder
  - address
  - account_number
  - routing_number
  - account_type
properties:
  rail:
    type: string
    enum:
      - ach
      - wire
  account_owner_type:
    type: string
    enum:
      - individual
      - business
    description: Who owns the destination bank account.
  account_holder:
    type: string
    description: Full name on the destination bank account.
  address:
    $ref: '#/components/schemas/PayoutBeneficiaryAddress'
  account_number:
    type: string
  routing_number:
    type: string
  account_type:
    type: string
    enum:
      - checking
      - savings
  bank_name:
    type: string
    minLength: 1
    maxLength: 128
    description: Name of the destination bank. Optional; some banks need it to be identified.
additionalProperties: false
```

<a id="payoutbeneficiaryukbank"></a>
## PayoutBeneficiaryUkBank

```yaml
type: object
description: 'UK sort-code rail beneficiary (`rail: faster_payments`).'
required:
  - rail
  - account_owner_type
  - account_holder
  - address
  - sort_code
  - account_number
properties:
  rail:
    type: string
    enum:
      - faster_payments
  account_owner_type:
    type: string
    enum:
      - individual
      - business
    description: Who owns the destination bank account.
  account_holder:
    type: string
    description: Full name on the destination bank account.
  address:
    $ref: '#/components/schemas/PayoutBeneficiaryAddress'
  sort_code:
    type: string
  account_number:
    type: string
additionalProperties: false
```

<a id="payoutbeneficiarypix"></a>
## PayoutBeneficiaryPix

```yaml
type: object
description: 'Brazil PIX rail beneficiary (`rail: pix`).'
required:
  - rail
  - account_owner_type
  - account_holder
  - address
  - pix_key
  - pix_key_type
properties:
  rail:
    type: string
    enum:
      - pix
  account_owner_type:
    type: string
    enum:
      - individual
      - business
    description: Who owns the destination bank account.
  account_holder:
    type: string
    description: Full name on the destination bank account.
  address:
    $ref: '#/components/schemas/PayoutBeneficiaryAddress'
  pix_key:
    type: string
  pix_key_type:
    type: string
    description: >-
      The PIX key's own kind (e.g. email, phone, CPF/CNPJ, random) — not further enumerated in
      RESOURCE-MODEL §2.2.
additionalProperties: false
```

<a id="payoutbeneficiaryclabe"></a>
## PayoutBeneficiaryClabe

```yaml
type: object
description: 'Mexico CLABE rail beneficiary (`rail: spei`).'
required:
  - rail
  - account_owner_type
  - account_holder
  - address
  - clabe
properties:
  rail:
    type: string
    enum:
      - spei
  account_owner_type:
    type: string
    enum:
      - individual
      - business
    description: Who owns the destination bank account.
  account_holder:
    type: string
    description: Full name on the destination bank account.
  address:
    $ref: '#/components/schemas/PayoutBeneficiaryAddress'
  clabe:
    type: string
additionalProperties: false
```

<a id="payoutbeneficiaryrequest"></a>
## PayoutBeneficiaryRequest

```yaml
description: >
  Adds the beneficiary to a `draft` payout — once, ever (RESOURCE-MODEL §2.2). Exactly one
  fully-specified rail shape must be supplied, chosen by `rail`: `sepa` (IBAN+BIC), `ach`/`wire` (US
  account+routing+`account_type`), `faster_payments` (UK sort code + account number), `pix` (a PIX
  key), or `spei` (a CLABE) (`docs/api/consumers/R12-pay-invoice.md` §1 `pi-recipient*`). Money
  boundary — REST only, never exposed as an MCP tool argument, because most MCP clients log tool
  arguments verbatim (D-6). When the provider rejects the details, the answer is `422
  beneficiary_invalid`; its `error.param` names the request field (for example `bank_name`,
  `routing_number`, `address.postal_code`) only when exactly one field can be named, and is absent
  otherwise.
oneOf:
  - $ref: '#/components/schemas/PayoutBeneficiaryIban'
  - $ref: '#/components/schemas/PayoutBeneficiaryUsBank'
  - $ref: '#/components/schemas/PayoutBeneficiaryUkBank'
  - $ref: '#/components/schemas/PayoutBeneficiaryPix'
  - $ref: '#/components/schemas/PayoutBeneficiaryClabe'
discriminator:
  propertyName: rail
  mapping:
    sepa: '#/components/schemas/PayoutBeneficiaryIban'
    ach: '#/components/schemas/PayoutBeneficiaryUsBank'
    wire: '#/components/schemas/PayoutBeneficiaryUsBank'
    faster_payments: '#/components/schemas/PayoutBeneficiaryUkBank'
    pix: '#/components/schemas/PayoutBeneficiaryPix'
    spei: '#/components/schemas/PayoutBeneficiaryClabe'
```

<a id="payout"></a>
## Payout

```yaml
description: >
  A fiat payout funded with crypto (RESOURCE-MODEL §2.2, "Pay an invoice"). No `expires_at` (written
  by nothing today, always null — cut per D-8) and no `reconciliation_status` (stays internal;
  surfaces here only as the derived `needs_attention` boolean).
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - object
      - status
      - invoice_amount
      - corridor_id
      - fiat_rail
      - source_chain
      - source_chain_chosen
      - source_asset
      - payer_type
      - fee_bps
      - notify_recipient
      - capability_snapshot
      - needs_attention
    properties:
      id:
        type: string
        pattern: ^po_
      object:
        type: string
        enum:
          - payout
      status:
        $ref: '#/components/schemas/PayoutStatus'
      invoice_amount:
        $ref: '#/components/schemas/Money'
        description: The invoice amount the recipient is owed, in fiat.
      corridor_id:
        type: string
        description: >
          The corridor this payout settles through, as listed by `GET
          /v1/capabilities?product=payouts`. Known values today (RESOURCE-MODEL §3 vocabulary):
          `usd_ach`, `usd_wire`, `eur_sepa`, `gbp_faster_payments`, `brl_pix`, `mxn_spei`,
          `cop_co_bank_transfer` (lifecycle `blocked` — never actually payable).
        example: eur_sepa
      fiat_rail:
        type: string
        description: The settlement rail underlying `corridor_id`.
        enum:
          - ach
          - wire
          - sepa
          - faster_payments
          - pix
          - spei
      source_chain:
        type: string
        description: >
          The chain the payer funds from. Read it together with `source_chain_chosen`: while that is
          false, no chain has been chosen yet and this is only the default (`base`), never a choice.
          The stored `payout.*` event payload (`GET /v1/events`, the SSE stream, webhooks, `GET
          /v1/activity`) carries the stored chain instead: `null` for a draft with no chain, and no
          `source_chain_chosen`.
        enum:
          - base
          - ethereum
          - polygon
          - arbitrum
          - optimism
      source_chain_chosen:
        type: boolean
        description: >
          True once a chain is fixed for this payout: sent as `source_chain` at create, or fixed at
          funding (`POST /v1/payouts/{id}/funding_instructions`, locked once the provider transfer
          exists). False on a draft created without `source_chain`; `source_chain` then shows the
          default.
      source_asset:
        type: string
        description: The stablecoin the payer funds with. Only USDC is live today.
        example: USDC
      payer_type:
        type: string
        enum:
          - individual
          - business
        description: >
          Set by Swaps at creation, never from the request: `business` only when the account the
          payout was created under and the payer's provider customer were both business. The
          individual per-payout limit is checked at funding against the account's type at that
          moment and the fresh provider customer, not against this stored value. A distinct axis
          from `beneficiary.beneficiary_owner_type`.
      fee_bps:
        type: integer
        description: >
          The Swaps fee, in basis points, on the gross source amount. Today always 100 (1%). A
          stored value that disagrees with the current fee contract is refused at fund
          (RESOURCE-MODEL §2.2 invariants).
        example: 100
      settled_amount:
        description: >
          The confirmed amount actually settled to the recipient — the invoice, or up to 0.05 % of
          the invoice plus 10 minor units (25 for MXN) above it when the funding buffer
          over-delivers. Null until `settled`.
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
      provider_paid_amount:
        description: >
          The confirmed amount the provider actually paid out. Set alongside
          `invoice_shortfall_amount` on `paid_with_shortfall`; null otherwise.
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
      invoice_shortfall_amount:
        description: >
          `invoice_amount` minus `provider_paid_amount` on a `paid_with_shortfall` payout. Null on
          every other status.
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
      note:
        type:
          - string
          - 'null'
        maxLength: 280
        description: Free-text note the payer attached at creation.
      recipient_email:
        type:
          - string
          - 'null'
        format: email
        description: >
          The recipient's email, when the payer chose to notify them (RESOURCE-MODEL §2.2 v2
          amendments — added create + read).
      notify_recipient:
        type: boolean
        default: false
        description: Whether `recipient_email` is emailed a payment confirmation once the payout lands.
      capability_snapshot:
        $ref: '#/components/schemas/PayoutCapabilitySnapshot'
      beneficiary:
        description: The masked beneficiary, once added. Null on a `draft` with no beneficiary yet.
        oneOf:
          - $ref: '#/components/schemas/PayoutBeneficiary'
          - type: 'null'
      failure_code:
        description: >
          Set on `failed` and `returned`; also set to `missing_return_policy` while Bridge holds the
          deposit with no address to return it to (`processing`, `needs_attention`).
        oneOf:
          - $ref: '#/components/schemas/PayoutFailureCode'
          - type: 'null'
      needs_attention:
        type: boolean
        description: >
          Router-derived from the internal `reconciliation_status` signal, which stays internal
          (RESOURCE-MODEL D-8, resolved). True renders as "Checking settlement" over whatever
          `status` currently reads, most commonly over `paid`.
      funded_at:
        type:
          - string
          - 'null'
        format: date-time
        description: When the funding provider transfer was created. Null before funding.
      paid_at:
        type:
          - string
          - 'null'
        format: date-time
        description: When the provider confirmed payout to the recipient's bank.
      settled_at:
        type:
          - string
          - 'null'
        format: date-time
        description: When the payout reached its terminal `settled` state.
unevaluatedProperties: false
```

<a id="payoutcreaterequest"></a>
## PayoutCreateRequest

```yaml
type: object
description: >
  Creates a `draft` payout. Nothing is charged and no money moves — funding is a separate, later
  step (`POST /v1/payouts/{id}/funding_instructions`).
required:
  - invoice_amount
  - corridor_id
properties:
  invoice_amount:
    $ref: '#/components/schemas/MoneyPositive'
  corridor_id:
    type: string
    description: >
      One of `GET /v1/capabilities?product=payouts` corridors[].id. Refused with a 409
      `capability_unavailable` if the corridor is not currently executable.
    example: eur_sepa
  source_chain:
    type: string
    enum:
      - base
      - ethereum
      - polygon
      - arbitrum
      - optimism
    description: >
      Optional. The USDC chain the payer funds from, when already chosen; it must be one of the
      corridor's `source_chains` (otherwise `409 capability_unavailable`, nothing created). Leave it
      out until the payer picks: the draft then has no chain (`source_chain_chosen: false`) and the
      chain is given when funding.
  payer_type:
    type: string
    enum:
      - individual
      - business
    description: >
      Optional. Swaps derives the payer type from the account the request acts for (`Swaps-Account`,
      or the key's account) and the payer's provider customer: `business` only when both are
      business. Leave it out. An equal value is accepted; a different one is refused with `422
      payer_type_mismatch` (`details.expected`, `details.received`) and nothing is created.
  note:
    type: string
    maxLength: 280
  recipient_email:
    type: string
    format: email
    description: Required if `notify_recipient` is true.
  notify_recipient:
    type: boolean
    default: false
    description: Email `recipient_email` a payment confirmation once, when the payment lands.
additionalProperties: false
```

<a id="payoutlist"></a>
## PayoutList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Payout'
unevaluatedProperties: false
```

<a id="payoutfundinginstructions"></a>
## PayoutFundingInstructions

```yaml
type: object
description: >
  The money boundary: what to send, and where, to fund a payout's provider transfer. The first call
  creates the transfer and freezes the fee: from that moment `fee_estimate`/`gross_estimate` are the
  figures the transfer was actually created against (`payouts.developer_fee`/`gross_amount`), and a
  later call returns THOSE, not a fresh computation against the current `fee_bps` — a re-quote would
  contradict a transfer that already exists. Before funding, with no committed pair yet, they are
  computed live. Every later call returns the same address (idempotent by design, not only by
  `Idempotency-Key`). No `expires_at` — written by nothing today, cut per D-8.
required:
  - deposit_address
  - amount
  - buffer
  - amount_is_estimate
  - fee_estimate
  - gross_estimate
  - chain
  - deposit_message
  - transfer_reference
  - refund_address
properties:
  deposit_address:
    type: string
    description: |
      Case-preserved verbatim — never lower-cased, never re-cased. Reproduce it exactly as returned.
  amount:
    $ref: '#/components/schemas/Money'
    description: >
      The source amount to send, in `source_asset`: the provider's quote rounded UP to the cent plus
      a buffer of at most 0.05 % (`buffer`), so the recipient receives the full invoice. A receipt
      above the invoice within that buffer still settles. Always an estimate, never an exact total —
      the rate can move before the funds arrive.
  buffer:
    description: >
      The part of `amount` that is the buffer (at most 0.05 % of the quote rounded up to the cent),
      published so a screen can state it without computing it. Null on a funding attempt created
      before the buffer existed.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  fee_estimate:
    $ref: '#/components/schemas/Money'
    description: >
      The estimated Swaps fee on this transfer. Computed server-side as `gross = amount × 10000 /
      (10000 − bps)`; clients never recompute.
  gross_estimate:
    $ref: '#/components/schemas/Money'
    description: >
      The estimated gross source amount before the Swaps fee. Computed server-side as `gross =
      amount × 10000 / (10000 − bps)`; clients never recompute.
  amount_is_estimate:
    type: boolean
    enum:
      - true
    description: Always `true` — the source amount is never an exact total (RESOURCE-MODEL §2.2 invariants).
  chain:
    type: string
    enum:
      - base
      - ethereum
      - polygon
      - arbitrum
      - optimism
  deposit_message:
    type:
      - string
      - 'null'
    description: A memo/tag to attach to the transfer, when the chain requires one; null otherwise.
  transfer_reference:
    type:
      - string
      - 'null'
    description: A reference the merchant can quote when tracing the fiat leg.
  refund_address:
    type:
      - string
      - 'null'
    description: >
      The address on `chain` where Bridge returns the USDC if this transfer can't be completed,
      recorded only after Bridge accepted it as the transfer's return address (EIP-55 checksummed).
      `null` when none was set: Swaps then has no refund address on file, and a deposit Bridge
      cannot deliver may wait in `missing_return_policy` (the payout reads `processing` with
      `needs_attention`).
  sandbox:
    type: boolean
    enum:
      - true
    description: >
      Present, and `true`, only on a test-mode (`livemode: false`) response: `deposit_address` is a
      provider-sandbox address on `chain`, not a real one. Never send real funds to it. Absent on
      every live response.
  funding_source:
    type: string
    enum:
      - swaps_wallet
    description: >
      Present only when this attempt is funded from the payer's Swaps wallet («Wallet balance»), on
      every read and fund of the attempt whatever the request body; absent means the payer sends
      USDC to `deposit_address` from any wallet («Another wallet»). While present, do not send from
      another wallet.
  wallet_send_intent:
    $ref: '#/components/schemas/SendIntent'
    description: >
      With `funding_source: swaps_wallet`: the unsigned send from the payer's own Tempo wallet to
      exactly `deposit_address` on `chain`. Its `amount` is what leaves the wallet (every cost of
      the leg included), `amount_out` what Relay guarantees to deliver. Sign
      `send_instructions.steps[]` with the wallet passkey only in the session `payouts.fund` handed
      it to (one session per send, never re-handed), then record the hash from that session through
      `POST /v1/wallet/send_intents/{id}/source_tx`. Returned only to a dashboard session; a
      business key sees `funding_source` alone.
additionalProperties: false
```

<a id="payoutfundrequest"></a>
## PayoutFundRequest

```yaml
type: object
description: >
  Optional body of `payouts.fund` (DS-14 «Pay with»). Omit it for the implicit fund. `source_chain`
  and `funding_source: swaps_wallet` are mutually exclusive — for wallet funding the server chooses
  the chain. `refund_address` is for «Another wallet» only.
properties:
  funding_source:
    type: string
    enum:
      - external_wallet
      - swaps_wallet
    description: >-
      `external_wallet` (default): send USDC from any wallet. `swaps_wallet`: fund from Wallet
      balance.
  source_chain:
    type: string
    enum:
      - base
      - ethereum
      - polygon
      - arbitrum
      - optimism
    description: >
      The USDC chain to fund from; only before a provider transfer exists (`409
      payout_source_chain_locked` afterwards). Required for a payout created without `source_chain`
      (`400 payout_source_chain_missing` otherwise, nothing changed), unless `funding_source` is
      `swaps_wallet`.
  refund_address:
    type: string
    description: >
      Where Bridge returns the USDC if this transfer can't be completed. An address on
      `source_chain` that you control. Not accepted with `funding_source: swaps_wallet`. EVM only
      (`0x` and 40 hex characters), never the zero address; a mixed-case address must carry a valid
      EIP-55 checksum, and the address is stored and sent in its EIP-55 form. Omit it to leave the
      current one unchanged; once set it can be changed only while the transfer awaits funds, never
      removed.
additionalProperties: false
```

<a id="payoutwalletfundingquote"></a>
## PayoutWalletFundingQuote

```yaml
type: object
description: >
  Whether «Wallet balance» can fund this payout and what it would cost. Every Money is
  USDC-denominated with 6 decimals: `required_delivery` in USDC on `source_chain`, the rest in
  USDC.e on Tempo. Null figures whenever `available` is false.
required:
  - funding_source
  - available
  - reason
  - source_chain
  - required_delivery
  - wallet_send_estimate
  - leg_cost
  - wallet_balance
  - covers
  - short_by
  - expires_at
properties:
  funding_source:
    type: string
    enum:
      - swaps_wallet
  available:
    type: boolean
  reason:
    type:
      - string
      - 'null'
    enum:
      - flag_off
      - wallet_not_provisioned
      - wallet_paused
      - route_unavailable
      - test_mode
      - marked_sent
      - null
    description: >
      Why Wallet balance is unavailable; null when `available` is true. `marked_sent`: the payout is
      already marked sent with no recorded wallet send.
  source_chain:
    type:
      - string
      - 'null'
    enum:
      - base
      - arbitrum
      - ethereum
      - null
    description: The chain the server would fund from (first of base, arbitrum, ethereum open to both sides).
  required_delivery:
    description: >
      What must arrive at the deposit address: the funding estimate rounded up to the cent plus a
      buffer of at most 5 bps.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  wallet_send_estimate:
    description: What leaves the wallet, the Relay leg's own cost included (no Swaps fee on the leg).
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  leg_cost:
    description: >-
      `wallet_send_estimate` minus `required_delivery` — the cross-network leg, published, never
      hidden.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  wallet_balance:
    description: The wallet's spendable USDC.e right now.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  covers:
    type: boolean
    description: '`wallet_balance` ≥ `wallet_send_estimate`; false whenever `available` is false.'
  short_by:
    description: How much more USDC.e the wallet needs; null when it covers.
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  expires_at:
    type:
      - string
      - 'null'
    format: date-time
    description: When to re-read this quote (the prepared send's own expiry once one exists).
additionalProperties: false
```

<a id="payoutreceipt"></a>
## PayoutReceipt

```yaml
type: object
description: >
  Exists only once a payout has settled — a payout paid with a shortfall is terminal and never
  produces one (RESOURCE-MODEL §2.2 invariants).
required:
  - payout_id
  - recipient_amount
  - actual_source
  - fee
  - gross
  - provider_reference
  - settled_at
  - beneficiary
properties:
  payout_id:
    type: string
    pattern: ^po_
  recipient_amount:
    type: object
    description: The confirmed amount the recipient actually received — never an estimate on a receipt.
    required:
      - amount
      - confirmed
    properties:
      amount:
        $ref: '#/components/schemas/Money'
      confirmed:
        type: boolean
        enum:
          - true
    additionalProperties: false
  actual_source:
    $ref: '#/components/schemas/Money'
    description: The confirmed amount actually sent from the source chain.
  fee:
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
    description: >
      The Swaps fee actually charged on this transfer, read from provider-confirmed settlement state
      — never an estimate, on a receipt or anywhere else. **`null` unless observed**, and today it
      is always `null`: the only fee figures a payout carries are `computePayoutFee`'s pre-funding
      projections, written at funding commit, and that function's own contract is that it produces
      an estimate, not realized revenue — the settlement webhook remains the SSOT. Publishing a
      projection here would state as observed something nothing observed. Those projections are
      published under `estimates` below instead. This field becomes non-null the day a
      settlement-confirmed fee is recorded in normalized state, and not before (SEC-D / P1-10).
  gross:
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
    description: >
      The gross source amount actually charged, before the Swaps fee — read from provider-confirmed
      settlement state, never an estimate. **`null` unless observed**, under exactly the same
      reasoning as `fee` above: the pre-funding projection (`gross = amount × 10000 / (10000 −
      bps)`) is a computation, not an observation, and is published as `estimates.gross_estimate`.
  estimates:
    type: object
    description: >
      The estimates shown before settlement, kept for the record — never authoritative. Present
      whenever the payout carries the fee projection written at funding commit; `source_estimate` is
      omitted when no source projection was stored for this payout.
    required:
      - fee_estimate
      - gross_estimate
    properties:
      fee_estimate:
        $ref: '#/components/schemas/Money'
      gross_estimate:
        $ref: '#/components/schemas/Money'
      source_estimate:
        $ref: '#/components/schemas/Money'
    additionalProperties: false
  provider_reference:
    type: string
    example: brg_7f3a2c48e1
  settled_at:
    type: string
    format: date-time
  beneficiary:
    $ref: '#/components/schemas/PayoutBeneficiary'
  note:
    type:
      - string
      - 'null'
    maxLength: 280
additionalProperties: false
```

<a id="payoutattempt"></a>
## PayoutAttempt

```yaml
description: One funding attempt on a payout — mirrors the payment-links `payments` shape.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - object
      - status
      - source_chain
      - source_currency
    properties:
      id:
        type: string
        pattern: ^poa_
      object:
        type: string
        enum:
          - payout_attempt
      status:
        type: string
        enum:
          - awaiting_funds
          - funds_received
          - payment_processed
          - returned
          - failed
          - abandoned
          - expired
      source_chain:
        type: string
        enum:
          - base
          - ethereum
          - polygon
          - arbitrum
          - optimism
      source_currency:
        type: string
      source_tx_hash:
        type:
          - string
          - 'null'
      refund_address:
        type:
          - string
          - 'null'
        description: >
          The return address Bridge accepted for this attempt's transfer (EIP-55); null when none
          was set.
unevaluatedProperties: false
```

<a id="payoutattemptlist"></a>
## PayoutAttemptList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/PayoutAttempt'
unevaluatedProperties: false
```

<a id="payouteventlist"></a>
## PayoutEventList

```yaml
description: >
  This payout's own event history — the only timestamp source for the 4-step tracker ladder
  (RESOURCE-MODEL §2.2 v2 amendments). Items share the global event envelope and `data.object`
  carries this payout's own allowlisted projection. Rows are read from the payout's own ledger
  (`payout_events`), not from the `api_events` outbox, and every row with a public counterpart is
  projected to its `payout.*` member of `EventType` — including the members `x-swaps-event-status`
  marks `catalogued` (never delivered to a webhook and never listed by `/v1/events` yet). An
  internal-only row with no public counterpart is skipped.
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Event'
unevaluatedProperties: false
```

<a id="payouteligibility"></a>
## PayoutEligibility

```yaml
type: object
description: >
  The holder's readiness to create and fund payouts, per corridor (RESOURCE-MODEL §2.2). Not
  paginated.
required:
  - kyc
  - corridors
  - capability_contract
properties:
  kyc:
    type: object
    required:
      - status
    properties:
      status:
        type: string
        description: The holder's KYC/KYB verification status (mirrors `customers`).
    additionalProperties: false
  corridors:
    type: array
    items:
      type: object
      required:
        - currency
        - fiat_rail
        - status
      properties:
        currency:
          type: string
          description: >-
            ISO 4217, uppercase (`EUR`, never `eur`) — the same casing as `Money.currency` and
            `Capabilities.corridors[].currency`, so a client can join this array to the capabilities
            catalogue on `(currency, fiat_rail)` without normalizing case on either side.
          example: EUR
        fiat_rail:
          type: string
          enum:
            - ach
            - wire
            - sepa
            - faster_payments
            - pix
            - spei
        status:
          type: string
          enum:
            - available
            - action_required
            - in_review
            - gathering_no_path
            - not_started
            - not_enabled
        blocker:
          description: >
            Set when `status` is not `available`. `in_review` and `gathering_no_path` are kept
            visually distinct even though both are muted, no-color states — never conflate them.
          oneOf:
            - type: 'null'
            - type: string
              enum:
                - kyc
                - address
                - tos
                - source_of_funds
                - additional_details
                - endorsement
                - capability
      additionalProperties: false
  capability_contract:
    type: object
    description: >
      The same fee/corridor contract as `GET /v1/capabilities?product=payouts` — not decomposed
      further here; read that resource for the full shape.
    additionalProperties: true
additionalProperties: false
```

<a id="payrollrunstatus"></a>
## PayrollRunStatus

```yaml
type: string
x-swaps-open-enum: true
description: |
  A1-2: open — an unrecognized member is an opaque string, never a deserialization failure.

  The run's lifecycle. `draft` and `approved` are DB-legal but not reachable through a
  `/v1` action of this caller's own: `runs_create` writes `reviewed` directly and
  `runs_approve` writes `funding_pending` directly, so neither state is ever produced by
  this API — only an internal ops webhook (or a seeded row) can leave one in `draft` or
  `approved`. Both are published in this enum regardless (G decision 2026-09-14):
  `GET /v1/payroll_runs` reads real rows, and a caller-unreachable status is still a
  status a real row can carry — omitting it 500'd the entire list the moment one such row
  existed, rather than 200ing every row but that one. `funded`: an inbound funding event
  was observed — it does not mean the run is fully funded; read `funding_state`,
  `funding_confirmed_amount` and `needs_attention`. Execution is refused for a `funded` run
  whose `funding_state` is not `funded` or `overfunded`.
enum:
  - draft
  - reviewed
  - approved
  - funding_pending
  - funded
  - executing
  - completed
  - partial
  - failed
  - cancelled
```

<a id="payrollrunitemstatus"></a>
## PayrollRunItemStatus

```yaml
type: string
description: >-
  The item's payout lifecycle inside its run (R14-payroll §4). No item-level `cancelled` exists —
  cancelling a run leaves its items wherever they were.
enum:
  - needs_destination
  - ready
  - queued
  - processing
  - paid
  - failed
  - returned
```

<a id="payrollblockercode"></a>
## PayrollBlockerCode

```yaml
type: string
x-swaps-open-enum: true
description: |
  Additive reason codes blocking approval, funding or execution, reconciling the two live
  dialects behind `BLOCKER_CODE_ALIASES` (R14-payroll §4, PR-G20): a **cached** check (4
  codes, drawn on the Today hero and the readiness gate before a live Bridge round trip) and
  a **fresh** check (7 codes, drawn in full on the readiness gate — `pr-gate`). `tos_not_accepted`
  is the one code both dialects share. New codes are appended, never renumbered or removed.
  `run_has_no_rows` (PR-APPROVE-ROWS) is a run-state rung, not a payer check: the run has
  zero rows, so `approve` and `fund` refuse it with `409 run_has_no_rows`.
  A1-2: open — an unrecognized member is an opaque string, never a deserialization failure.
enum:
  - bridge_customer_missing
  - bridge_customer_type_unsupported
  - verification_not_approved
  - tos_not_accepted
  - payer_customer_missing
  - kyc_not_approved
  - missing_address_data
  - requirements_outstanding
  - no_base_endorsement
  - no_settlement_endorsement
  - run_has_no_rows
```

<a id="payrollrun"></a>
## PayrollRun

```yaml
type: object
description: |
  A pay run: a batch of payments to recipients, funded once by the employer and executed as
  one unit. When enabled, the fee is 1% of each recipient's amount, **employer-funded on
  top**, rounded per item in minor units. The gross transfer amount is recipient amount
  plus fee (100 + 1 = 101); Bridge receives the fee as an absolute `developer_fee`, not a
  percentage of that gross amount. Default caps are 25,000 on the run's funding total
  (including any enabled fee) and 10,000 on each recipient amount, in the run's own
  currency with no FX conversion. These server-configurable amount caps are enforced at
  `create` and `execute`; the 500-row limit is checked at creation, not repeated by the
  execute amount-cap check. `funded_at` marks that a
  funding event was observed — never that the run is *fully* funded. `funding_state` is the
  amount-aware verdict, and it, not `status`, is what `execute` reads.

  `funding_amount` (D-PF-8, BL-24) is the GROSS total the employer funds and always was —
  unchanged in meaning. `funding_fee` and `funding_net_amount` are additive: `funding_fee`
  is the 1% Swaps fee baked into `funding_amount` (`0` for every run created while the fee
  is dark-launched off), and `funding_net_amount` is the aggregate of the run's recipient
  amounts — what the run funds them for, not a settlement receipt: a `partial`/`failed` run's
  `failed`/`returned` items are still counted, so this is what recipients were scheduled to
  receive, not necessarily what settled. `funding_amount == funding_fee + funding_net_amount`
  always holds. A client renders this split from these three server figures — never
  re-derives a fee by applying a percentage to `funding_amount` itself, which is exactly how
  a client would go stale the moment the fee bps or the fee-on/off state changed server-side
  without a client release.

  `funding_fee`/`funding_net_amount` are published on this REST projection only (`GET
  /v1/payroll_runs`, `GET /v1/payroll_runs/{id}`, `GET /v1/payroll_runs/{id}/events`). The
  `payroll_run.*` event object delivered via `GET /v1/events`, the SSE stream and webhooks
  carries `funding_amount` alone — a consumer of the outbox feed that needs the split
  re-reads the run over REST rather than re-deriving a fee client-side.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - status
      - title
      - currency
      - pay_period_start
      - pay_period_end
      - funding_method
      - recipient_summary
      - funding_state
      - needs_attention
    properties:
      id:
        type: string
        pattern: ^pr_
        example: pr_01J9ZK3Q8M2F5A7C9E1G3H5J7K
      object:
        type: string
        enum:
          - payroll_run
        example: payroll_run
      status:
        $ref: '#/components/schemas/PayrollRunStatus'
      title:
        type: string
      currency:
        type: string
        description: ISO 4217 code the run pays in.
      pay_period_start:
        type:
          - string
          - 'null'
        format: date
        description: >-
          `payroll_runs.pay_period_start` is a NULLABLE DB column — the internal create path
          (`optionalDate`) writes `null` whenever the caller omits it, and most real rows do (prod:
          59 of 74 rows, 21 of 25 employers, have a null pay period on at least one side).
          `respondValidated`'s `.strict()` parse used to require a string here, so that dominant row
          shape 500'd the entire list the same way the missing `draft`/`approved` enum values did
          (BL-15) — the key stays present (never omitted), the value is null when the run has no pay
          period on record.
      pay_period_end:
        type:
          - string
          - 'null'
        format: date
        description: Nullable for the same reason as `pay_period_start` — see that field.
      memo:
        type:
          - string
          - 'null'
      funding_amount:
        $ref: '#/components/schemas/Money'
      funding_fee:
        $ref: '#/components/schemas/Money'
      funding_net_amount:
        $ref: '#/components/schemas/Money'
      funding_confirmed_amount:
        anyOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
        description: >-
          The inbound amount the funding classifier attributed to this run and compared against
          `funding_amount`. Denominated in the currency the money actually arrived in: the run's own
          currency on the bank rail, `USDC` on the crypto rail, where the requirement itself was
          converted before it was compared. It is never restated into another currency. Null means
          no amount was ever confirmed — and a run with a null value here cannot execute.
      funding_state:
        type: string
        enum:
          - unfunded
          - partially_funded
          - funded
          - overfunded
        description: >-
          The funding classifier's verdict, independent of `status`. `unfunded`: nothing
          attributable arrived. `partially_funded`: money arrived and is short of `funding_amount` —
          `payroll_runs.execute` answers `409 payroll_run_underfunded` naming the shortfall.
          `funded`: the confirmed amount matches. `overfunded`: more arrived than was asked for; the
          run may execute and the surplus is recorded.
      needs_attention:
        type: boolean
        description: >
          Derived from `funding_state`: true when money was observed but the amount is short, is in
          surplus, or

          was never confirmed at all. Advisory — the refusal that actually protects the money is on
          `execute`.
      funding_method:
        type: string
        enum:
          - fiat
          - crypto
        description: |
          How the employer funds the run: `fiat` by bank transfer to a Bridge virtual account,
          `crypto` by sending USDC to the employer's Bridge payroll wallet. `crypto` is offered
          per currency by `GET /v1/capabilities?product=payroll`
          (`funding_currencies[].crypto_funding`); while crypto funding is switched off it
          answers `409 capability_unavailable` on `create`, `approve` and the funding call — it
          is never silently substituted with a fiat instruction.
      pool_asset:
        type:
          - string
          - 'null'
        description: >-
          The asset this run's funding pool holds — what the deposit becomes in the employer's
          Bridge payroll wallet and what every payout is paid out of. `USDC` for every run, or
          `EURC` for an EUR run whose recipients chose EURC wallets, where the EURC pool is enabled
          for the account. Chosen at `approve` from the wallet destinations (one pool per run: a run
          that mixes EURC and USDC wallets answers `409 run_mixed_pool`) and never changed once
          money was observed (`409 pool_asset_locked`). Null while the run's funding records no
          pool: before approve (and on a run cancelled before it), or on an older approved run whose
          funding recorded no wallet. Read-only.
      funding_instructions:
        anyOf:
          - $ref: '#/components/schemas/PayrollFundingInstructions'
          - type: 'null'
        description: Present once a funding attempt has been requested; null before that.
      recipient_summary:
        type: object
        description: Counts over this run's items, by destination readiness.
        required:
          - ready
          - waiting
          - total
        properties:
          ready:
            type: integer
          waiting:
            type: integer
          total:
            type: integer
      approved_at:
        type:
          - string
          - 'null'
        format: date-time
      funded_at:
        type:
          - string
          - 'null'
        format: date-time
        description: A funding event was observed for this run — not proof the run is fully funded.
      completed_at:
        type:
          - string
          - 'null'
        format: date-time
unevaluatedProperties: false
```

<a id="payrollruncreaterequest"></a>
## PayrollRunCreateRequest

```yaml
type: object
description: |
  Drafts a run. `runs_create` writes `reviewed` directly — there is no `draft` state on
  `/v1`. The default amount caps are 25,000 on the funding total (including any enabled fee)
  and 10,000 per recipient amount, in the run currency without FX conversion. Exceeding
  either configured amount cap answers `422`; these amount caps are checked again at
  `execute`. More than 500 rows fails create request validation with `400`; the execute
  amount-cap check does not repeat the row-count limit.
required:
  - title
  - currency
  - pay_period_start
  - pay_period_end
  - funding_method
  - items
properties:
  title:
    type: string
  currency:
    type: string
  pay_period_start:
    type: string
    format: date
  pay_period_end:
    type: string
    format: date
  memo:
    type: string
  funding_method:
    type: string
    enum:
      - fiat
      - crypto
    description: >-
      Offer `crypto` only when `GET /v1/capabilities?product=payroll` shows
      `crypto_funding.available` for this currency. `create` refuses it with `409
      capability_unavailable` only while crypto funding is switched off (`crypto_funding.reason:
      crypto_funding_not_enabled`). For `bridge_funding_currency_unsupported` or
      `verification_required` the run is accepted and cannot be funded: `approve` refuses it or its
      funding instructions read `blocked`.
  items:
    type: array
    minItems: 1
    maxItems: 500
    items:
      type: object
      required:
        - recipient
        - amount
      properties:
        recipient:
          type: object
          description: >-
            Identifying details only — the destination is added later by the recipient, through
            their own session.
          required:
            - display_name
            - email
          properties:
            display_name:
              type: string
            email:
              type: string
              format: email
            country:
              type: string
            worker_type:
              type: string
        amount:
          $ref: '#/components/schemas/MoneyPositive'
additionalProperties: false
```

<a id="payrollrunlist"></a>
## PayrollRunList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/PayrollRun'
      summary:
        $ref: '#/components/schemas/PayrollRunListSummary'
unevaluatedProperties: false
```

<a id="payrollrunlistsummary"></a>
## PayrollRunListSummary

```yaml
type: object
x-swaps-since: '2026-09-25'
description: >-
  Present only with `?expand=summary` (LIST-SUMMARY-1). Computed over every run matching the
  request's `status_group` (never `cursor`/`limit`). The three counts are the Payroll hub's own
  sections, NOT the `status_group` buckets: `needs_action` = draft, approved, reviewed,
  funding_pending, funded, partial, failed; `in_progress` = executing; `completed` = completed,
  cancelled. A status this version does not know counts in `total` only.
required:
  - total
  - needs_action
  - in_progress
  - completed
properties:
  total:
    type: integer
    minimum: 0
  needs_action:
    type: integer
    minimum: 0
  in_progress:
    type: integer
    minimum: 0
  completed:
    type: integer
    minimum: 0
additionalProperties: false
```

<a id="payrollrunitem"></a>
## PayrollRunItem

```yaml
type: object
description: >-
  One recipient's line inside a run — a masked destination projection, never the raw bank or wallet
  detail (mirrors the `payouts` beneficiary projection).
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - run_id
      - recipient_snapshot
      - destination_status
      - status
      - amount
    properties:
      id:
        type: string
        pattern: ^pri_
        example: pri_01J9ZK3Q8M2F5A7C9E1G3H5J7K
      object:
        type: string
        enum:
          - payroll_run_item
      run_id:
        type: string
        pattern: ^pr_
      recipient_snapshot:
        type: object
        description: Frozen at run creation — does not track later edits to the recipient record.
        properties:
          display_name:
            type: string
          email:
            type: string
            format: email
          country:
            type: string
          worker_type:
            type: string
      destination_snapshot:
        anyOf:
          - type: object
            description: >-
              Masked — rail, country, bank name (where present) and the last 4 characters only.
              Never the raw account or wallet detail.
            properties:
              rail:
                type: string
                description: >-
                  The bank destination's settlement rail (UK Faster Payments is `faster_payments`
                  here). For a crypto destination this instead carries the chain name — the same
                  masked field is shared by both kinds.
              country:
                type: string
              bank_name:
                type: string
              last4:
                type: string
            additionalProperties: false
          - type: 'null'
        description: Null while `destination_status` is `missing`.
      destination_status:
        type: string
        enum:
          - missing
          - ready
      destination_changed_after_approval:
        type: boolean
        description: |
          Planned — no producer exists yet (`payroll_run_items` carries no such column and
          `payroll_events` mints no such event today); a caller must not read this as a live
          signal until a producer ships. Always `false` until then.
      status:
        $ref: '#/components/schemas/PayrollRunItemStatus'
      amount:
        $ref: '#/components/schemas/Money'
      paid_at:
        type:
          - string
          - 'null'
        format: date-time
      local_payout:
        $ref: '#/components/schemas/PayrollLocalPayout'
unevaluatedProperties: false
```

<a id="payrolllocalpayout"></a>
## PayrollLocalPayout

```yaml
type: object
description: >-
  Present only when this item pays a local bank account in another currency than the run's — a USD
  run paying BRL by Pix, MXN by SPEI or COP by Bre-B. The item's `amount` stays in the run currency;
  Bridge converts it once, at payout, at its own rate (its FX spread is inside that rate, never a
  second deduction). `estimate` is Bridge's non-binding figure from when the account was saved;
  `delivered` is exactly what Bridge's receipt says arrived, set only once the payout is `paid` and
  cleared if it is returned — never the estimate in its place.
required:
  - currency
  - rail
  - estimate
  - delivered
properties:
  currency:
    type: string
    enum:
      - BRL
      - MXN
      - COP
    description: The bank account's currency.
  rail:
    type: string
    enum:
      - pix
      - spei
      - bre_b
  estimate:
    anyOf:
      - $ref: '#/components/schemas/PayrollLocalPayoutEstimate'
      - type: 'null'
  delivered:
    anyOf:
      - type: object
        required:
          - amount
          - recorded_at
        properties:
          amount:
            $ref: '#/components/schemas/Money'
          exchange_rate:
            type:
              - string
              - 'null'
            description: Decimal string from Bridge's receipt, when it states one.
          recorded_at:
            type: string
            format: date-time
        additionalProperties: false
      - type: 'null'
additionalProperties: false
```

<a id="payrolllocalpayoutestimate"></a>
## PayrollLocalPayoutEstimate

```yaml
type: object
description: >-
  Bridge's non-binding estimate of what the item delivers in the local currency: the item amount
  (USDC, 1:1 with USD) times Bridge's rate, rounded down to the cent. Labelled `estimate` — what
  arrives is fixed by Bridge at payout.
required:
  - kind
  - rate
  - receive_amount
  - quoted_at
properties:
  kind:
    type: string
    enum:
      - estimate
  rate:
    type: string
    description: Decimal string, local currency per 1 USD.
  receive_amount:
    $ref: '#/components/schemas/Money'
  quoted_at:
    type: string
    format: date-time
  expires_at:
    type:
      - string
      - 'null'
    format: date-time
additionalProperties: false
```

<a id="payrollrunitemlist"></a>
## PayrollRunItemList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/PayrollRunItem'
unevaluatedProperties: false
```

<a id="payrollrunitemlinkreissue"></a>
## PayrollRunItemLinkReissue

```yaml
type: object
description: >-
  The result of `payroll_runs.items.reissue_link` — rotating and re-sending one recipient's
  destination-request link. Inside the handler's own short idempotent-reissue window, the SAME
  still-usable link is returned and no new mail goes out for that call; `email.sent` always reflects
  whether THIS call sent mail, never a prior call's outcome. `email.reason` is always one of the
  members below — a safe CLASS token, never the raw provider body, HTTP status or exception message
  the transport actually reported (that full detail stays in `payroll_events`, off the wire).
  `send_returned_null` means the transport genuinely reported no reason at all, never a stand-in for
  one that was withheld. `superseded_by_concurrent_reissue` means a second, overlapping call rotated
  the link again before this one could confirm its own send.
required:
  - object
  - item
  - link
  - email
properties:
  object:
    type: string
    enum:
      - payroll_run_item_link
  item:
    $ref: '#/components/schemas/PayrollRunItem'
  link:
    type:
      - object
      - 'null'
    description: >-
      The employer's own copy of the recipient's onboarding link. The token appears ONLY inside
      `url`, never as a bare field, and only to this run's own employer (the same trust boundary the
      operation description states). Null on an Idempotent-Replayed response — the token is shown
      once, never re-served from the idempotency store (`AuthenticatedRoute.secretFields`, the same
      mechanism `api_keys`' own `secret` uses).
    required:
      - url
      - expires_at
    properties:
      url:
        type: string
        format: uri
      expires_at:
        type:
          - string
          - 'null'
        format: date-time
    additionalProperties: false
  email:
    type: object
    required:
      - sent
    properties:
      sent:
        type: boolean
      message_id:
        type: string
      reason:
        type: string
        enum:
          - recipient_email_missing
          - reissue_skipped_link_still_valid
          - resend_api_key_missing
          - suppressed_by_preference
          - suppressed_bounce_complaint
          - suppressed_reserved_tld
          - suppressed_test_mode
          - output_lint_failed
          - resend_non_2xx
          - resend_invalid_response
          - send_exception
          - send_failed
          - send_returned_null
          - superseded_by_concurrent_reissue
        x-swaps-open-enum: true
    additionalProperties: false
unevaluatedProperties: false
```

<a id="payrollrecipient"></a>
## PayrollRecipient

```yaml
type: object
description: >-
  A payee referenced by one or more runs. Created only as a byproduct of `create` — there is no
  direct create, update or archive on `/v1`; the one write is `adopt_pending_destination`, which
  saves the destination in `pending_destination` as the default. `email` is genuinely optional:
  existing rows can be email-less — from data that predates this field's own validation, or from the
  legacy dashboard-v1 action handler (`payroll/actions.ts`'s own `optionalEmail`) — even though the
  public `POST /v1/payroll_runs` itself always requires `recipient.email` and accepts no destination
  input of its own (`PayrollRunCreateRequest`; a `/v1` caller cannot currently create an email-less
  row this way — Codex review finding, round 12, correcting an earlier description here that implied
  it could). The sibling `PayrollRunItem.recipient_snapshot.email` and
  `PayrollTemplate.items_snapshot[].recipient.email` already model the identical optionality for the
  identical field on those resources (Codex review finding, round 7 — `email` was required here
  only, out of step with every other resource carrying the same field, and the list route's
  `.strict()` validation of every returned row meant one such legitimately-email-less recipient
  failed the ENTIRE list response with a 500, not just its own row).
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - display_name
      - destination_status
    properties:
      id:
        type: string
        pattern: ^prcp_
        example: prcp_01J9ZK3Q8M2F5A7C9E1G3H5J7K
      object:
        type: string
        enum:
          - payroll_recipient
      display_name:
        type: string
      email:
        type: string
        format: email
      country:
        type: string
      worker_type:
        type: string
      destination_kind:
        type:
          - string
          - 'null'
        enum:
          - bank
          - crypto
          - null
      destination_status:
        type: string
        enum:
          - missing
          - ready
      destination_snapshot:
        anyOf:
          - type: object
            description: >-
              Masked — rail, country, bank name (where present) and the last 4 characters only.
              Never the raw account or wallet detail. Identical shape to
              `PayrollRunItem.destination_snapshot`.
            properties:
              rail:
                type: string
                description: >-
                  The bank destination's settlement rail (UK Faster Payments is `faster_payments`
                  here). For a crypto destination this instead carries the chain name — the same
                  masked field is shared by both kinds.
              country:
                type: string
              bank_name:
                type: string
              last4:
                type: string
            additionalProperties: false
          - type: 'null'
        description: >-
          Non-null only once the employer has adopted a recipient-supplied destination onto the
          roster (`payroll_recipients.adopt_pending_destination`) — for where a specific payment
          actually lands, read `payroll_run_items.destination_snapshot` instead; `null` for every
          recipient with no adopted destination, including rows created before this field existed,
          which are never backfilled.
      pending_destination:
        anyOf:
          - type: object
            description: >-
              A destination the recipient saved through their own payout link that is not their
              default yet. It already pays the run whose link they used; new runs copy it only after
              `payroll_recipients.adopt_pending_destination`. Masked like `destination_snapshot`.
            required:
              - destination_kind
              - destination
              - submitted_at
            properties:
              destination_kind:
                type:
                  - string
                  - 'null'
                enum:
                  - bank
                  - crypto
                  - null
              asset:
                type: string
                description: The wallet's asset symbol (for example `USDC`). Crypto destinations only.
              destination:
                anyOf:
                  - type: object
                    description: >-
                      Masked — rail (the chain for a wallet), country, bank name (where present) and
                      the last 4 characters only. Never the raw account or wallet detail. Same shape
                      as `destination_snapshot`.
                    properties:
                      rail:
                        type: string
                      country:
                        type: string
                      bank_name:
                        type: string
                      last4:
                        type: string
                    additionalProperties: false
                  - type: 'null'
              submitted_at:
                type:
                  - string
                  - 'null'
                format: date-time
                description: >-
                  When the recipient saved it. Send this value back as `expected_submitted_at` to
                  adopt exactly this destination.
            additionalProperties: false
          - type: 'null'
        description: >-
          `null` when nothing is waiting. Independent of `destination_status`: a recipient with a
          ready default can also have a newer destination waiting.
unevaluatedProperties: false
```

<a id="payrollrecipientadoptpendingdestinationrequest"></a>
## PayrollRecipientAdoptPendingDestinationRequest

```yaml
type: object
description: Binds the adoption to the destination the caller showed.
required:
  - expected_submitted_at
properties:
  expected_submitted_at:
    type: string
    format: date-time
    description: >-
      The `pending_destination.submitted_at` value you read. If the recipient saved another
      destination since, the call answers `409 pending_destination_changed` and the default is
      unchanged.
additionalProperties: false
```

<a id="payrollrecipientdestinationadoption"></a>
## PayrollRecipientDestinationAdoption

```yaml
type: object
description: >-
  The result of `payroll_recipients.adopt_pending_destination`. `recipient` carries the new default
  (`destination_status: ready`, `pending_destination: null`). `updated_run_items` are the
  recipient's rows in unapproved runs (`draft`, `reviewed`) that were waiting for a destination and
  now use this one — one run can appear more than once. `skipped_run_items` are rows that keep
  asking the recipient because this destination cannot pay that run's currency.
required:
  - object
  - recipient
  - updated_run_items
  - skipped_run_items
properties:
  object:
    type: string
    enum:
      - payroll_recipient_destination_adoption
  recipient:
    $ref: '#/components/schemas/PayrollRecipient'
  updated_run_items:
    type: array
    items:
      type: object
      required:
        - id
        - run_id
      properties:
        id:
          type: string
          pattern: ^pri_
        run_id:
          type: string
          pattern: ^pr_
      additionalProperties: false
  skipped_run_items:
    type: array
    items:
      type: object
      required:
        - id
        - run_id
        - code
      properties:
        id:
          type: string
          pattern: ^pri_
        run_id:
          type: string
          pattern: ^pr_
        code:
          type: string
          description: >-
            Why the row kept asking the recipient. `destination_currency_mismatch` — the bank
            account's currency or rail does not match the run's currency.
            `destination_currency_unsupported` — wallet payouts are not available for the run's
            currency. `destination_rail_unavailable` — wallet payouts are switched off.
            `destination_not_usable` — any other reason this destination cannot pay the row.
          enum:
            - destination_currency_mismatch
            - destination_currency_unsupported
            - destination_rail_unavailable
            - destination_not_usable
          x-swaps-open-enum: true
      additionalProperties: false
unevaluatedProperties: false
```

<a id="payrollrecipientlist"></a>
## PayrollRecipientList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/PayrollRecipient'
unevaluatedProperties: false
```

<a id="payrolltemplate"></a>
## PayrollTemplate

```yaml
type: object
description: |
  A saved snapshot of a completed run's items, for reuse when drafting a future run. There is
  no "start a run from a template" operation — nothing in the backend consumes
  `items_snapshot` back into a new run yet, so publishing that op would document a capability
  that does not exist (R14-payroll §3, PR-G13).
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - name
      - currency
      - items_snapshot
      - source_run_id
    properties:
      id:
        type: string
        pattern: ^prt_
        example: prt_01J9ZK3Q8M2F5A7C9E1G3H5J7K
      object:
        type: string
        enum:
          - payroll_template
      name:
        type: string
      currency:
        type: string
      items_snapshot:
        type: array
        description: The source run's items at the moment the template was saved.
        items:
          type: object
          properties:
            recipient:
              type: object
              properties:
                display_name:
                  type: string
                email:
                  type: string
                  format: email
                country:
                  type: string
                worker_type:
                  type: string
            amount:
              $ref: '#/components/schemas/Money'
      source_run_id:
        type: string
        pattern: ^pr_
unevaluatedProperties: false
```

<a id="payrolltemplatecreaterequest"></a>
## PayrollTemplateCreateRequest

```yaml
type: object
description: Saves the given run's current items as a reusable template.
required:
  - source_run_id
properties:
  source_run_id:
    type: string
    pattern: ^pr_
  name:
    type: string
    description: Defaults server-side to "<run title> template" when omitted.
additionalProperties: false
```

<a id="payrolltemplatelist"></a>
## PayrollTemplateList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/PayrollTemplate'
unevaluatedProperties: false
```

<a id="payrollfundinginstructions"></a>
## PayrollFundingInstructions

```yaml
type: object
description: |
  How to fund a run. Not a first-class object — it has no id, and `get`/`create-or-refresh`
  both return this shape for the owning run. `verified` means the instructions are ready to
  act on, and stays `verified` (with the same instructions) after money is observed — read the
  run's `status` and `funding_state` for whether it has been funded. Detection runs on a poll,
  not a webhook: every five minutes, and on the crypto route each poll reads the wallet only
  up to a minute before it began, so the run's `status` and `funding_state` can show a real
  transfer about six minutes late. A crypto deposit that could not be priced reads `failed`
  with `failure_reason: fx_unconverted` and `retryable: true`; the next funding call prices
  it again. A crypto run whose deposit details were issued more than 14 days ago is held for
  review: it reads `blocked` with `failure_reason: deposit_window_expired`, `crypto: null` and
  `retryable: false` — a deposit to those details is no longer counted automatically. While
  crypto funding is switched on, a funding call that does not switch method returns those
  same instructions unchanged (it never re-prices the amount or restarts the deposit window);
  while it is off, that call answers `409 capability_unavailable`. A call that switches to
  `fiat` provisions bank instructions instead, unless money was already observed on the run
  (`409 funding_method_locked`). Contact support.
required:
  - status
  - run_reference
  - retryable
properties:
  status:
    type: string
    enum:
      - preparing
      - verified
      - failed
      - blocked
      - pending
  funding_route_type:
    type: string
    description: >-
      The rail family behind these instructions (bank or crypto), driving which of `bank`/`crypto`
      below is populated. A run whose `funding_method` is `crypto` reads `crypto_deposit_to_wallet`
      in every state before `verified`.
  run_reference:
    type: string
    description: '`PAYROLL-` plus the run id, stripped of non-alphanumerics, first 12 characters, upper-cased.'
    example: PAYROLL-3F2A9C1B4D0E
  bank:
    anyOf:
      - $ref: '#/components/schemas/BankDepositInstructions'
      - type: 'null'
    description: |
      Present when `funding_route_type` is `bank`. `beneficiary_name` carries what was previously
      `beneficiary`; `rail` is the single settlement rail for this instruction (the run's own
      `run_reference` above stays on the parent object, not nested here).
  crypto:
    anyOf:
      - $ref: '#/components/schemas/DepositInstructions'
      - type: 'null'
    description: >
      Present when `funding_route_type` is `crypto_deposit_to_wallet` and `status` is

      `verified`: the employer's Bridge payroll wallet — `address` (case-preserved, send to it

      verbatim), `chain` (the wallet's chain, e.g. `solana`), `asset` (`USDC`) and `amount`, the
      run's

      `funding_amount` (fee included) converted to USDC when the instructions were prepared.

      Send the whole `amount` in ONE transfer of `asset` on `chain`: deposits on this route are

      not summed — a short deposit leaves the run `partially_funded`, and a top-up does not

      fund it (contact support). A later funding call without a method switch returns this

      same amount; it is never re-priced under a deposit already sent. This whole object is

      null (never a zero-valued object) when the amount cannot be priced; `status` then reads

      `failed` with `failure_reason: fx_unconverted`. This route answers `capability_unavailable` on

      `create`/`approve`/`refresh` while crypto funding is switched off — it is never returned

      as a silent substitute for the bank shape, and the bank shape is never returned as a

      silent substitute for it.
  failure_reason:
    type:
      - string
      - 'null'
    description: >-
      Normalized only — never the raw Bridge or provider error text. A snake_case code (for example
      `bridge_wallet_address_unavailable`); `provider_error` when the provider failed with text that
      is not published. On `blocked`, the first blocker's code — for example
      `bank_payout_fx_unsupported`, a bank row on a run whose pool is not paid out 1:1 in the run
      currency: a non-USD run paid out of the USDC its funding converted into, or an EUR run's EURC
      pool (bank payouts from an EURC pool are not offered yet) — or `deposit_window_expired`, a
      crypto run whose deposit details were issued more than 14 days ago (held for review; not
      retryable). A BRL (Pix) or MXN (SPEI) bank funding account opens only under the employer's own
      deposit approval at Bridge, checked before the account is requested:
      `funding_endorsement_required` (a verification step is left — finish it, then retry),
      `funding_endorsement_in_review` (Bridge is reviewing it; retry later) or
      `funding_rail_not_available` (Bridge does not offer that deposit rail to this employer, e.g.
      Pix to a non-Brazilian resident; not retryable) on `blocked`, and
      `funding_endorsement_unverifiable` (the approval could not be read; retryable) on `failed`.
  retryable:
    type: boolean
additionalProperties: false
```

<a id="payrollreadiness"></a>
## PayrollReadiness

```yaml
type: object
description: |
  A dry read of the payer-compliance gate `approve`, `create-or-refresh funding` and
  `execute` each check FIRST (Bridge-customer verification, KYC, ToS, rail endorsement), so a
  caller can show why THAT gate is blocked before attempting a mutation. `blockers` is empty
  exactly when `ready` is `true`, but `ready: true` here does not promise the mutation
  succeeds — a state-specific check this preview does not run (destinations, funding, rail,
  caps) can still refuse it separately, with its own error code.
required:
  - ready
  - blockers
properties:
  ready:
    type: boolean
  blockers:
    type: array
    items:
      $ref: '#/components/schemas/PayrollBlockerCode'
additionalProperties: false
```

<a id="payrollattempt"></a>
## PayrollAttempt

```yaml
type: object
description: One provider-side attempt to pay a single run item.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - run_item_id
      - status
    properties:
      id:
        type: string
        pattern: ^pra_
        example: pra_01J9ZK3Q8M2F5A7C9E1G3H5J7K
      object:
        type: string
        enum:
          - payroll_attempt
      run_item_id:
        type: string
        pattern: ^pri_
      provider_id:
        type: string
      rail_type:
        type: string
        x-swaps-open-enum: true
        description: >-
          A provider-scoped rail identifier, not a `Rail` value: `bank_bridge` (bank payout via
          Bridge), `bridge_crypto_wallet` (crypto-wallet payout via Bridge) or `tempo_wallet` (Swaps
          Tempo wallet). Open — a new provider or transport adds a value. A provider-neutral
          vocabulary is an A1-10 (naming freeze) follow-up.
        enum:
          - bank_bridge
          - bridge_crypto_wallet
          - tempo_wallet
      provider_reference:
        type:
          - string
          - 'null'
      status:
        type: string
        enum:
          - queued
          - processing
          - paid
          - failed
          - returned
      normalized_error_code:
        type:
          - string
          - 'null'
      normalized_error_message:
        type:
          - string
          - 'null'
        description: Humanised — never raw engineering vocabulary.
unevaluatedProperties: false
```

<a id="payrollattemptlist"></a>
## PayrollAttemptList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/PayrollAttempt'
unevaluatedProperties: false
```

<a id="payrolleventlist"></a>
## PayrollEventList

```yaml
description: >-
  One run's own event history. Rows are read from the payroll ledger (`payroll_events`), not from
  the `api_events` outbox, and every row with a public counterpart is projected to its
  `payroll_run.*`, `payroll_item.*` or `payroll_template.*` member of `EventType` — including the
  members `x-swaps-event-status` marks `catalogued` (never delivered to a webhook and never listed
  by `/v1/events` yet). An internal-only row with no public counterpart is skipped.
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Event'
unevaluatedProperties: false
```

<a id="payrollrecipientsession"></a>
## PayrollRecipientSession

```yaml
type: object
description: |
  The recipient's own payer-facing view of one run item, resolved from the capability token
  in the path — never from an identity. Reading it has no side effect: unlike a payment
  session, nothing here advances on a mere `GET`.
required:
  - run_title
  - amount
  - for_name
  - status
  - can_update_destination
  - employer_display_name
  - capabilities
properties:
  run_title:
    type: string
  amount:
    $ref: '#/components/schemas/Money'
  for_name:
    type: string
  status:
    type: string
    enum:
      - needs_destination
      - ready
      - locked
    description: >-
      Humanised item state. `locked` covers every status past `ready` — the page shows one line,
      never a per-status detail.
  can_update_destination:
    type: boolean
    description: >-
      Whether `payroll_recipient_sessions.destination.set` will currently accept a submission from
      this token. `status` alone stopped predicting this once a `ready` item's parent run could also
      lock it: a `ready` item reads `can_update_destination: false` once the run has moved past
      `draft`/`reviewed` (`approved` onward), and a `needs_destination` item reads `false` once the
      run is `cancelled`/`completed`/`failed` — nothing will ever be approved against it. Check this
      before rendering the destination form rather than inferring it from `status`.
  employer_display_name:
    type: string
    description: The employer's real business name, not an internal handle.
  rotated_token:
    type:
      - string
      - 'null'
    description: >-
      Only present on `payroll_recipient_sessions.destination.set`, and only for the holder that
      just used the old token. Changing a destination retires the token that did it and mints a
      replacement, so a leaked copy of a payslip link cannot redirect the same payout twice. Use
      this value for every subsequent call; the token in the path you just called is dead.
  destination:
    anyOf:
      - type: object
        description: >-
          The destination saved for this run item, masked: what it is, its rail and the last four
          characters. Never the account number, routing number, IBAN, account holder or wallet
          address.
        required:
          - kind
        properties:
          kind:
            type: string
            enum:
              - bank
              - crypto
          rail:
            type: string
            description: >-
              A bank destination's payout rail (`ach` for a US bank account, `sepa`, …); for a
              crypto destination the network (`base`, …).
          last4:
            type: string
        additionalProperties: false
      - type: 'null'
    description: Null until a destination is saved for this run item.
  local_payout:
    $ref: '#/components/schemas/PayrollLocalPayout'
  capabilities:
    type: object
    required:
      - bank
      - crypto_external_wallet
    properties:
      bank:
        type: object
        required:
          - enabled
        properties:
          enabled:
            type: boolean
          reason:
            type:
              - string
              - 'null'
            description: >-
              Set when `enabled` is false — the live reason this rail is closed, checked in this
              order. `bank_payout_fx_unsupported`: the run is not in the currency its payout wallet
              holds (a EUR run funded into USDC), and a bank payout would be converted back by the
              provider at a second spread, in units the run never stated; choose a USDC wallet
              instead. `bridge_bank_rail_disabled`: bank payouts are switched off.
              `bank_currency_unsupported`: the bank rail does not pay this currency.
              `payroll_recipient_sessions.destination.set` refuses a bank destination on such a run
              with the same code (`bank_payout_fx_unsupported`) or `capability_unavailable` (the
              other two). On a USD run `enabled: true` means a US bank account (ACH) can be saved
              here and passes the same rail checks approve and execute apply.
          local_currencies:
            type: array
            description: >-
              The local bank accounts this recipient may choose instead, beyond a bank account in
              the run currency: a USD run may pay BRL by Pix, MXN by SPEI or COP by Bre-B — Bridge
              converts the item once, at payout, at its rate. Listed only where the run is offered
              them (enabled per payer and per rail); absent when none is. A listed entry with
              `enabled: false` cannot be saved right now: `reason`
              `destination_fx_estimate_unavailable` (Bridge's rate could not be read; retry) or
              `local_payout_below_minimum` (the item is below Bridge's minimum for the rail at
              today's rate).
            items:
              type: object
              required:
                - currency
                - rail
                - enabled
                - minimum
              properties:
                currency:
                  type: string
                  enum:
                    - BRL
                    - MXN
                    - COP
                rail:
                  type: string
                  enum:
                    - pix
                    - spei
                    - bre_b
                enabled:
                  type: boolean
                reason:
                  type:
                    - string
                    - 'null'
                fx_estimate:
                  $ref: '#/components/schemas/PayrollLocalPayoutEstimate'
                minimum:
                  $ref: '#/components/schemas/Money'
              additionalProperties: false
      crypto_external_wallet:
        type: object
        required:
          - enabled
        properties:
          enabled:
            type: boolean
          networks:
            type: array
            items:
              type: string
          reason:
            type:
              - string
              - 'null'
            description: >-
              Set when `enabled` is false — the live reason this rail is closed. `enabled`,
              `networks`, `reason` and `fx_estimate` describe the USDC wallet; every wallet asset
              open for the run is in `assets`. `destination_pool_mismatch`: the run's funding pool
              is not USDC (an EURC run pays EURC wallets only).
          assets:
            type: array
            description: >-
              Every wallet asset this recipient may choose for this run, each with the networks its
              payout is documented on — the same networks the destination write accepts and the
              payout uses. `USDC` mirrors `enabled`/`networks`/`fx_estimate` above. `EURC` appears
              only where the run is offered an EURC pool (enabled per account; an EUR run funded by
              bank transfer) — Base and Ethereum; before the run is funded it carries no
              `fx_estimate` (the provider publishes no EUR→EURC price, so no figure is stated before
              the deposit converts), once funded `realized`: the rate the deposit realized and
              exactly what this item pays. Absent when no wallet asset is open.
            items:
              type: object
              required:
                - asset
                - networks
              properties:
                asset:
                  type: string
                  description: The wallet asset — `USDC` or `EURC`.
                networks:
                  type: array
                  items:
                    type: string
                fx_estimate:
                  type: object
                  description: >-
                    The figure this item pays in `asset`, shaped like `fx_estimate` above:
                    `realized` (the run is funded; exact, rounded down to the provider's two-decimal
                    transfer precision) or, for USDC on a run not in USD before funding, `estimate`
                    (non-binding). Never a float.
                  required:
                    - kind
                    - rate
                    - receive_amount
                    - quoted_at
                  properties:
                    kind:
                      type: string
                      enum:
                        - estimate
                        - realized
                    rate:
                      type: string
                      description: Decimal string, payout asset per unit of the run currency.
                    receive_amount:
                      $ref: '#/components/schemas/Money'
                    quoted_at:
                      type: string
                      format: date-time
                    expires_at:
                      type:
                        - string
                        - 'null'
          fx_estimate:
            type: object
            description: >-
              Present only when `enabled` is true AND the run's currency is not the payout asset's
              own (a EUR run paid in USDC); a USD run pays 1:1 and carries no estimate. `realized`:
              the run is funded, `rate` is the rate the payer's deposit actually converted at, and
              `receive_amount` is exactly what this item pays — rounded down to the provider's
              transfer precision (two decimal places: Bridge accepts at most two on a transfer's
              `amount` and `developer_fee`), expressed as a `Money` at the asset's own scale (USDC:
              6). `estimate`: the run is not funded yet; a non-binding provider estimate, labelled
              as such — the paid amount is fixed only when the deposit converts. Never a float.
            required:
              - kind
              - rate
              - receive_amount
              - quoted_at
            properties:
              kind:
                type: string
                enum:
                  - estimate
                  - realized
              rate:
                type: string
                description: Decimal string, payout asset per unit of the run currency.
              receive_amount:
                $ref: '#/components/schemas/Money'
              quoted_at:
                type: string
                format: date-time
                description: >-
                  For `realized`, when the run was funded — the moment the rate was fixed. For
                  `estimate`, when the provider quoted it.
              expires_at:
                type:
                  - string
                  - 'null'
                description: An `estimate` expires (date-time); a `realized` figure does not and reads `null`.
additionalProperties: false
```

<a id="payrollrecipientdestinationrequest"></a>
## PayrollRecipientDestinationRequest

```yaml
type: object
description: >
  Sets where this run item's payment lands. Bank fields mirror the `payouts` beneficiary

  per-rail matrix — exactly one rail set for the chosen country. The crypto branch answers

  `capability_unavailable` while that rail is closed for the run's currency, regardless of

  the fields supplied. A bank destination on a run whose currency the payout wallet does not

  hold (a EUR run funded into USDC) answers `409 bank_payout_fx_unsupported`, and one while

  the bank rail is off or does not pay the run currency answers `409 capability_unavailable`

  — read `capabilities.bank` off the session first. A USD run takes a US bank account (ACH):

  `account_number` + `routing_number`, `bank_account_type`, and a US mailing address with

  `state`. When the provider refuses the details it answers `422 beneficiary_invalid`, with

  `param` naming the one field it refused when it names exactly one. A crypto destination is a

  `USDC` wallet unless `crypto.asset` says `EURC`; an EURC wallet is accepted only where the

  session lists it in `capabilities.crypto_external_wallet.assets` (otherwise `400

  invalid_request`, as for any asset the run is not paid in), and only on a network

  that asset is paid out on — EURC on Polygon or Arbitrum answers `409 asset_network_unsupported`

  before anything is written. A USD run may also take a local bank account where the session lists
  its currency

  in `capabilities.bank.local_currencies`: a Pix key (BRL), a CLABE (MXN, SPEI) or a Bre-B key
  (COP), with

  `account_holder` and nothing else — no bank name, no address. A local currency the session does
  not list answers

  `409 capability_unavailable`; an item below Bridge's minimum for the rail at today's rate answers

  `409 local_payout_below_minimum`, both before anything is created.
required:
  - destination_kind
properties:
  destination_kind:
    type: string
    enum:
      - bank
      - crypto
  bank:
    type: object
    description: >-
      Required when `destination_kind` is `bank`. Supply exactly the rail's own field set: SEPA
      (`iban` + `bic`) or, for a USD run, ACH (`account_number` + `routing_number`) or a local
      account the session lists (`pix_key`, `clabe` or `bre_b_key`, with `account_holder` only).
      `address` is required for every bank rail, and `bank_name` for every one except ACH (the
      server's own `normalizePayrollBankDestinationInput` refuses `destination_bank_name_required` /
      `destination_beneficiary_address_required` without them, the same beneficiary-address
      requirement the authenticated `payouts` beneficiary shape already carries). On ACH the
      provider resolves the bank from the routing number, so `bank_name` is optional there.
    properties:
      account_holder:
        type: string
      iban:
        type: string
      bic:
        type: string
      account_number:
        type: string
        description: >-
          ACH: 4 to 17 digits (spaces and hyphens are ignored). Never echoed back — the session
          publishes the last four only.
      routing_number:
        type: string
        description: >-
          ACH: the 9-digit ABA routing number, its check digit valid (spaces and hyphens are
          ignored).
      bank_account_type:
        type: string
        enum:
          - checking
          - savings
        description: >-
          ACH only — whether the US account is a checking or a savings account. `checking` when
          absent.
      sort_code:
        type: string
      pix_key:
        type: string
        description: >-
          Brazil (Pix, paid in BRL): the key exactly as registered — a CPF or CNPJ (check digits
          verified; punctuation ignored), an email, a phone number starting with `+55`, or a random
          key (sent lowercase).
      clabe:
        type: string
        description: >-
          Mexico (SPEI, paid in MXN): the 18-digit CLABE, its check digit verified (spaces and
          hyphens are ignored).
      bre_b_key:
        type: string
        description: >-
          Colombia (Bre-B, paid in COP): the key exactly as registered, without spaces (a number
          typed with spaces or hyphens keeps its digits). Never echoed back — the session publishes
          the last four only.
      bank_name:
        type: string
        description: >-
          The recipient's bank name — required alongside `address` for every bank rail except ACH,
          where the provider resolves it from the routing number.
      address:
        type: object
        description: >-
          The beneficiary's own mailing address — required for every bank rail (never guessed from
          `iban`/`clabe`/…). `state` is required for a US address (`country` `US`/`USA`; refused
          `destination_beneficiary_state_required` without it) and optional elsewhere, forwarded
          when present.
        required:
          - street_line_1
          - city
          - postal_code
          - country
        properties:
          street_line_1:
            type: string
          city:
            type: string
          state:
            type: string
            description: >-
              State / province — required for a US address (the two-letter USPS code, e.g. `TX`),
              optional elsewhere; forwarded to Bridge when supplied.
          postal_code:
            type: string
          country:
            type: string
            description: ISO 3166-1 alpha-2 or alpha-3 country code.
        additionalProperties: false
  crypto:
    type: object
    description: Required when `destination_kind` is `crypto`.
    required:
      - network
      - address
    properties:
      asset:
        type: string
        enum:
          - USDC
          - EURC
        description: >-
          The wallet's asset. `USDC` when absent. `EURC` only where the session's
          `capabilities.crypto_external_wallet.assets` lists it. Any other value answers `400
          invalid_request`.
      network:
        type: string
      address:
        type: string
        description: Case-preserved — never lower-cased.
additionalProperties: false
```

<a id="quoterequest"></a>
## QuoteRequest

```yaml
type: object
description: >
  Price a route before ever creating an order. Field names mirror `bestQuoteRequestSchema`
  (`packages/contracts-exchange/schemas.ts`); `from_asset`/`to_asset` already name the currency or
  chain asset, so no separate `fiat_currency` field is published. `from_amount`/`to_amount` are
  plain decimal strings in that asset's major units, not `Money` objects — the zod contract's
  `fromAmount`/`toAmount` take a bare decimal string, never a `Money`-shaped minor-unit integer.
required:
  - side
  - from_asset
  - to_asset
properties:
  side:
    type: string
    enum:
      - buy
      - sell
      - swap
    description: >-
      Direction of the trade. `swap` is crypto-to-crypto; `search` exists in the underlying contract
      but is never a public input (RESOURCE-MODEL §2.4, R15 §3).
  from_asset:
    type: string
    description: Source asset symbol or ISO 4217 fiat code (`bestQuoteRequestSchema.fromAsset`).
  to_asset:
    type: string
    description: Destination asset symbol or ISO 4217 fiat code (`bestQuoteRequestSchema.toAsset`).
  from_amount:
    type: string
    pattern: ^[0-9]{1,15}(\.[0-9]{1,18})?$
    maxLength: 34
    description: >-
      Amount to spend, as a decimal string in `from_asset`'s major units — must be strictly
      positive; the router rejects zero and validates it against the route's resolved minimum
      (`bestQuoteRequestSchema.fromAmount`). Exactly one of `from_amount` / `to_amount` is given;
      the router quotes the other side. Bounded by digit count, not a flat character cap (SEC/BSR-5,
      buy/sell v-fix review 2026-09-17; widened fixer round 1, 2026-09-19) — this string previously
      had no length cap at all: `from_amount "99999999999999999999"` (20 digits) reached a live
      Bridge `backend_native` offer and echoed back as `final_in: "9999999999999999999900"` minor
      units. A flat `maxLength: 18` closed that but was itself too narrow: prod `quote_attempts`
      (last 30 days, widget-originated but pricing the SAME shape an `/v1` destination-amount caller
      sends) has real rows up to 20 characters, e.g. `0.03853376279604058` (19) and
      `0.004231894472103885` (20) — full-precision crypto amounts an ERC-20 caller pricing by
      destination amount routinely sends. The pattern instead bounds the INTEGER part to 15 digits —
      one above the widest documented per-currency ceiling (`CURRENCY_MAX_AMOUNT_MAJOR_UNITS.COP`,
      `packages/config/amountLimits.ts`, 14 integer digits at 0 decimals) — and the FRACTIONAL part
      to 18 digits, comfortably covering every real value above while still rejecting the original
      20-digit integer abuse case. This is a contract-layer SHAPE bound only, not a value ceiling:
      the per-offer/per-route VALUE limit (`resolved_limits.max`, further clamped by a config-owned
      risk ceiling) is enforced separately in `createBridgeNativeOrder`
      (`supabase/functions/api-v1/routes/orders.ts`) — a 15-digit integer string can still spell an
      amount far above a route's own limit without tripping a pattern/length check alone.
      Separately, for `side: 'buy'` only, `POST /v1/quotes` also refuses (`422 amount_invalid`) a
      `from_amount` whose fractional part carries more precision than `from_asset`'s OWN scale
      (BSR-8, `getPrecision` — `packages/config/amountLimits.ts`'s `CURRENCY_PRECISION`, the
      canonical fiat-decimals table `.claude/rules/money.md` names for every amount limit in this
      codebase) — e.g. `"20.999"` for a 2-decimal fiat currency like EUR — a shape this pattern
      deliberately still admits (18 fractional digits, for a real 18-decimal crypto amount) but that
      no fiat rail can move: LIVE evidence (dev) showed a Bridge `backend_native` offer round that
      exact value to EUR 21.00 internally while sealing the raw "20.999" verbatim into the deposit
      instructions, instructing the payer to send an amount SEPA cannot carry. A value with harmless
      trailing zeros beyond the scale (`"20.990"`) is unaffected, as is a real 3-decimal Gulf dinar
      (`"1.001"` KWD/BHD/OMR/JOD — `CURRENCY_PRECISION` lists these at 3, not the naive 2-decimal
      guess an earlier cut of this fix used). This scale check applies to `from_amount` only, never
      `to_amount` (see that field's own description for why), and to `side: 'buy'` only, never
      `sell`/`swap`: `sell`'s `from_asset` is the crypto being sold and `swap` is crypto-to-crypto,
      neither a fiat amount `CURRENCY_PRECISION` has an opinion on — an unlisted ERC-20 (e.g. DAI,
      18 decimals) checked against `getPrecision`'s own 2-decimal fallback would be misjudged and
      refused for a perfectly real amount.
  to_amount:
    type: string
    pattern: ^[0-9]{1,15}(\.[0-9]{1,18})?$
    maxLength: 34
    description: >-
      Amount to receive, as a decimal string in `to_asset`'s major units — must be strictly
      positive, when the caller is pricing by destination amount instead of source amount
      (`bestQuoteRequestSchema.toAmount`). Bounded the same way as `from_amount` (SEC/BSR-5, widened
      fixer round 1) — see that field's description for why. Deliberately NOT held to
      `from_amount`'s BSR-8 per-asset scale check: `from_amount` (or the offer's own `final_in`
      derived from it, when priced in receive-mode) is the one figure this gateway ever seals into a
      live payer-facing deposit instruction, and `to_amount` is not — no route this API exposes
      today seals a raw `to_amount` into anything a payer must send or a rail must move. A caller
      pricing BY destination amount routinely sends the full on-chain precision of what it wants to
      receive (prod `quote_attempts`: `to_amount: "0.004231894472103885"`, 18 fractional digits, for
      `to_asset: "USDC"`, a 6-decimal token) — the SmartRouter still prices that correctly into a
      from-leg the provider itself scales properly, so there is no verbatim-sealing hazard on this
      side to close.
  country:
    type: string
    pattern: ^[A-Z]{2}$
    description: >-
      ISO 3166-1 alpha-2 country of the payer, when known — narrows the payment-method fan-out
      (`bestQuoteRequestSchema.country`). A well-formed but sanctioned or unrecognized code is
      refused with `409 capability_unavailable`, not accepted and quoted (BSR-6, SEC-02) — checked
      against the same catalog `GET /v1/capabilities?product=buy_sell` validates `country` against.
  payment_method:
    type: string
    description: >-
      Preferred payment method id, when known (`bestQuoteRequestSchema.paymentMethod`). Omit to let
      the router rank every connected method.
  from_network:
    type: string
    description: Source chain, for a crypto leg (`bestQuoteRequestSchema.fromNetwork`).
  to_network:
    type: string
    description: >-
      Destination chain, for a crypto leg (`bestQuoteRequestSchema.toNetwork`). Optional here, but a
      Bridge-native `orders.create` (`handoff_mode: backend_native`) requires the WINNING offer's
      own `execution_context.to_network` to be set — a quote priced without it cannot become a
      Bridge-native order (`409 capability_unavailable`). Set it explicitly for any asset with more
      than one public chain.
  region:
    type: string
    pattern: ^[A-Z0-9]{1,3}$
    description: >-
      The person's sub-national region, an uppercase ISO 3166-2 subdivision code without the country
      prefix (e.g. `CA`, `NY`), as on `GET /v1/quote_estimates`. Read only for `country=US`, where
      it must be a US state, DC or outlying-area code (otherwise `400`, `param: region`); ignored
      for every other country. Send the same value the estimate used, so a provider that prices a US
      route only once it knows the state (Stripe Onramp) can quote the offer the estimate showed. A
      hint, not proof of residence — the provider's own checks decide.
  hosted_handoff:
    type: boolean
    description: >-
      D-123 — `true` declares that the caller creates its order through `POST /v1/orders` and
      follows that order's `next_action` to a provider-hosted payment page, including a
      Bridge-native order. Only then are offers priced whose payment can only be completed on such a
      page: today a Bridge buy by COP bank transfer — PSE (`payment_method: co_bank_transfer`),
      which `POST /v1/orders` answers with `next_action` and no `bank_deposit_instructions`. Absent
      or `false`: no such offer is priced (a PSE request then gets no Bridge offer); every other
      offer is unchanged. Send the same value on `GET /v1/quote_estimates`.
additionalProperties: false
```

<a id="provider"></a>
## Provider

```yaml
type: object
description: >
  A provider's identity and, where the caller is pricing or reviewing capabilities, its
  registry-published limits — reused verbatim on `Quote.provider`, `Order.provider` and
  `QuoteProviderResult` (D-26, D-56). `Capabilities.providers[]` (`product=buy_sell`) uses
  `CapabilityProvider` below — a flat schema that duplicates these fields rather than an `allOf`
  extension of this one — so its own `status` vocabulary never collides with
  `QuoteProviderResult.status`, and so composing a second closed schema on top of this closed one
  never produces an unsatisfiable combined schema (K6b — see `CapabilityProvider`'s own
  description).
required:
  - id
  - display_name
properties:
  id:
    type: string
    description: >-
      Stable registry id (e.g. `bridge`, `paybis`, `transak`) — not a `/v1` prefixed object id
      (RESOURCE-MODEL §0.2 lists no provider prefix).
  display_name:
    type: string
  logo_url:
    type: string
    format: uri
    description: >-
      Served from Swaps' own CDN, never recoloured (D-26, brand-asset invariant) —
      `https://swaps.app/providers/ {id}.svg`, the same first-attempt asset path the exchange
      widget's own `ProviderLogo` component requests (K6b).
  kyc_type:
    type: string
    description: >-
      The provider's KYC posture from `PROVIDER_REGISTRY` (D-56) — e.g. whether it verifies before
      or after the first trade. No fixed enum is published upstream; treat as an opaque label.
  min:
    $ref: '#/components/schemas/Money'
    description: This provider's minimum for the route, from the registry (D-56).
  max:
    $ref: '#/components/schemas/Money'
    description: This provider's maximum for the route, from the registry (D-56).
  description:
    type: string
    description: >-
      Short, public-safe copy for this provider, from `PROVIDER_REGISTRY[].description` (K6b) —
      never the internal `notes` field (operational status, env var names, commercial terms).
      Omitted, not guessed, for a provider with no vetted public description yet.
additionalProperties: false
```

<a id="capabilityprovider"></a>
## CapabilityProvider

```yaml
type: object
description: >
  A `Capabilities.providers[]` (`product=buy_sell`) row — every `Provider` field, plus the
  operational state colouring the exchange checkout needs to grey out or hide a degraded/disabled
  provider row without a second round-trip (K6b, §52 C4-D14). A FLAT schema that duplicates
  `Provider`'s own properties rather than an `allOf` extension of it: `Provider` is published closed
  (`additionalProperties: false`) for its own standalone uses (`Quote.provider`, `Order.provider`,
  `QuoteProviderResult`), and composing two independently-closed schemas via `allOf` is
  unsatisfiable under JSON Schema — each branch's own `additionalProperties: false` only recognises
  that branch's own `properties`, so `Provider`'s branch would reject `status` and this schema's own
  `status`-only branch would reject `id`/`display_name` (Codex review, PR #2989). `status` is a
  different vocabulary than `QuoteProviderResult.status` and must never collide with it.
required:
  - id
  - display_name
  - status
properties:
  id:
    type: string
    description: >-
      Stable registry id (e.g. `bridge`, `paybis`, `transak`) — not a `/v1` prefixed object id
      (RESOURCE-MODEL §0.2 lists no provider prefix).
  display_name:
    type: string
  logo_url:
    type: string
    format: uri
    description: >-
      Served from Swaps' own CDN, never recoloured (D-26, brand-asset invariant) —
      `https://swaps.app/providers/ {id}.svg`, the same first-attempt asset path the exchange
      widget's own `ProviderLogo` component requests. Present only when that SVG is actually checked
      in under `public/providers/` — a provider without one yet is omitted here rather than
      advertising a URL that 404s for an external client with no monogram fallback of its own (K6b
      P2 fix, Codex review PR #2989).
  kyc_type:
    type: string
    description: >-
      The provider's KYC posture from `PROVIDER_REGISTRY` (D-56) — e.g. whether it verifies before
      or after the first trade. No fixed enum is published upstream; treat as an opaque label.
  min:
    $ref: '#/components/schemas/Money'
    description: This provider's minimum for the route, from the registry (D-56).
  max:
    $ref: '#/components/schemas/Money'
    description: This provider's maximum for the route, from the registry (D-56).
  description:
    type: string
    description: >-
      Short, public-safe copy for this provider, from `PROVIDER_REGISTRY[].description` (K6b) —
      never the internal `notes` field (operational status, env var names, commercial terms).
      Omitted, not guessed, for a provider with no vetted public description yet.
  status:
    type: string
    enum:
      - available
      - degraded
      - disabled
    description: >-
      Derived from THREE signals, combined, never the static one alone: this provider's
      `RAIL_CAPABILITIES_ REGISTRY` row `lifecycle` (`public` -> `available` (live), `preview` ->
      `degraded` (real code, not fully live — dark-flagged or cohort-gated), `blocked` ->
      `disabled`) — the SAME registry `buildBuySellProviders()` already reads — overridden to
      `disabled` whenever EITHER of the two sanctioned runtime kill switches the quote route's own
      eligibility check reads is tripped right now: `provider_config.enabled = false` (global
      switch), or every `provider_directions` row for this provider is `enabled = false`
      (per-direction switch — a provider with no `provider_directions` rows at all is unaffected). A
      read failure on either check 503s the whole response rather than guessing (Codex review, PR
      #2989 rounds 2-4: a provider an operator had runtime-disabled via either switch otherwise
      still reported `available` here).
additionalProperties: false
```

<a id="quoteproviderresult"></a>
## QuoteProviderResult

```yaml
description: >
  One terminal outcome per provider observed in this call (D-21, BS-G2), ordered by provider ID. A
  usable quote on any payment method wins over that provider's sibling-method failures.
allOf:
  - $ref: '#/components/schemas/Provider'
  - type: object
    required:
      - status
    properties:
      status:
        type: string
        enum:
          - quoted
          - quoting
          - no_offer
          - paused
          - error
        description: >
          `quoted` = this provider returned a priced offer; `quoting` is reserved for future
          streaming and is never returned by synchronous POST /quotes; `no_offer` = it answered with
          nothing executable; `paused` = disabled server-side; `error` = it failed to answer in
          time. Never collapse `no_offer` into `error`.
      offer:
        type: object
        description: >-
          Present only when `status` is `quoted` — this provider's own priced offer, same shape as
          the fields `Quote` carries for the winning route (`quoteOfferSchema`). `payment_method`
          alone stays optional (absent means unknown) — every other field here is a money/pricing
          fact the server never quotes a provider without (BL-35 follow-up, DEV-2).
        required:
          - offer_id
          - rate
          - final_out
          - total_fees
          - expires_at
        properties:
          offer_id:
            type: string
            description: Opaque identifier of this provider's selectable offer; preserve it verbatim.
          payment_method:
            type: string
            pattern: ^[a-z][a-z0-9_]*$
            description: >-
              Canonical method priced for this offer. Preserve it with offer_id; never infer it from
              the provider or another offer. Absent means unknown.
          rate:
            type: string
            description: >-
              Decimal string, `from_asset` per `to_asset` (BSR-9) — the server's own `final_in`'s
              decimal value ÷ `final_out`'s decimal value for THIS offer, computed BEFORE either is
              rounded into the published `Money` fields below (when `final_in` is absent it equals
              the request's `from_amount`), in the SAME two request currencies, computed identically
              for every offer in this response regardless of provider or side (never a provider's
              own internal pricing field, whose basis is not uniform). Because `rate` is derived
              pre-rounding, `rate` × `final_out.amount` (scaled by `final_out.decimals`) can differ
              from `final_in.amount` in the last significant digit(s) when an asset's published
              `Money.decimals` is smaller than the server's own internal precision — do not treat
              that identity as exact. Informational only — NEVER compare offers by `rate`
              (rounding/precision differ by asset). Compare by `final_out` for a `from_amount`
              request (every offer targets a different `final_out` for the same pay amount). For a
              `to_amount` request, compare by `final_in` only among offers whose `final_out` equals
              the requested `to_amount` (lower `final_in` is better there) — an offer whose
              `final_out` differs is not comparable this way; not every provider honors an
              exact-output target.
          final_out:
            $ref: '#/components/schemas/Money'
          final_in:
            $ref: '#/components/schemas/Money'
          total_fees:
            $ref: '#/components/schemas/QuoteFees'
          expires_at:
            type: string
            format: date-time
          eta_seconds:
            type: integer
          resolved_limits:
            $ref: '#/components/schemas/QuoteResolvedLimits'
        additionalProperties: false
      reason:
        type: string
        description: >-
          Present only when `status` is `no_offer`, `paused` or `error` — the dead-end reason code
          (never a raw provider error string).
additionalProperties: false
```

<a id="quotefees"></a>
## QuoteFees

```yaml
type: object
description: >
  Fee breakdown, field names from `quoteFeesSchema` (`packages/contracts-exchange/schemas.ts`). Each
  leg converts to a minor-unit `Money` object at the `/v1` boundary per RESOURCE-MODEL §0.3;
  `network_currency` (the zod schema's optional currency-of-the-network-fee field) is folded into
  `network.currency` rather than published twice.
required:
  - provider
  - network
  - total
properties:
  provider:
    $ref: '#/components/schemas/Money'
    description: >-
      The provider's own fee (`quoteFeesSchema.provider`). On a Bridge route it is what Bridge
      itself withholds: on a `wire` buy, Bridge's wire fee (a flat amount passed through at cost,
      named «Bridge wire fee», listed in `/v1/capabilities?product=buy_sell` `provider_fees`; a
      sell's outgoing wire carries none); on USDT, its 0.1 % USD ↔ USDT conversion fee, taken from
      what the developer fee leaves (`final_out` is the delivered amount, truncated to the cent); on
      a cross-currency route (EUR, GBP, MXN, BRL, COP), Bridge's FX spread as an amount — what it
      costs on the amount converted, rounded up to the cent; it is already inside `rate` and
      `final_out`, so it is named here, not deducted twice. `0` on a same-currency route with none
      of these.
  network:
    $ref: '#/components/schemas/Money'
    description: >-
      On-chain/network fee (`quoteFeesSchema.network`). `0` on Bridge routes on every rail (Bridge's
      wire fee is `provider`, not network); a fee the payer's own bank charges to send a wire is
      outside the quote.
  total:
    $ref: '#/components/schemas/Money'
    description: Sum of every fee line (`quoteFeesSchema.total`).
  fx:
    $ref: '#/components/schemas/Money'
    description: FX conversion fee, when the route crosses currencies (`quoteFeesSchema.fx`, optional).
  swaps:
    $ref: '#/components/schemas/Money'
    description: >-
      Swaps' own fee, itemized where Swaps charges it directly (Bridge: the developer fee, 1 % of
      the gross source amount today, also shown on Bridge's receipt), in the same currency as the
      other legs and always part of `total`. `0` on a route with no Swaps share at all (Stripe
      Onramp). On routes where Swaps' share is a markup inside the provider's price (Transak,
      Onmeta), it is not itemized: it stays inside `provider`, and `final_out` is always net. See
      public pricing per route. Never read an absent `swaps` as zero.
additionalProperties: false
```

<a id="quoteresolvedlimits"></a>
## QuoteResolvedLimits

```yaml
type: object
description: >
  Field names from `resolvedLimitsSchema`. `min`/`max` share one `currency` rather than each
  carrying their own `Money` object, matching the zod source's flat shape verbatim — RESOURCE-MODEL
  §2.4 lists this field as `resolved_limits{min,max,currency,source}`, not as two nested `Money`
  objects. A1-4 — this schema is ALSO `Capabilities.defaults.resolved_limits`' shape (a producer
  outside this item's file ownership); `min`/`max` gained a `pattern` here rather than becoming
  `Money` objects, so that other producer's existing plain-decimal output is not broken by a shape
  change it was never part of fixing.
required:
  - min
  - max
  - currency
  - source
properties:
  min:
    type: string
    pattern: ^[0-9]+(\.[0-9]+)?$
    description: Decimal amount in `currency`'s major units (`resolvedLimitsSchema.min`).
  max:
    type: string
    pattern: ^[0-9]+(\.[0-9]+)?$
    description: Decimal amount in `currency`'s major units (`resolvedLimitsSchema.max`).
  currency:
    type: string
  source:
    type: string
    description: >-
      Human-readable provenance, e.g. `"Global fallback · converted from USD"`
      (`resolvedLimitsSchema.source`).
additionalProperties: false
```

<a id="quoteexecutioncontext"></a>
## QuoteExecutionContext

```yaml
type: object
description: >-
  The non-PII request facts used to produce this offer. It binds the offer to the caller's exact
  route intent (assets, amount side, optional country, payment method and networks); it is
  descriptive only and does not authorize execution. Order creation must revalidate the quote,
  offer_id and all risk controls.
required:
  - side
  - from_asset
  - to_asset
  - amount_mode
properties:
  side:
    type: string
    enum:
      - buy
      - sell
      - swap
  from_asset:
    type: string
  to_asset:
    type: string
  amount_mode:
    type: string
    enum:
      - from
      - to
    description: Which side of the requested route amount was supplied.
  from_amount:
    type: string
    description: Requested source amount in the source asset's major units, when supplied.
  to_amount:
    type: string
    description: Requested destination amount in the destination asset's major units, when supplied.
  country:
    type: string
    description: Country supplied for route eligibility, when supplied. This is not proof of residency.
  payment_method:
    type: string
    description: Payment method supplied as a caller preference, when supplied.
  from_network:
    type: string
    description: Source crypto network supplied for the route, when supplied.
  to_network:
    type: string
    description: Destination crypto network supplied for the route, when supplied.
  region:
    type: string
    pattern: ^[A-Z0-9]{1,3}$
    description: >-
      The US state the quote was priced with (`QuoteRequest.region`, `country=US` only). An order
      made from this quote carries it to the provider's checkout (Stripe Onramp checks a US route
      against the state).
additionalProperties: false
```

<a id="quoteoffer"></a>
## QuoteOffer

```yaml
type: object
description: >-
  One fully priced, selectable offer from the same provider fan-out as the winning route. The list
  is ordered by the server's price-first ranking. Preserve the provider, offer_id and payment_method
  together; they identify the exact route facts that must be revalidated before execution. Provider
  payloads, redirect URLs and identity metadata are intentionally excluded.
required:
  - provider
  - offer_id
  - rate
  - final_out
  - total_fees
  - expires_at
  - checkout_readiness
  - execution_context
properties:
  provider:
    $ref: '#/components/schemas/Provider'
  offer_id:
    type: string
    description: Opaque provider offer identifier; preserve it verbatim.
  payment_method:
    type: string
    pattern: ^[a-z][a-z0-9_]*$
    description: >-
      Canonical payment method priced for this offer, when the provider returned one. Never infer
      it.
  rate:
    type: string
    description: >-
      Decimal string, `from_asset` per `to_asset` (BSR-9) — the server's own `final_in`'s decimal
      value ÷ `final_out`'s decimal value for THIS offer, computed BEFORE either is rounded into the
      published `Money` fields below (when `final_in` is absent it equals the request's
      `from_amount`), in the SAME two request currencies, computed identically for every offer in
      this list regardless of provider or side (never a provider's own internal pricing field, whose
      basis is not uniform). Because `rate` is derived pre-rounding, `rate` × `final_out.amount`
      (scaled by `final_out.decimals`) can differ from `final_in.amount` in the last significant
      digit(s) when an asset's published `Money.decimals` is smaller than the server's own internal
      precision — do not treat that identity as exact. Informational only — NEVER compare offers by
      `rate` (rounding/precision differ by asset). Compare by `final_out` for a `from_amount`
      request (every offer targets a different `final_out` for the same pay amount). For a
      `to_amount` request, compare by `final_in` only among offers whose `final_out` equals the
      requested `to_amount` (lower `final_in` is better there) — an offer whose `final_out` differs
      is not comparable this way; not every provider honors an exact-output target.
  final_out:
    $ref: '#/components/schemas/Money'
  final_in:
    $ref: '#/components/schemas/Money'
  total_fees:
    $ref: '#/components/schemas/QuoteFees'
  expires_at:
    type: string
    format: date-time
    description: This offer's expiry; re-quote after it expires.
  checkout_readiness:
    type: object
    required:
      - policy_version
      - handoff_mode
      - quote_expires_at
      - checkout_fresh_until
      - safety_window_ms
    properties:
      policy_version:
        type: string
        enum:
          - checkout-readiness-v1
      handoff_mode:
        type: string
        enum:
          - deferred_request
          - hosted_session
          - signed_url
          - backend_native
      quote_expires_at:
        type: string
        format: date-time
      checkout_fresh_until:
        type: string
        format: date-time
      safety_window_ms:
        type: integer
        minimum: 0
    additionalProperties: false
  resolved_limits:
    $ref: '#/components/schemas/QuoteResolvedLimits'
  eta_seconds:
    type: integer
    minimum: 0
  execution_context:
    $ref: '#/components/schemas/QuoteExecutionContext'
additionalProperties: false
```

<a id="quote"></a>
## Quote

```yaml
description: >
  The best executable route across every connected provider, plus the fan-out that produced it.
  `POST /v1/quotes` is the only writer; there is no update.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - side
      - provider
      - rate
      - final_out
      - total_fees
      - expires_at
      - checkout_readiness
      - quote_status
    properties:
      id:
        type: string
        pattern: ^qt_
        example: qt_01J9ZK3Q8M2F5A7C9E1G3H5J7K
      object:
        type: string
        enum:
          - quote
      side:
        type: string
        enum:
          - buy
          - sell
          - swap
      offer_id:
        type: string
        description: Opaque identifier of the winning offer; preserve it verbatim for selection.
      payment_method:
        type: string
        pattern: ^[a-z][a-z0-9_]*$
        description: >-
          Canonical method priced for the winning offer. This is requested funding intent, not proof
          of the eventual received settlement rail. Absent means unknown.
      provider:
        $ref: '#/components/schemas/Provider'
        description: The winning route's provider (D-26).
      rate:
        type: string
        description: >-
          Decimal string, `from_asset` per `to_asset` (BSR-9) — the server's own `final_in`'s
          decimal value ÷ `final_out`'s decimal value for the winning offer, computed BEFORE either
          is rounded into the published `Money` fields below (when `final_in` is absent it equals
          the request's `from_amount`), in the SAME two request currencies, computed identically for
          every offer in this response regardless of provider or side (never a provider's own
          internal pricing field — Bridge's and Coinbase's own rate math disagree on this basis by
          side, which is exactly the bug BSR-9 closed: `docs/api/CHANGELOG.md`). Because `rate` is
          derived pre-rounding, `rate` × `final_out.amount` (scaled by `final_out.decimals`) can
          differ from `final_in.amount` in the last significant digit(s) when an asset's published
          `Money.decimals` is smaller than the server's own internal precision — do not treat that
          identity as exact. Informational only — NEVER compare offers by `rate` (rounding/precision
          differ by asset). Compare by `final_out` for a `from_amount` request (every offer targets
          a different `final_out` for the same pay amount). For a `to_amount` request, compare by
          `final_in` only among offers whose `final_out` equals the requested `to_amount` (lower
          `final_in` is better there) — an offer whose `final_out` differs is not comparable this
          way; not every provider honors an exact-output target.
      final_out:
        $ref: '#/components/schemas/Money'
        description: What the caller receives (`bestQuoteResponseSchema.finalOut`).
      final_in:
        $ref: '#/components/schemas/Money'
        description: >-
          What the caller pays, when it differs from the request amount
          (`bestQuoteResponseSchema.finalIn`).
      total_fees:
        $ref: '#/components/schemas/QuoteFees'
      expires_at:
        type: string
        format: date-time
        description: Quotes expire — re-quote rather than reusing a stale one.
      checkout_readiness:
        type: object
        description: >-
          Field names from `quoteOfferSchema.checkoutReadiness` — tells the caller how the eventual
          order will complete.
        required:
          - handoff_mode
          - quote_expires_at
          - checkout_fresh_until
        properties:
          policy_version:
            type: string
            enum:
              - checkout-readiness-v1
          handoff_mode:
            type: string
            enum:
              - deferred_request
              - hosted_session
              - signed_url
              - backend_native
            description: >-
              `backend_native` completes without a redirect (Bridge-native); the other three hand
              off to a hosted provider (R15 `buysell-checkout.html`).
          quote_expires_at:
            type: string
            format: date-time
          checkout_fresh_until:
            type: string
            format: date-time
          safety_window_ms:
            type: integer
            minimum: 0
        additionalProperties: false
      resolved_limits:
        $ref: '#/components/schemas/QuoteResolvedLimits'
      eta_seconds:
        type: integer
      kyc_level:
        type: string
        description: '`quoteOfferSchema.kycLevel` — the identity tier this route requires, when known.'
      route_facts:
        type: object
        description: >-
          Field names from `routeFactsSchema` — comparison context for a poor-value or
          no-KYC-eligible alternative.
        properties:
          best_price:
            type: object
            properties:
              payment_method:
                type: string
              pay_amount:
                type: string
              receive_amount:
                type: string
              rate:
                type: string
                description: >-
                  Decimal string, `from_asset` per `to_asset` (BSR-9) — fixer round 1 (Codex review,
                  P3): the SAME value as the winning offer's own `rate` (`Quote.rate`; see that
                  field's description, including its `to_amount`-mode caveat), never a second,
                  independent derivation from `pay_amount`/`receive_amount`. Those two fields
                  describe this request's best-price context and are not always the winning offer's
                  own `final_in`/`final_out` (`receive_amount` is the REQUESTED amount, not
                  necessarily the offer's realized one, in `to_amount` mode) — reusing one rate
                  keeps a single winning offer from publishing two disagreeing numbers in the same
                  response. Informational only — never compare offers by `rate`.
              limit:
                type: object
                description: >-
                  A1-4 — `max` is `Money` (was a bare float sharing a sibling `currency` —
                  `root.yaml`'s own "Floats never appear on a money path"); the currency now lives
                  on `Money` itself.
                properties:
                  max:
                    $ref: '#/components/schemas/Money'
                  source:
                    type: string
                additionalProperties: false
            additionalProperties: false
          no_kyc_eligible:
            type:
              - object
              - 'null'
            properties:
              payment_method:
                type: string
              receive_amount:
                type: string
              max_spend:
                type: object
                description: A1-4 — `amount` is `Money` (was a bare float sharing a sibling `currency`).
                properties:
                  amount:
                    $ref: '#/components/schemas/Money'
                  scope:
                    type: string
                    enum:
                      - annual
                      - order
                additionalProperties: false
              warning:
                type: string
            additionalProperties: false
          limit_warning:
            type: object
            description: A1-4 — `max` is `Money` (was a bare float sharing a sibling `currency`).
            properties:
              max:
                $ref: '#/components/schemas/Money'
              tone:
                type: string
                enum:
                  - neutral
                  - warning
            additionalProperties: false
        additionalProperties: false
      quote_status:
        type: string
        enum:
          - ready
          - indicative
        description: >-
          `indicative` came from a warm cache without running the live fan-out — re-quote live
          before acting on it once older than the safety window
          (`bestQuoteResponseSchema.quoteStatus`).
      indicative_as_of:
        type: string
        format: date-time
        description: >-
          When the indicative price was actually fetched — never "now"
          (`bestQuoteResponseSchema.indicativeAsOf`).
      offers:
        type: array
        description: >-
          Every fully priced offer from the same call, including sibling payment methods and
          providers. This additive list is the priced method surface; `providers[]` remains the
          terminal one-row-per-provider diagnostic summary for backward compatibility. An offer
          omitted from this list was not safely priced and must not be inferred from capabilities —
          including when the reason is this gateway being unable to verify that offer's own
          currency/asset decimal scale (fixer round 1, 2026-09-22), a money reason, not a rate one.
          If `payment_method` is absent, the provider did not return a canonical method and the
          client must not present that entry as a selectable named method.
        items:
          $ref: '#/components/schemas/QuoteOffer'
      providers:
        type: array
        description: >
          Terminal outcomes from this call's existing provider offers and diagnostics (D-21, BS-G2).
          No additional provider requests are made to populate these rows. Reasons are public codes;
          raw diagnostics and provider payloads are never exposed.
        items:
          $ref: '#/components/schemas/QuoteProviderResult'
      price_change_threshold_bps:
        type: integer
        description: >-
          The basis-point move that triggers a re-quote prompt on the client (today a server
          constant, 50) — D-27.
    additionalProperties: false
unevaluatedProperties: false
```

<a id="quoteestimate"></a>
## QuoteEstimate

```yaml
type: object
description: >
  An anonymous, indicative price for a buy or a sell, answered from the quote cache without signing
  in (`GET /v1/quote_estimates`). It is not a `Quote`: it has no `id` and no `offer_id`, nothing in
  it can be selected, and `POST /v1/orders` never accepts it — that operation requires a `quote_id`
  from `POST /v1/quotes`, which is the binding price, taken under a credential at the review step.
  Every estimate is `quote_status: indicative`. Each offer carries `identity`, computed by the same
  identity policy the authenticated quote path applies. Amount limits are deliberately absent: a
  cached price does not carry them — read each method's limits from `GET /v1/capabilities/buy_sell`
  (`defaults.resolved_limits`; on a sell, `sell_defaults.resolved_limits` and
  `sell_coverage[].assets[].min`/`max`).
required:
  - object
  - livemode
  - quote_status
  - indicative_as_of
  - expires_at
  - offers
  - providers
properties:
  object:
    type: string
    enum:
      - quote_estimate
  livemode:
    type: boolean
    description: >-
      `true` on production, where every estimate is priced by live providers. The development
      project, which serves provider sandboxes, answers `false`. An anonymous caller has no test
      mode of its own.
  quote_status:
    type: string
    enum:
      - indicative
    description: Always `indicative` — an estimate is a historical price reference, never an executable quote.
  indicative_as_of:
    type: string
    format: date-time
    description: When the providers actually priced this — never "now".
  expires_at:
    type: string
    format: date-time
    description: >-
      When this estimate becomes too old to show: `indicative_as_of` plus the longest age a cached
      price is ever replayed (330 seconds). It is not a provider's quote deadline; request a new
      estimate after it, and a binding quote before acting.
  offers:
    type: array
    description: >-
      Every provider and payment method that priced this route in the cached fan-out, in the
      server's price-first order. Paybis is never included, and on a sell only Bridge is (see
      `providers`). An empty list never reaches the wire — a route with nothing to show is `409
      capability_unavailable`.
    items:
      $ref: '#/components/schemas/QuoteEstimateOffer'
  providers:
    type: array
    description: >-
      Only the providers that priced this route in the cached fan-out (`quoted`), plus Paybis as
      `paused` when it priced it, because `/v1` never offers Paybis — and, on a sell, any provider
      other than Bridge as `paused`, because only Bridge sells through `/v1` today. It is no
      availability claim for any other provider: one that is absent produced no usable cached price,
      and the estimate does not say why — a binding quote reports every provider's own outcome.
    items:
      $ref: '#/components/schemas/QuoteEstimateProvider'
  first_screen:
    type: object
    description: >-
      Only on `GET /v1/quote_estimates/first_screen`: the market the server chose from the country's
      coverage defaults and priced — key the estimate by it. Absent on `GET /v1/quote_estimates`,
      whose caller named the market.
    required:
      - from_asset
      - to_asset
      - to_network
      - from_amount
    properties:
      from_asset:
        type: string
        description: The fiat paid, the country's default (`defaults.default_fiat`), uppercase ISO 4217.
      to_asset:
        type: string
        description: The coin received, the country's default (`defaults.default_asset`).
      to_network:
        type: string
        description: The network the coin is priced on, a lowercase slug (the coin's first network).
      from_amount:
        type: string
        description: >-
          The suggested amount in `from_asset`, within its published limits, as a decimal string in
          major units.
    additionalProperties: false
additionalProperties: false
```

<a id="quoteestimateoffer"></a>
## QuoteEstimateOffer

```yaml
type: object
description: One cached price from one provider and payment method. Nothing here can be selected or executed.
required:
  - provider
  - from_amount
  - to_amount
  - fees
  - identity
properties:
  provider:
    $ref: '#/components/schemas/Provider'
  payment_method:
    type: string
    pattern: ^[a-z][a-z0-9_]*$
    description: >-
      Canonical method this price was given for, when the provider returned one. Absent means
      unknown — never present an entry without it as a selectable named method.
  identity:
    $ref: '#/components/schemas/QuoteEstimateIdentity'
  from_amount:
    $ref: '#/components/schemas/Money'
    description: >-
      What the person gives, in the request's `from_asset` — the fiat paid on a buy, the asset sent
      on a sell.
  to_amount:
    $ref: '#/components/schemas/Money'
    description: >-
      What the person receives, in the request's `to_asset` — the asset on a buy, the fiat paid out
      on a sell.
  fees:
    $ref: '#/components/schemas/QuoteFees'
    description: >-
      The fee lines as the provider priced them: in `from_asset` on a buy; in `to_asset` (the payout
      fiat) on a sell — the currency the binding sell quote states them in.
  eta_seconds:
    type: integer
    minimum: 0
    description: Expected delivery time in seconds, present only when the provider gave one.
additionalProperties: false
```

<a id="quoteestimateidentity"></a>
## QuoteEstimateIdentity

```yaml
type: object
description: >
  Whether this buy or sell needs an identity document, decided by the one identity policy the
  authenticated quote path uses (`_shared/services/identity-policy.ts`): by provider, the payment
  method actually quoted, the fiat currency, the side and the USD equivalent of the requested
  amount. A sell always needs one. Any uncertainty — an unknown method, an amount that cannot be
  converted — answers `required: true`; no-ID is only ever the documented exception (today OnMeta in
  IDR via QRIS and in PHP via QR Ph). It is the provider's policy for this route, not a verdict
  about the person: the provider may still ask, which `warning` states.
required:
  - required
properties:
  required:
    type: boolean
    description: '`true` unless this provider, method and currency are a documented no-ID buy route.'
  max_spend:
    type: object
    description: >-
      The published no-ID allowance, when the route has one — the same shape as
      `Quote.route_facts.no_kyc_eligible.max_spend`. Absent when the provider owns an unpublished
      limit.
    required:
      - amount
      - scope
    properties:
      amount:
        $ref: '#/components/schemas/Money'
      scope:
        type: string
        enum:
          - annual
          - order
    additionalProperties: false
  warning:
    type: string
    description: >-
      Present only with `required:` `false` — the condition under which the provider may still ask
      for ID.
additionalProperties: false
```

<a id="quoteestimateprovider"></a>
## QuoteEstimateProvider

```yaml
type: object
required:
  - provider
  - status
properties:
  provider:
    $ref: '#/components/schemas/Provider'
  status:
    type: string
    enum:
      - quoted
      - paused
    x-swaps-open-enum: true
    description: >-
      `quoted` — this provider priced the route. `paused` — `/v1` does not offer this provider on
      this route (Paybis; on a sell, every provider but Bridge); its price is withheld. Treat an
      unrecognized value as "no offer from this provider".
  reason:
    type: string
    enum:
      - provider_paused
    x-swaps-open-enum: true
    description: Present only when `status` is `paused`.
additionalProperties: false
```

<a id="ordercreaterequest"></a>
## OrderCreateRequest

```yaml
type: object
description: >
  Collapses the browser-shaped `redirect_intent_create` → `redirect_init` → `redirect_execute` chain
  into one idempotent create for an authenticated caller (D-20, BS-G1). The public-token payer
  redirect is a separate surface and is out of this file's scope.
required:
  - quote_id
properties:
  quote_id:
    type: string
    pattern: ^qt_
    description: A `Quote.id` from a prior `POST /v1/quotes`.
  offer_id:
    type: string
    description: >-
      Selects a specific `QuoteProviderResult` offer instead of the winning route, when the caller
      reviewed the fan-out and picked a different provider.
  external_account_id:
    type: string
    pattern: ^ba_
    description: >-
      REQUIRED for a Bridge-native sell (`execution_context.side: sell` on a `backend_native`
      offer): the saved bank destination the fiat payout settles to, from `GET
      /v1/wallet/external_accounts`. It must belong to this account's own Bridge customer, and its
      currency and rail must match the quote's `to_asset` and the offer's payout rail — a mismatch
      is refused, never reinterpreted. Never accepted on a `buy` or `swap`.
  wallet_address:
    type: string
    description: >-
      Crypto destination for a `buy`. NOT accepted on a Bridge-native `sell` (`400 invalid_request`,
      `param: wallet_address`): Bridge matches the incoming deposit from ANY sending wallet, so a
      declared source address would be a promise the server cannot keep. Send from any wallet you
      control to the address in the created order's `deposit_instructions`. With `address_book_id`,
      omit it, or send the entry's address byte for byte (addresses are case-sensitive and never
      case-folded); any other value is `400 invalid_request`, `param: wallet_address`.
  address_book_id:
    type: string
    pattern: ^adr_
    description: >-
      A buy destination from this caller's own address book (`GET /v1/address_book`), instead of a
      typed `wallet_address`: the server reads the address and its network from the entry.
      Dashboard-session callers only — a business key is refused (`403 permission_error`,
      `scope_denied`), as it is on every `/v1/address_book` operation. An id that is not this
      caller's own is `404`. The entry must be `rail: crypto`, saved for the offer's
      `execution_context.to_network` (network names are matched the way screenings match them —
      `Ethereum`, `eth` and `1` are one network; `base` is another), and its latest screening must
      not be `block` (`403 destination_blocked`); anything else is `400 invalid_request`, `param:
      address_book_id`. Not accepted on a sell. The destination is still screened at order time
      exactly like a typed address. After a `201`, the entry's `last_used_at` is set.
  return_url:
    type: string
    maxLength: 2048
    description: >-
      Where the hosted provider sends the payer back after checkout. `https` only, at most 2048
      characters, no user name or password, no query string and no fragment (put an order reference
      in the path; no personal data ever travels in a URL), and its origin must be one of
      `https://swaps.app`, `https://www.swaps.app`, `https://staging.swaps.app`. Anything else is
      `400 invalid_request`, `param: return_url` — never replaced with a default. Absent, the
      provider returns the payer to the Swaps `/success` page. Signed into the checkout and checked
      again before the provider receives it. Used by providers that redirect back (Transak, OnMeta)
      and by a Bridge-native COP PSE buy (`co_bank_transfer`, D-123), where it is REQUIRED — Bridge
      sends the payer back there after its hosted bank-transfer page (absent: `400 invalid_request`,
      `param: return_url`). Stripe Onramp's and Partna's hosted checkouts have no return redirect,
      and every other Bridge-native order has no hosted checkout, so for those it is validated and
      not used.
  email:
    type: string
    format: email
    description: >-
      F-5: ignored. No path reads this field — a hosted checkout's identity is built from this
      account's own verified email (`public.users`), never from the request body, and the internal
      action this route calls has no email parameter to forward one to. Ignore any value sent here;
      do not build client logic on it being used. Kept on the wire in case a future provider path
      needs a caller-suggested address, not because one exists today.
additionalProperties: false
```

<a id="order"></a>
## Order

```yaml
description: >
  A buy, sell or swap in flight or settled. `status` publishes `transactions.status` verbatim. Read
  `money_state` and `money_status` together when `money_state` is present: it says whether funds
  were ever captured, `money_status` carries refund truth (D-10, D-22). `money_state` is published
  only when the Paybis/Bridge failure-classification pipeline has actually classified this row —
  ABSENT, never a guessed `captured`/`never_authorized`, otherwise (BL-38, #3086).
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - status
      - side
    properties:
      id:
        type: string
        pattern: ^ord_
        example: ord_01J9ZK3Q8M2F5A7C9E1G3H5J7K
      object:
        type: string
        enum:
          - order
      status:
        type: string
        x-swaps-open-enum: true
        enum:
          - pending
          - initiated
          - awaiting_payment
          - awaiting_funds
          - processing
          - funds_received
          - payment_submitted
          - in_review
          - refund_in_flight
          - completed
          - payment_processed
          - failed
          - cancelled
          - abandoned
          - refunded
          - expired
          - undeliverable
          - returned
          - refund_failed
        description: >-
          A1-2: open — a new `transactions.status` value is a real, growing set (see K6/BL-36
          history below); an unrecognized member is an opaque string, never a deserialization
          failure. `transactions.status` verbatim (D-22) — `transactions/recovery.ts`'s
          `ACTIVE_STATUSES` + `TERMINAL_STATUSES` + `UNCLASSIFIED_STATUSES` vocabulary (19 values),
          widened from the seven the approved R15 §3 screens draw on (K6 fix: the original enum only
          covered the seven approved terminal/active screens and rejected every other real
          `transactions.status` value as a contract violation). BL-36 (2026-09-16) adds `initiated`
          (3 prod rows) and `abandoned` (1293 prod rows) — the DB `status_check` constraint has
          allowed both since `20260708201001_transactions_status_add_refunded.sql`, but the
          vocabulary here and in `recovery.ts` never caught up: `orders.get` 500ed on either status
          (`respondValidated` rejects a `status` the enum does not list) and a page of `orders.list`
          containing one failed the same way whenever that row also carried a `quote_id`.
          `abandoned` joins `TERMINAL_STATUSES`; `initiated` is admitted here for contract purposes
          only and is deliberately left in `recovery.ts`'s `UNCLASSIFIED_STATUSES` — prod shows
          every live `initiated` row already carries an on-chain `tx_hash` (one is
          `metadata.lifecycle_stage: 'completed'`), so it is a stranded post-broadcast swap the DEX
          reconciler never picked up, not a start-of-lifecycle "no money moved" state; see
          `recovery.ts`'s comment for the full writer/reconciler trace. Never conflate with
          `failure_kind` — the failed screen's "Status code" must read `failure_kind`, not this
          field (BS-G9).
      side:
        type: string
        enum:
          - buy
          - sell
          - swap
      quote_id:
        type: string
        pattern: ^qt_
        description: >-
          An opaque reference to the quote this order came from, when one exists. OPTIONAL — ABSENT,
          never a fabricated id, for an order that has no producer for it (a live write-path gap on
          some provider-native orders, or a swap that never went through a quote at all; see the API
          changelog for current prod scope). NOT resolvable via `GET /v1/quotes/{id}` for a
          historical order: this value can reference `quote_attempts.rid`, a different table than
          the one that endpoint reads, while an order created through `POST /v1/orders` carries a
          `GET /v1/quotes/{id}`-resolvable id instead.
      rate:
        type: string
        description: >-
          Decimal string, `from_asset` per `to_asset`, the SAME basis as `QuoteOffer.rate` (BL-28,
          BS-10; fixer round 2, P1, independent review 2026-09-20 — round 1 published this on
          Bridge's own native, opposite basis, which collided with sibling PR BSR-9/#3357's
          provider-agnostic wire contract). BSR-10 gives this a live producer for a Bridge-native
          `buy` order: read `rate_basis`, below, to know which of two figures this is — never assume
          one from this field's mere presence. The planned read-time source (`transactions.quote_id`
          -> `quotes.id` -> `quotes.offers`) is a dead join in production — `transactions.quote_id`
          has referenced `quote_attempts.rid`, not `quotes.id`, since
          `20260210100000_fix_transactions_quote_fk.sql`, and `quote_attempts` carries no
          rate/offers column. BL-48 re-examined this for `orders.create`'s Bridge-native path
          (BL-47), which prices via `api_quotes` (a genuinely joinable row, unlike `quotes`) — still
          ruled out as a read-time source, since `api_quotes` is quote-cache infrastructure with its
          own TTL, and reading it back from `orders.get`/`.list` days after settlement, for a row a
          future cleanup job may have already pruned, would make this field's presence flicker on a
          purge schedule having nothing to do with the order itself. BSR-10 uses a WRITE-time source
          instead: `createBridgeNativeOrder`'s execute-transfer call opens a server-sealed route
          claim and `bridge-core.ts` derives the persisted rate from THAT claim, inverted onto this
          wire basis (never from a request-body value), onto the `transactions` row itself at
          handoff (`baseMetadata`) — durable transaction history, never a re-derived read, and never
          a caller-supplied figure. Once settlement writes the observed `to_amount`
          (`bridge-webhook/index.ts`'s `transfer.payment_processed` handler) AND Bridge's own
          transfer receipt proves the actual settled source amount, this field is RECOMPUTED as that
          receipt-proven source amount divided by the observed `to_amount` — a provider can
          legitimately re-price between quote and arrival for some corridors, so the settled figure
          is never assumed equal to the quoted one even when they agree; `rate_basis` flips to
          `at_arrival` in the same response. A row whose receipt never proves that source amount, or
          that reaches a terminal status without ever settling, publishes neither `rate` nor
          `rate_basis` rather than a stale or unproven figure. Still absent for every other order —
          a Sell/Swap order, a non-Bridge provider, or a Bridge buy this fix's write-time source
          never reached (a row created before this deploy) — never guessed.
      rate_basis:
        type: string
        enum:
          - quoted
          - at_arrival
        description: >-
          Qualifies `rate` (above; both regimes share the same `from_asset` per `to_asset` basis).
          `quoted`: the taken offer's quote-time price, published only while the order is still
          active — the persisted estimate, never the final word; a terminal row without a
          receipt-proven settled figure publishes neither `rate` nor `rate_basis` (fixer round 3,
          P2, independent review 2026-09-20 — this paragraph had drifted to say the opposite of what
          `buildOrderRateProjection` actually gates on; `rate`'s own description above, and
          RESOURCE-MODEL.md §2.4, already stated the gate correctly). `at_arrival`: the REALIZED
          rate, Bridge's own receipt-proven settled source amount divided by the observed
          `amounts.receive` once its transfer receipt lands — never assumed equal to the quoted
          figure, since a provider can legitimately re-price between quote and arrival for some
          corridors (Bridge's own "rate fixed on arrival" settlement semantics this field exists to
          name structurally, replacing a copy caveat with a field). Published alongside `rate` for a
          Bridge-native `buy` order (BSR-10); still absent alongside it for every other order, for
          the identical reason.
      provider:
        $ref: '#/components/schemas/Provider'
      payment_method:
        type: string
        description: >-
          The payment method originally requested for this order (`transactions.payment_method`,
          e.g. `sepa`/`card`/`ach_push`), verbatim — BL-48. Not restricted to a fiat leg: a Bridge
          buy funded from a crypto balance can carry a crypto method here too (e.g.
          `bitcoin`/`ethereum`) — prod carries 184 such rows. Absent when the row predates this
          column or the value was never set. Distinct from `payment_rail`: this is the REQUEST, not
          necessarily what settled — see that field.
      payment_rail:
        type: string
        description: >-
          Bridge's own OBSERVED settlement rail for a Bridge order (`transactions.payment_rail`,
          BL-48) — starts equal to `payment_method` at creation and is overwritten once
          `bridge-webhook/index.ts`'s `buildBridgeTransferUpdatePayload` resolves one (from
          `transfer.created` onward, not only once settled), so the two can genuinely diverge.
          Published ONLY for `provider.id: bridge` orders (fix-pack, independent review): the same
          column carries Transak's own private rail vocabulary (`order.paymentOptionId`, e.g.
          `credit_debit_card`) for a Transak order, which is not Bridge's vocabulary and disagreed
          with `payment_method` on the method itself on 448 prod rows — publishing it under this
          Bridge-documented field for another provider would misrepresent what the customer paid
          with. Never inferred — published verbatim from the column, or absent when it was never set
          or the provider is not Bridge.
      amounts:
        type: object
        description: >
          The checkout's own money lines (R15 §4: "You pay" / "You receive"). The exact sub-field
          set is not yet spelled out beyond this pairing in RESOURCE-MODEL — flagged for
          confirmation rather than invented further. Fixer round 1 (2026-09-22) — either leg is
          WITHHELD, never published at an unverified decimal scale, when this gateway cannot verify
          that leg's currency/asset — see `docs/api/CHANGELOG.md`'s entry of the same date. This
          rule is identical on every `order.*` event's own `amounts` (`GET /v1/events`, the SSE
          stream, `GET /v1/activity`) — each leg withholds independently there too, and `amounts` is
          absent from the event only when NEITHER leg resolved, never when just one could not be
          verified — see `docs/api/CHANGELOG.md`'s L5-FIX-event-money-parity fixer round 1 entry
          (2026-09-22).
        properties:
          pay:
            $ref: '#/components/schemas/Money'
            description: >-
              The `transactions.from_amount` column. On a Bridge-native `sell` it is the crypto the
              order was quoted for until Bridge reports the deposit it received, then the amount
              Bridge received (2026-10-28): the order sells the first deposit that arrives, whatever
              its amount, so it can differ from the quote, and the payout follows it.
          receive:
            $ref: '#/components/schemas/Money'
            description: >-
              The `transactions.to_amount` column, published VERBATIM for ANY provider/side that
              writes it (provider-agnostic, unchanged by BSR-10 — fixer round 1, P2, independent
              review 2026-09-20 corrected a prior draft of this text that wrongly claimed this
              figure was new and Bridge-buy-only; prod already publishes it for Paybis, Transak,
              OKX, Bridge-sell and Moonpay orders). Whether this figure is itself SETTLED is
              provider-specific, not guaranteed by this field alone (fixer round 3, P2, independent
              review 2026-09-20 — Transak writes `to_amount` on every synced observation, not only
              `completed`, so a `processing` Transak order can publish an unsettled provider figure
              here with no `rate_basis` alongside it): only a Bridge-native buy's `rate_basis`
              qualifies this value at all (`quoted` names it an unsettled estimate; its absence here
              means no route-specific caveat is known, not that the figure is proven settled). What
              BSR-10 actually adds is the PENDING case: before `to_amount` is written
              (`bridge-webhook/index.ts`'s `transfer.payment_processed` handler, Bridge-native `buy`
              only), `createBridgeNativeOrder` persists the taken offer's own quote-time `final_out`
              at handoff (`bridge-core.ts`'s `baseMetadata`, derived from the sealed route claim —
              never a caller-supplied value), and this field publishes that ESTIMATE instead — see
              `rate_basis`, above, which names which of the two this value is — until the settled
              `to_amount` lands and takes over unconditionally, or the order reaches a terminal
              status without ever settling, at which point neither figure is published. No other
              order path persists a quote-time estimate here; every other order either shows the
              settled `to_amount` or nothing.
          fee:
            $ref: '#/components/schemas/Money'
            description: >-
              Declared since BL-19; BL-28/BS-10 investigated populating it off the same linked-quote
              offer as `rate`, above, and found no live source (see `rate`'s description) — left
              reserved at the time. BL-48 gives it a real, narrow producer: a Bridge-native `buy`
              order's own transfer receipt (Bridge's observed `developer_fee`, netted from the
              transfer's source amount — the buy's fiat `from_asset` leg, matching this field's own
              currency contract below) — published ONLY when `status: completed` (fix-pack,
              independent review). Bridge writes this receipt at `transfer.created`, before any
              money has moved — prod carries a fee figure on the majority of `awaiting_payment`,
              `cancelled` and `refunded` Bridge buys too, all a request-time QUOTED figure Bridge
              echoes back, not an observation of what was actually charged, so `completed` is the
              gate, not merely "the receipt is non-empty". Still ABSENT for every other case — a
              Sell/Swap order, a non-Bridge provider, or a Bridge buy that has not reached
              `completed` — never guessed. Denominated in this order's fiat leg (`from_asset` on a
              buy, `to_asset` on a sell) — never on a `swap` order, which has no fiat leg to
              denominate it in. On a Bridge `wire` buy it is the whole developer fee Bridge
              withheld: the Swaps fee plus Bridge's wire fee (passed through at cost, D-86b). This
              is a single total, not a provider/network breakdown — Bridge exposes no on-chain
              network-fee figure this codebase reads anywhere; a breakdown is real follow-up work; a
              Sell's fee is left unpopulated because Bridge nets it from the crypto leg, not
              `to_asset`, and publishing it here would misrepresent the currency.
        additionalProperties: false
      money_state:
        type: string
        enum:
          - never_authorized
          - hold_placed
          - captured
        description: >-
          OPTIONAL — absent when this row has never been classified (BL-38, #3086): the writer is
          the Paybis/Bridge failure-classification pipeline, gated on `failed`/`cancelled` Paybis
          rows only, so most rows never receive one. When present, read it together with
          `money_status`: `money_state` says whether funds were ever captured, `money_status`
          carries refund truth. A row can read `status: completed` while `money_state` reads
          `never_authorized` (D-10). Never infer a money state from `status` alone, and never treat
          its absence as `never_authorized` — that is exactly the fabricated-default bug this
          field's optionality fixes.
      money_status:
        type: string
        enum:
          - not_charged
          - unknown
          - refund_in_progress
          - refund_completed
        description: >-
          Drives the "Money:" line on the terminal screens (R15 §3). Every bank rail resolves to
          `unknown`; only a card rail is ever positively `not_charged`.
      failure_kind:
        type: string
        enum:
          - session_expired
          - declined
          - rejected
          - refunded
          - verification_failed
          - risk_declined
          - generic_failed
        description: >-
          The seven normalized failure kinds (R15 BS-G9, prod-inventory §1.4). This is the field the
          failed screen's "Status code" must read from — never `status`.
      failure_reason:
        type: string
        description: Safe, human-readable copy for `failure_kind` — never a raw provider error string.
      return_reason:
        type: string
        description: >-
          Why a settled leg was sent back, as the provider itself worded it (e.g. `Receiver Name
          Mismatch`, `Too Many Recent Transactions`) — the returned leg is a fiat pay-in on a buy,
          and either the crypto deposit or the fiat payout on a Bridge-native sell (L1-5). Present
          only on an order whose money actually arrived and was then returned — distinct from
          `failure_reason`, which is OUR safe copy for a `failure_kind`. Same field and same
          contract as `payments.return_reason` on a payment link, so one integration handles both.
          Provider vocabulary is open-ended and NOT an enum: branch on `failure_kind`/`status`, and
          treat this as display and support text.
      next_action:
        type: object
        description: >-
          What the caller must do next before this order can advance, when anything is required of
          them at all. Present ONLY while a hosted-provider checkout is open and reachable: the
          order is in an active status, a hand-off URL exists for it, that URL has not expired, and
          its host is one this API recognises. ABSENT — never null, never a stale URL — the moment
          any of those stops being true, and on a Bridge-native order (which completes in place and
          publishes `bank_deposit_instructions` or `deposit_instructions` instead) — with ONE
          exception (D-123, 2026-10-20): a Bridge-native COP PSE buy (`payment_rail:
          co_bank_transfer`) is paid on Bridge's hosted page, so it carries `next_action` (Bridge's
          `start_url` and its `expires_at`) and no bank details, while the order is
          `pending`/`awaiting_payment` and the page has not expired. That URL's host is not on a
          list this API keeps (founder ruling 2026-10-20): it comes from Bridge's own
          server-to-server response and is published only when it parses, is `https` and carries no
          user name or password. A caller that receives no `next_action` on a hosted order whose
          `status` is still active must request a new quote; there is no way to re-mint a checkout
          session for an order that already has one. L1-6b (contract only, no producer yet): every
          route stays silent on this field until L1-6c/d build the hosted-checkout branch and its
          resume. Published by `orders.create`'s `201` and `GET /v1/orders/{id}` only — `GET
          /v1/orders` never carries it, for any row.
        required:
          - type
          - checkout_url
          - expires_at
        properties:
          type:
            type: string
            enum:
              - redirect
            x-swaps-open-enum: true
            description: >-
              `redirect`: open `checkout_url` in a browser and let the provider complete the
              payment. This enum may gain members within `/v1`; treat an unknown value as opaque and
              do not fail deserialization.
          checkout_url:
            type: string
            format: uri
            description: >-
              An `https` URL on a provider host this API recognises, minted server-side for THIS
              order — for a Bridge-native COP PSE buy (D-123), Bridge's own `start_url`, `https`
              with no user name or password, on whatever host Bridge serves it from. Never
              caller-influenced, never reused across orders. Open it; do not parse it, store it past
              `expires_at`, or send it to anyone but the person paying.
          expires_at:
            type: string
            format: date-time
            description: >-
              When this URL stops being usable. Past it the order stays readable but the checkout is
              gone: the field disappears from `GET /v1/orders/{id}` and a replayed `POST /v1/orders`
              response may still carry the expired value it was stored with — check this field, do
              not assume a replay is fresh.
        additionalProperties: false
      offer_id:
        type: string
        description: >-
          The offer actually executed for this order (`transactions.offer_id`), verbatim. Normally
          the `offer_id` the caller sent, or the quote's winning offer when none was sent; it can
          differ when the selected offer was not executable at order time and the server fell
          through to the next-ranked one — see `offer_selection`. Absent on an order created before
          this field existed or by a path that records no offer.
      offer_selection:
        type: object
        description: >-
          Why THIS offer ran, when that is not simply "the one you asked for". Published on the
          `201` from `orders.create` only — it is a record of a creation-time decision, and a later
          `GET` of the same order republishes `offer_id` but not this object. Absent when the
          executed offer is the one the request selected.
        required:
          - executed_offer_id
          - reason
        properties:
          executed_offer_id:
            type: string
          skipped:
            type: array
            items:
              type: object
              required:
                - offer_id
                - provider_id
                - reason
              properties:
                offer_id:
                  type: string
                provider_id:
                  type: string
                reason:
                  type: string
                  enum:
                    - provider_paused
                    - provider_not_supported
                    - capability_unavailable
                    - offer_expired
                  x-swaps-open-enum: true
              additionalProperties: false
          reason:
            type: string
            enum:
              - next_ranked_eligible_offer
            x-swaps-open-enum: true
        additionalProperties: false
      deposit_instructions:
        $ref: '#/components/schemas/DepositInstructions'
        description: >
          Present on a self-custody sell that needs crypto sent to a provider-issued address before
          payout — the shared `DepositInstructions` component, same shape as
          `wallet.deposit_intents.deposit_instructions` (D-24, BS-G7). On a Bridge sell the deposit
          deadline is `deposit_instructions.expires_at` (24 hours after the order was created);
          `Order` itself has no `expires_at`.
      bank_deposit_instructions:
        $ref: '#/components/schemas/BankDepositInstructions'
        description: >
          Present on a Buy order awaiting a bank-rail payment — the deposit slip (IBAN/account
          number, beneficiary, reference) the payer sends fiat to (BL-19). Reuses the shared
          `BankDepositInstructions` component verbatim rather than a second same-named shape, so
          this never drifts from the identical projection `payment_sessions`/`payroll` funding
          already publish. UNLIKE `payout_destination` below, this is deliberately the full,
          unmasked provider payload (D-24): the payer must have the real IBAN to actually send funds
          to it. Bound by the same D-24 rule as every `BankDepositInstructions` consumer — never
          logged, never placed in a URL or query string.
      payout_destination:
        type: object
        description: >
          Present on a buy, sell or swap once its settlement destination is known (the buy crypto
          branch, BL-48, added below). The bank branch is masked the same way as
          `payouts.beneficiary`'s output projection — never the full account number, never a public
          link or token (D-24); a sell order never itself becomes a `payout` object. The crypto
          branch (`chain`/`address`/`tx_hash`, BL-19; buy, BL-48) is exclusive with the bank branch:
          a self-custody payout is on-chain and has no account number to mask, so `address` is
          published verbatim (BS-G7's own `deposit_instructions.address` precedent). `tx_hash` on
          the buy branch is read from the SAME Bridge transfer receipt (`bridge_data`'s
          `receipt.destination_tx_hash`) the `amounts.fee` producer reads (fix-pack, independent
          review — 66 of 70 `completed` Bridge buys carry one); absent before the transfer settles
          or on an older payload shape that predates the field, never guessed.
        properties:
          account_holder:
            type: string
          account_tail:
            type: string
          bank_country:
            type: string
          beneficiary_owner_type:
            type: string
          chain:
            type: string
            enum:
              - tempo
              - base
              - ethereum
              - polygon
              - arbitrum
              - optimism
              - solana
            description: >-
              The settlement chain for a crypto payout (Sell/Swap, BL-19; Buy, BL-48) — the same
              `chain` vocabulary as `DepositInstructions.chain`, never a second `network` spelling
              of the same fact (CMP-8's own lesson: a `source_network` duplicate of `chain` was
              removed from `payment-links`, not reintroduced here). Present only when the settlement
              chain can be resolved to this exact enum from a known chain id — an unrecognised chain
              id still publishes `address`/`tx_hash` with `chain` simply absent, never guessed.
          address:
            type: string
            description: >-
              The on-chain settlement address funds actually landed at — case-preserved verbatim
              (AGENTS.md address-case rule).
          tx_hash:
            type: string
            description: The on-chain settlement transaction hash, when the payout provider has confirmed one.
        additionalProperties: false
      progress:
        type: object
        description: >-
          Where a Bridge-native sell stands, read from Bridge's own transfer state — a finer reading
          of `status`, never a second status. `awaiting_deposit`: Bridge is waiting for the crypto
          deposit (`status: awaiting_payment`). `received`: Bridge has received the deposit
          (`processing`). `paying_out`: Bridge has sent the bank payout (`processing`). `paid`:
          Bridge reports the bank payout processed (`completed`); when the money shows in the
          account is up to the receiving bank. Present only on a Bridge-native sell whose transfer
          state is one of those four and agrees with `status`. Absent otherwise — any other provider
          or side, a transfer state outside those four (a review, a return, a failure), or a state
          that disagrees with `status`. Absence says nothing about the money: read `status`,
          `money_state` and `money_status`.
        required:
          - stage
        properties:
          stage:
            type: string
            x-swaps-open-enum: true
            enum:
              - awaiting_deposit
              - received
              - paying_out
              - paid
            description: >-
              This enum may gain members within `/v1`; treat an unknown value as opaque and do not
              fail deserialization.
        additionalProperties: false
    additionalProperties: false
unevaluatedProperties: false
```

<a id="orderlist"></a>
## OrderList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Order'
unevaluatedProperties: false
```

<a id="ordereventlist"></a>
## OrderEventList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Event'
unevaluatedProperties: false
```

<a id="capabilitycorridor"></a>
## CapabilityCorridor

```yaml
type: object
description: >
  One payout corridor's public and (when authenticated) entitlement-bearing facts (RESOURCE-MODEL
  §2.2, v2 amendments; D-48).
required:
  - id
  - currency
  - fiat_rail
  - lifecycle
  - public_claim_status
  - status
properties:
  id:
    type: string
  currency:
    type: string
  fiat_rail:
    type: string
  minimum:
    $ref: '#/components/schemas/Money'
  max_amount:
    $ref: '#/components/schemas/Money'
    description: >-
      The Travel-Rule ceiling for this corridor (§2.2 v2 amendments). On `crypto_relay`, the
      per-invoice cap: a payer is refused above it, and `payment_links.activate` refuses a link it
      would leave with no payable rail (`422 no_payable_rail`).
  source_chains:
    type: array
    items:
      type: string
  lifecycle:
    type: string
    enum:
      - public
      - preview
      - blocked
  public_claim_status:
    type: string
    enum:
      - live
      - unverified
      - planned
  sender_display:
    type: string
  legal_entity_name:
    type: string
    description: >-
      Present only where the settling entity differs from the account holder (e.g. a EUR SEPA IBAN
      held by the provider's EEA entity).
  eta_seconds:
    type: integer
  executable:
    type: boolean
    description: >-
      Availability, never authorization — the server re-checks at create and at the money boundary
      (RESOURCE-MODEL §0.10).
  blocked_reason:
    type: string
    enum:
      - lifecycle_blocked
      - minimum_unconfigured
      - rail_not_enabled
      - route_unavailable
      - payin_fiat_pending
      - collection_account_uses_currency
      - relay_requires_tempo_mainnet
      - wallet_not_found
      - verification_closed
      - endorsement_not_requestable
      - payin_fiat_refused
      - owner_only
    x-swaps-open-enum: true
    description: >-
      A machine-readable code for a corridor whose `status` alone does not say enough — read it per
      `status`, never generically. For a `payouts` corridor, `handlePayoutsEligibility`'s own
      blocker classification (`lifecycle_blocked`, `minimum_unconfigured`, `rail_not_enabled`,
      `route_unavailable`). For a `wallet_bank` `wallet_bank_virtual_account` corridor (receive by
      bank, W-R23), the reasons apply in one order — the same one `POST /wallet/virtual_accounts`
      refuses by — and each reads `status: not_enabled` and `action: none` unless said otherwise.
      First what no step of the person opens, so do not tell them to verify: `owner_only`
      (2026-10-12) — the caller is a member signed in as themselves who is not the account's owner,
      and only the owner opens receive by bank (the create refuses them before anything else: `403
      role_denied` for a `member`, at the operation's role gate, and `403 scope_denied` for an
      `admin` who is not the owner); `rail_not_enabled` — the rail is not launched for this account
      (the `wallet_bank_virtual_account_enabled` flag does not admit it); `verification_closed`
      (2026-10-11) — verifying again cannot open this account: the provider has given its final
      answer on it (it rejected the verification with a final outcome — the customer's
      `rejection_outcome: terminal` — or it paused or offboarded the customer, `kyc_status` `paused`
      or `offboarded`), or (2026-10-12) verifying is its next step and it has used every attempt
      Swaps starts (two rejections; the customer reads `verification_attempts_exhausted: true`),
      whether its verification has not started, needs an action or was rejected, so `POST
      /customers/{id}/verification_links` mints no further link and support is the next step — that
      case carries `detail_code: verification_attempts_exhausted`; a rejection whose
      `rejection_outcome` is `retryable` or `unknown`, below that limit, still reads as a
      verification step; `payin_fiat_refused` (2026-10-11) — an `individual` account whose fiat
      pay-in activation (`payin_fiat` on the provider customer, read live, or Swaps' stored copy
      when the live read fails) is `inactive` or `rejected`: the provider's answer, not a wait;
      `endorsement_not_requestable` (2026-10-11) — this currency needs its own provider endorsement
      (USD `base`, EUR `sepa`, MXN `spei`), the account's provider customer does not carry it, and
      no `/v1` operation can add it (`POST /customers/{id}/verification_links` takes no endorsement;
      an account with no provider customer yet gets `base` and `sepa` from `POST /customers`, never
      `spei`), whatever the verification state; `route_unavailable` — the account's active Tempo
      wallet is not on Tempo mainnet (Bridge settles the deposit as USDC there; on a staging project
      whose Bridge runs in its sandbox, not on that project's own test network);
      `collection_account_uses_currency` (W-R33) — a Payment links collection account already
      receives this currency into this account's Tempo wallet, no activated wallet-funding account
      does (the create would reuse that one), and the provider would hand the collection account
      back instead of issuing a wallet-funding one. Then the person's own steps, in the order they
      take them: a verification step with no `blocked_reason` (`not_started`, `action_required`,
      `in_review` or `gathering_no_path`); `payin_fiat_pending` with `status: gathering_no_path` for
      an `individual` account whose `payin_fiat` could not be read (no snapshot, or a value outside
      `pending | active | inactive | rejected`) — a `pending` one does not block this corridor since
      2026-10-11 (B4: the provider issued these accounts to individuals whose activation reads
      `pending`); and last `wallet_not_found` (B4, 2026-10-11) — the account has no active Tempo
      wallet yet, so a bank deposit would have nowhere to settle: create the wallet first (`POST
      /wallet/wallets`). `POST /wallet/virtual_accounts` refuses each with its own answer: `403
      role_denied` or `403 scope_denied` for `owner_only` (above), `409 capability_unavailable` with
      `details.reason: rail_not_enabled` for `rail_not_enabled`, `409 wallet_not_found`, `409
      network_not_supported` for `route_unavailable`, `409 conflict` for
      `collection_account_uses_currency`, and `409 capability_unavailable` with the same
      `details.blocked_reason` for every other reason. `payin_fiat_pending` on a fiat pay-in
      corridor of `payment_links` (its own `kind: 'bank'` rows; never `payroll`): an `individual`
      account whose Bridge customer carries no endorsement snapshot and whose fiat pay-in activation
      is not exactly `active` — the fail-closed fallback of C4-D23
      (`docs/capabilities/CAPABILITIES-CANON.md` §1.8) that `status` alone cannot distinguish from
      any other `gathering_no_path` cause. Since 2026-10-14 a customer with status-bearing
      endorsements is decided by the corridor's own endorsement instead, exactly as the payment-time
      gate decides it: an approved endorsement reads `available` whatever the deprecated
      `payin_fiat` scalar says. This is the SAME underlying condition `rail_not_allowed`'s
      `details.reason` publishes as `merchant_fiat_payin_pending` (`root.yaml`) at the payer's
      execution-time error boundary — same fact, two scopes: this field describes the account's OWN
      capability resource, that one describes a payer's refused attempt against a merchant's link.
      `relay_requires_tempo_mainnet` (RELAY-ENV-1-T, P-72), only on the `payment_links`
      `crypto_relay` corridor, with `status: not_enabled` and `source_chains: []`: Relay routes are
      admitted (`founder_accepted` or `proven`) only into Tempo mainnet, and this deployment settles
      Payment links on a Tempo test network (dev and staging), or its Tempo network could not be
      resolved, so the payer page never offers Relay here whatever the flags say. It outranks
      `rail_not_enabled`; `max_amount` is still published while the cap is set. Production (Tempo
      mainnet) reads it only if its Tempo network cannot be resolved. Absent for every corridor this
      does not apply to. Open enum: a client tolerates a value it does not know.
  detail_code:
    type: string
    enum:
      - verification_attempts_exhausted
    x-swaps-open-enum: true
    description: >-
      Which of its reason's sentences `detail` carries, where a client must tell them apart — absent
      when the reason has one sentence. Today only on a `wallet_bank` `wallet_bank_virtual_account`
      corridor with `blocked_reason: verification_closed`: `verification_attempts_exhausted`
      (2026-10-12) — the account has used every verification attempt Swaps starts, so support is the
      next step (offer it), not the provider's final answer. `POST /wallet/virtual_accounts` refuses
      the same corridor with the same `details.detail_code`. Open enum: a client tolerates a value
      it does not know.
  advisory:
    type: string
    enum:
      - payin_fiat_pending
    x-swaps-open-enum: true
    description: >-
      A fact to tell the person about the corridor's bank accounts — never a block (`status` and
      `blocked_reason` say whether the corridor is open), and absent when there is nothing to say.
      Today only on the `wallet_bank` `wallet_bank_virtual_account` corridor: `payin_fiat_pending`
      (2026-10-12) — an `individual` account whose fiat pay-in activation (`payin_fiat` on the
      provider customer, read live, or Swaps' stored copy when the live read fails) is `pending`.
      Since 2026-10-11 that no longer blocks the wallet's own bank account, but the provider may
      review a deposit into any of the account's bank accounts and return it. It is a fact about the
      account holder, so every corridor of a launched account carries it whatever its `status` — a
      closed one too, as accounts already issued (those of a previous wallet included) keep
      receiving — except `owner_only`, whose caller's own accounts are not the owner's. Say so
      before the person shares the bank details of any account in that corridor's currency,
      including one created before this field existed. Open enum: a client tolerates a value it does
      not know.
  status:
    type: string
    enum:
      - available
      - action_required
      - in_review
      - gathering_no_path
      - not_started
      - not_enabled
    description: >-
      The corridor's eligibility state for this account. `in_review` and `gathering_no_path` are
      never the same answer (V-G1, D-48).
  review_category:
    type: string
    enum:
      - genuinely_reviewing
      - actionable
      - gathering_no_path
    description: Whether the rail row gets an action button at all (`RailReviewCategory`, R17 §3).
  display_label:
    type: string
    description: >-
      The one machine-readable label every client renders, so `in_review` and `gathering_no_path`
      never collapse to the same text again (D-48).
  detail:
    type:
      - string
      - 'null'
    description: >-
      The human sentence for `status`'s current value (K6b, §52 C4-D14) — one fixed sentence per
      `CorridorEligibilityStatus`, never a per-reason variant, so it never claims more than `status`
      itself already promises. One exception: a `wallet_bank_virtual_account` corridor with
      `blocked_reason: rail_not_enabled` reads `Receive by bank is not launched yet.` (the rail is
      off for everyone it does not admit, not for this account in particular); with `blocked_reason:
      collection_account_uses_currency` it reads `A Payment links account already receives
      <CURRENCY> for this wallet.`; with `blocked_reason: wallet_not_found` it reads `Create your
      wallet first — bank deposits settle into it.`; with `blocked_reason: verification_closed` it
      reads `The provider has closed or paused verification for this account; verifying again will
      not open receive by bank.`, or, for an account at the verification attempt limit
      (`detail_code: verification_attempts_exhausted`), `This account has used every verification
      attempt; contact support to continue. Verifying again here will not open receive by bank.`;
      with `blocked_reason: endorsement_not_requestable` it reads `This account has no provider
      approval for <CURRENCY> deposits, and that approval cannot be requested through the API
      today.`; with `blocked_reason: payin_fiat_refused` it reads `The provider does not accept fiat
      pay-ins for this account: it reports them as <inactive|rejected>.`; with `blocked_reason:
      owner_only` it reads `Only the account owner can open receive by bank for this account.`
  action:
    type:
      - object
      - 'null'
    description: >-
      What the caller can do about this corridor right now (K6b, §52 C4-D14), derived from `status`
      and, when a richer signal exists, overridden by it: for a
      `payment_links`/`payroll`/`wallet_bank` corridor the resolver's own `next_action`, for a
      `payouts` corridor `handlePayoutsEligibility`'s own `blocker` (`kyc`/`endorsement` ->
      `verify`; `address`/`tos`/`source_of_funds`/`additional_details` -> `provide_details`;
      `capability` -> `none`). Never invented: a corridor with no such signal named falls back to
      the coarse `status` default alone (Codex review, PR #2989).
    required:
      - kind
      - href
    properties:
      kind:
        type: string
        enum:
          - provide_details
          - verify
          - none
      href:
        type:
          - string
          - 'null'
        format: uri
        description: >-
          Always `null` today — no verification-link minting is wired into this route (`POST
          /v1/customers/{id}/verification_links` is the separate call that mints one).
    additionalProperties: false
  requested_at:
    type:
      - string
      - 'null'
    format: date-time
    description: >-
      Reserved for when this corridor's endorsement was requested (K6b, §52 C4-D14) — always absent
      today. Bridge stores no per-endorsement request timestamp anywhere in this codebase
      (`bridge_customers.bridge_data.endorsements[]` carries `name`/`status`/`requirements` only),
      and `bridge_customers.updated_at` bumps on ANY write to the customer (KYC, ToS, an unrelated
      endorsement), so it is not a safe per-endorsement proxy — publishing it would misreport an old
      request as freshly made whenever something else on the account changed (Codex review, PR
      #2989). Left in the schema, nullable, so a future PR can populate it once a real
      per-endorsement timestamp is persisted, rather than never having a field to fill.
  crypto_only:
    $ref: '#/components/schemas/CapabilityCryptoOnly'
additionalProperties: false
```

<a id="capabilitycryptoonly"></a>
## CapabilityCryptoOnly

```yaml
type: object
x-swaps-since: '2026-09-25'
description: >-
  CP-T2 (CP-G16, D-63) — present only on the `crypto_tempo` corridor of `product=payment_links`.
  Whether THIS account can activate a crypto-only link (settlement to its own Swaps Wallet, K13 §4)
  right now, judged by the same account-level gates `payment_links.activate` enforces, in the same
  order: the `payment_links_crypto_only` flag for this merchant, the account's own status, the
  `crypto_tempo` rail and its fee wallet, and the account owner's active Tempo wallet on a Tempo
  network. No Bridge customer, KYC or endorsement is read or required. The corridor's own `status`
  keeps its meaning (the rail is on for this deployment, which a saved Tempo address also uses);
  `crypto_only` refines it and is never `available` while that `status` is not. Link-level checks
  (USD currency, attestation, rail restriction) still run at activation. Availability only, never
  authorization.
required:
  - status
  - display_label
  - requires_bridge
properties:
  status:
    type: string
    enum:
      - available
      - action_required
      - not_enabled
    description: >-
      The corridor vocabulary: `available`, `action_required` (only `wallet_not_found` — the account
      can create its Swaps Wallet), `not_enabled` (every other reason; nothing the caller can do in
      this API).
  blocked_reason:
    type: string
    enum:
      - crypto_only_not_enabled
      - account_inactive
      - rail_not_enabled
      - wallet_not_found
      - settlement_destination_unsupported
    x-swaps-open-enum: true
    description: >-
      The first closed gate, absent when `status` is `available`. `crypto_only_not_enabled`: the
      `payment_links_crypto_only` flag does not admit this merchant. `account_inactive`: the account
      is not `active`. `rail_not_enabled`: the `crypto_tempo` rail is off or has no fee wallet.
      `wallet_not_found`: the account owner has no active Tempo wallet (activation answers `409
      wallet_not_found`). `settlement_destination_unsupported`: that wallet does not resolve to a
      Tempo network this account can settle on (activation answers `422 invalid_request`, or `422
      settlement_rail_unsupported` for a tempo-mainnet wallet on a non-livemode account).
  display_label:
    type: string
    description: The D-48 label for `status`, the same map every corridor uses.
  requires_bridge:
    type: boolean
    enum:
      - false
    description: Always `false` — the crypto-only path never needs a Bridge customer (K13 §4 step 7).
additionalProperties: false
```

<a id="capabilitypayrollcryptofunding"></a>
## CapabilityPayrollCryptoFunding

```yaml
type: object
x-swaps-since: '2026-09-25'
description: >-
  PAYROLL-CRYPTO-1 — whether a pay run in this currency can be funded by sending USDC to the
  employer's Bridge payroll wallet (`POST /v1/payroll_runs` with `funding_method: crypto`), the
  second funding method production offers next to the bank transfer. `available` reads the
  deployment's crypto-funding switch first, then the same per-currency facts as
  `funding_account_available` (a Bridge funding rail for the currency and this account's
  verification for it): the shared funding service checks the currency's funding rail before it
  prepares a crypto deposit. `asset` and `chain` are present only when `available` is true; they are
  the wallet the deposit address is on — send only that asset on that chain. The exact amount, fee
  included, is published on the run's `funding_instructions.crypto` once funding is requested; send
  it in ONE transfer: deposits on this route are not summed, and a top-up after a short deposit does
  not fund the run. Availability only, never authorization: `create`, `approve` and the funding call
  re-check the same switch. An entry without this object reads as not available.
required:
  - available
properties:
  available:
    type: boolean
  reason:
    type: string
    enum:
      - crypto_funding_not_enabled
      - bridge_funding_currency_unsupported
      - verification_required
    x-swaps-open-enum: true
    description: >-
      Present only when `available` is false; the first closed gate. `crypto_funding_not_enabled`:
      crypto funding is switched off for payroll (a run asking for it answers `409
      capability_unavailable`). `bridge_funding_currency_unsupported` and `verification_required`
      mean what they mean on the parent `funding_currencies[]` entry.
  asset:
    type: string
    description: The asset to send, e.g. `USDC`.
    example: USDC
  chain:
    type: string
    description: The chain the payroll wallet's deposit address is on, e.g. `solana`.
    example: solana
additionalProperties: false
```

<a id="capabilitytempowalletsettlement"></a>
## CapabilityTempoWalletSettlement

```yaml
type: object
x-swaps-since: '2026-09-27'
description: >-
  §52.33 (PL-TEMPO-BRIDGE-1-T, founder decisions 27.09; mechanism fixed by PL-TEMPO-BRIDGE-2-T; the
  flag mode fixed by PL-TEMPO-BRIDGE-3-T) — whether a payment link in THIS currency can settle to
  the merchant's Tempo wallet, and which rail carries the money. `via: splitter` is the existing
  non-custodial `crypto_tempo` rail (USD only, 1:1, no FX — BL-37 stays for USD links). `via:
  bridge` is the Bridge-settled real-FX path (`payment_links_tempo_via_bridge`, platform-wide or a
  per-merchant allowlist): Bridge holds a collection account whose destination carries
  `payment_rail: 'tempo'` + the address (never a crypto external account — Bridge external accounts
  are fiat-only) and converts this currency to USDC on Tempo, fee visible. ORDER IS LAW (founder
  ruling): the Bridge sandbox proof, then the founder's own production €5 test — run as a
  per-merchant allowlist entry, the SAME mechanism `crypto_relay_rail` uses — then the wizard option
  becomes selectable for everyone (platform-wide) — this field can read `available: true, via:
  bridge` before the wizard exposes it, because availability here is a server fact, not a UI gate.
  Availability only, never authorization — `payment_links.activate` re-checks the same flag mode and
  currency, for the SAME merchant, at write time.
required:
  - available
  - via
properties:
  available:
    type: boolean
  via:
    type: string
    enum:
      - splitter
      - bridge
  reason:
    type: string
    enum:
      - tempo_via_bridge_not_enabled
      - verification_required
      - tempo_rail_not_enabled
      - wallet_not_provisioned
      - wallet_network_not_supported
    x-swaps-open-enum: true
    description: >-
      Present only when `available` is false. `tempo_via_bridge_not_enabled`: the
      `payment_links_tempo_via_bridge` flag does not admit THIS merchant — off entirely (the seeded
      default), or a per-merchant allowlist that does not list this account's `public.users.id`.
      `verification_required`: the flag admits this merchant (platform-wide, or this merchant is
      allowlisted) but this account's own bank corridor for the currency (`corridors[]` above, same
      currency) has not itself resolved to `available` — Bridge requires that same endorsement to
      execute the FX leg. `tempo_rail_not_enabled` (`via: splitter`, USD only): the non-custodial
      `crypto_tempo` rail's own kill switch is off (fixer round, findings #5/#11) — never fabricate
      `available: true` for a rail nobody can actually use.
      `wallet_not_provisioned`/`wallet_network_not_supported` (`via: bridge` only): the flag admits
      this merchant, but the account either has no active Tempo wallet on file, or its wallet is not
      on THIS project's Tempo network (`tempo-mainnet` in production) — the SAME two gates
      `payment_links.activate`'s wallet-via-bridge branch checks, read from the SAME
      `wallet_entities` row, so this field and that write path can never disagree about which
      merchants can actually activate.
additionalProperties: false
```

<a id="capabilities"></a>
## Capabilities

```yaml
type: object
description: >
  What this account can execute right now, rail by rail — or, unauthenticated, the public claims
  projection with no entitlement, route id, flag or preview row (RESOURCE-MODEL §2.4). One resource,
  five `product` projections (D-23, D-34).
required:
  - version
properties:
  version:
    type: string
  verification_checked_at:
    type: string
    format: date-time
    description: >-
      When the account's Bridge verification facts behind `corridors` were last checked: the moment
      of a live read, or, with `verification=snapshot`, when Swaps last received the customer's
      whole state from Bridge (never more than 24 hours earlier). Show it next to the corridors
      ("Checked …"). Absent for `product=buy_sell`, when the account has no Bridge customer, and for
      a degraded read of a check whose time is unknown.
  fee:
    type: object
    description: >
      Shape varies by `product`: payouts publishes `{kind, bps, applies_to}`; payment_links
      publishes `{kind, bps, applies_to, rails, rounding, rounding_decimals}`; payroll publishes
      `{bps, shape, enabled}` (employer-funded-on-top, never a percentage deducted from the
      recipient — §2.5). All keys are optional here to cover every shape.

      **payment_links** — the Swaps fee on the splitter rails named in `rails` (`crypto_tempo`,
      `crypto_relay`), the same constant the server charges: `applies_to: payer` — added on top of
      the invoice and paid by the payer; the merchant receives the full invoice. For an invoice of
      `amount`, the payer's deposit instructions charge `fee = floor(amount × 10^rounding_decimals ×
      bps / 10000) / 10^rounding_decimals` (a USD invoice with at most two decimals is never
      rounded); `Payment.fee` and `Payment.amount_expected` on these rails publish the same figures
      at that scale. It applies to crypto subscriptions too: every subscription invoice is a
      crypto-only payment link paid on these rails. A rail not listed in `rails` carries no fee
      claim from this object (no rate, no bearer); the fee on a Bridge-routed rail (bank rails,
      `crypto_bridge`) is deducted from what the merchant receives and is published separately as
      `deducted_fee`. Provider fees (Relay relay/gas, Bridge network/processing) are not this fee
      and are quoted per payment. Present whatever the rails' current availability (`corridors[]`).
    properties:
      kind:
        type: string
        description: e.g. `percentage`.
      bps:
        type: integer
      applies_to:
        type: string
      rails:
        type: array
        items:
          type: string
        description: >-
          `product=payment_links` only — the rails (`corridors[].fiat_rail`) this fee is true for. A
          rail not listed carries no fee claim from this object.
      rounding:
        type: string
        enum:
          - floor
        x-swaps-open-enum: true
        description: >-
          `product=payment_links` only — how the charged fee is rounded: `floor` (down, never an
          overcharge) at `rounding_decimals`.
      rounding_decimals:
        type: integer
        description: >-
          `product=payment_links` only — the scale the fee is computed and floored at: the
          settlement token's decimals (6 on every Tempo stablecoin).
      shape:
        type: string
        enum:
          - employer_funded_on_top
        description: Payroll only (§2.5) — the fee is added on top of the run, never deducted from a recipient.
      enabled:
        type: boolean
    additionalProperties: false
  deducted_fee:
    type: object
    description: >
      `product=payment_links` only — the Swaps fee on the Bridge-routed rails named in `rails` (bank
      rails and `crypto_bridge`): `applies_to: merchant` — the payer pays exactly the invoice and
      the provider deducts this percentage from what arrives, so the merchant receives what arrived
      minus it (founder ruling «Bank: 1% is deducted from what you receive»). This is the rate
      configured now; the figure a payment was charged, to the cent and rounded up, is that
      payment's own `Payment.deducted_fee`, read from the provider receipt (`null` there when the
      payer paid in another currency than the invoice — `crypto_bridge`'s USDC source included — or
      no consistent receipt was recorded). Omitted when no fee is configured.
    required:
      - kind
      - bps
      - applies_to
      - rails
    properties:
      kind:
        type: string
        description: e.g. `percentage`.
      bps:
        type: integer
        description: The configured rate in basis points (100 = 1%).
      applies_to:
        type: string
        enum:
          - merchant
        x-swaps-open-enum: true
      rails:
        type: array
        items:
          type: string
        description: The rails (`corridors[].fiat_rail` / `Payment.payment_rail`) the deduction applies to.
    additionalProperties: false
  provider_fees:
    type: array
    description: >
      `product=buy_sell` only (D-86b) — a charge a provider makes on a rail that Swaps passes
      through at cost, named after the provider: never a Swaps fee and never Swaps revenue. Today
      one entry: Bridge's wire fee on Bridge `wire` BUYS (a wire Bridge receives) — withheld from
      the amount together with the Swaps fee (both inside the transfer's developer fee) and
      published on every such offer as its own «Bridge wire fee» part of `total_fees.provider`;
      `final_out` is already net of it. A sell's outgoing wire carries none (its Bridge charge is
      unknown, so nothing is deducted); ACH and Bridge's other rails carry none.
    items:
      type: object
      required:
        - provider
        - side
        - rail
        - kind
        - amount
        - label
        - pass_through
      properties:
        provider:
          type: string
          description: The provider that charges it, e.g. `bridge`.
        side:
          type: string
          enum:
            - buy
            - sell
          x-swaps-open-enum: true
          description: The side it applies to — `buy` for Bridge's wire fee.
        rail:
          type: string
          description: The payment rail it applies to, e.g. `wire`.
        kind:
          type: string
          enum:
            - flat
          x-swaps-open-enum: true
        amount:
          $ref: '#/components/schemas/Money'
        label:
          type: string
          description: The line name the quote uses, e.g. `Bridge wire fee`.
        pass_through:
          type: string
          enum:
            - at_cost
          x-swaps-open-enum: true
      additionalProperties: false
  corridors:
    type: array
    items:
      $ref: '#/components/schemas/CapabilityCorridor'
  funding_sources:
    type: array
    description: >
      `product=payouts` only — the «Pay with» sources for this caller: `external_wallet` (USDC from
      any wallet, always available) and `swaps_wallet` («Wallet balance», `coming_soon` while its
      kill switch is off; `unavailable` to a business key and in test mode).
    items:
      type: object
      required:
        - id
        - status
      properties:
        id:
          type: string
          enum:
            - external_wallet
            - swaps_wallet
        status:
          type: string
          enum:
            - available
            - coming_soon
            - unavailable
        reason:
          type: string
          enum:
            - flag_off
            - wallet_not_provisioned
            - wallet_paused
            - route_unavailable
            - unverifiable
            - test_mode
      additionalProperties: false
  providers:
    type: array
    items:
      $ref: '#/components/schemas/CapabilityProvider'
  limits:
    type: object
    properties:
      per_payout:
        type: object
        properties:
          individual:
            $ref: '#/components/schemas/Money'
          business:
            $ref: '#/components/schemas/Money'
        additionalProperties: false
    additionalProperties: false
  defaults:
    type: object
    description: >-
      `product=buy_sell` only — the resolved first-load defaults (`exchange_bootstrap`, D-23,
      BS-G4).
    properties:
      country:
        type: string
      default_fiat:
        type: string
      default_asset:
        type: string
      primary_method:
        type:
          - string
          - 'null'
        description: >-
          The preferred payment method from the same fresh route snapshot, or null when no usable
          route exists.
      selectable_methods:
        type: array
        items:
          type: string
      families:
        type: array
        items:
          type: string
      fallback_fiats:
        type: array
        items:
          type: string
      fiat_reason:
        type: string
        x-swaps-open-enum: true
        description: >-
          Why `default_fiat` is the currency shown (D-65): `local_currency` — the country's own
          currency; `fallback_fiat` — the country's own currency has no buy route in this snapshot,
          so this is the first fallback that has one, and a surface names it as not the local
          currency; `requested` — the caller asked for it. Absent from a deployment that predates
          it. A1-2: open — an unrecognized member is an opaque string, never a deserialization
          failure.
        enum:
          - local_currency
          - fallback_fiat
          - requested
      resolved_limits:
        $ref: '#/components/schemas/QuoteResolvedLimits'
    additionalProperties: false
  coverage:
    type: array
    description: '`product=buy_sell` only — the country × method × provider availability grid (D-23, BS-G5).'
    items:
      type: object
      required:
        - country
        - fiat
        - direction
        - method
        - provider
        - state
      properties:
        country:
          type: string
        fiat:
          type: string
          description: Fiat currency considered for this coverage cell.
        direction:
          type: string
          enum:
            - buy
        method:
          type: string
        provider:
          type: string
        state:
          type: string
          enum:
            - selectable_background
            - selectable_on_selection
            - blocked
            - drift
          description: >-
            A representation contract, not provider health (`buyCoverageContract`, BUY-SELL-CANON
            §4).
        reason:
          type: string
          x-swaps-open-enum: true
          description: >-
            A1-2: open — an unrecognized member is an opaque string, never a deserialization
            failure. `state_required` (D-82): the provider prices this route only once it knows the
            person's state, which a per-country grid does not carry — Stripe Onramp in the US; `GET
            /v1/quote_estimates` with `region` prices it.
          enum:
            - policy_blocked
            - provider_disabled
            - country_unsupported
            - coverage_unconfirmed
            - method_unsupported
            - currency_unsupported
            - direction_unsupported
            - route_disabled
            - provider_unavailable
            - state_required
            - ELIGIBLE_NOT_SELECTABLE
            - SELECTABLE_WITHOUT_PROVIDER
            - QUOTE_ON_SELECTION_NOT_SELECTABLE
            - SANCTIONED_ROUTE_SELECTABLE
      additionalProperties: false
  side:
    type: string
    x-swaps-open-enum: true
    enum:
      - sell
    description: >-
      `GET /v1/capabilities/buy_sell?side=sell` only — present, `sell`, on every sell answer (a
      degraded one included); absent on a buy answer. A sell answer carries `sell_defaults` and
      `sell_coverage`, never `defaults` or `coverage`. A1-2: open — an unrecognized member is an
      opaque string.
  sell_defaults:
    $ref: '#/components/schemas/CapabilitySellDefaults'
  sell_coverage:
    type: array
    description: >-
      `side=sell` only — one cell per payout method and fiat the country can be paid in by Bridge's
      residency rule, Bridge only, with the assets that may be sent. A person is paid in their own
      currency where a rail pays it; a resident no rail pays in their own currency is paid in USD
      (ACH, wire), EUR (SEPA) or GBP (Faster Payments) instead, to an account in their own name on
      that rail's network — a US account, an IBAN, a UK account (D-100). Omitted on a degraded
      answer — never an empty grid for a check that could not be made.
    items:
      $ref: '#/components/schemas/CapabilitySellCoverageCell'
  currencies:
    type: array
    description: '`product=wallet_bank` only — the currency → endorsement → status map (§2.3 v2 amendments).'
    items:
      type: object
      required:
        - currency
        - endorsement
        - status
      properties:
        currency:
          type: string
        endorsement:
          type: string
          description: The Bridge endorsement backing this currency (e.g. `base`, `sepa`, `spei`).
        status:
          type: string
        minimum:
          $ref: '#/components/schemas/Money'
      additionalProperties: false
  funding_currencies:
    type: array
    description: >-
      `product=payroll` only (BL-60, C4-D37) — for each payroll fiat currency (the SAME set
      `supabase/functions/payroll/lib.ts`'s `FIAT_CURRENCIES` accepts as a run currency), whether
      Bridge can issue a FUNDING account for it today, with a reason when it cannot. Additive: the
      run-currency picker keeps every fiat currency (the founder rejected removing COP over dropping
      it just because it has no Bridge funding rail — C4-D37; since 13.10 COP has no payout corridor
      either: bank payouts exist for USD runs only and the USDC-wallet allowlist does not admit it,
      so `corridors` above carries no COP row) and reads this array to warn honestly up front
      instead of the gap surfacing only after a run is created. `funding_account_available` is the
      bank-transfer method; `crypto_funding` (PAYROLL-CRYPTO-1) is the USDC method for the same
      currency, and its absence means the USDC method is not available.
    items:
      type: object
      required:
        - currency
        - funding_account_available
      properties:
        currency:
          type: string
        funding_account_available:
          type: boolean
        reason:
          type: string
          enum:
            - bridge_funding_currency_unsupported
            - verification_required
          description: >-
            Present only when `funding_account_available` is `false`.
            `bridge_funding_currency_unsupported` is the SAME blocker `payroll/lib.ts`'s
            `provisionPayrollFundingInstructions` already returns when it cannot resolve a Bridge
            funding rail for a recognized payroll fiat currency (COP today). `verification_required`
            (fix round 1, findings #1/#3) means a Bridge funding rail exists for this currency but
            THIS account's `bank_bridge` corridor (`corridors[]` above — one corridor, USD, the only
            bank payout corridor since 13.10; its KYC + `base` status is the gate every payroll
            funding currency shares) has not resolved to `available` yet — an account-level fact,
            never a static, caller-independent one. Per-currency funding endorsements are not
            published here.
        crypto_funding:
          $ref: '#/components/schemas/CapabilityPayrollCryptoFunding'
      additionalProperties: false
  settlement_currencies:
    type: array
    description: >-
      `product=payment_links` only (§52.33, PL-TEMPO-BRIDGE-1-T) — for each link fiat currency (the
      same currencies the bank corridors above cover), whether a link in that currency can settle
      its Tempo-wallet destination and through which rail. An entry without `tempo_wallet.available:
      true` reads as not available for that currency today.
    items:
      type: object
      required:
        - currency
        - tempo_wallet
      properties:
        currency:
          type: string
        tempo_wallet:
          $ref: '#/components/schemas/CapabilityTempoWalletSettlement'
      additionalProperties: false
  ceiling:
    $ref: '#/components/schemas/Money'
    description: '`product=wallet_bank` only — the V1 ceiling ($3,000, §2.3 v2 amendments).'
  degraded:
    type: boolean
    description: >
      `product=buy_sell` only — true when this read is not backed by a fresh route snapshot: the
      upstream hop (best-quote) was unreachable or itself had no fresh route-truth lease (NB-5).
      `providers[]` is still a live, independent read; `defaults`/`coverage` (on a sell,
      `sell_defaults`/`sell_coverage`) are OMITTED — never an empty grid or a fabricated "nothing is
      available" claim, and never a permanent 503 for a condition that cannot change.
  degraded_reason:
    type: string
    enum:
      - upstream_unreachable
      - snapshot_unavailable
      - unavailable_in_test_mode
    description: >
      Present only when `degraded` is true. Reserved: `unavailable_in_test_mode` is never emitted
      since A4-FIX-6; a test-mode caller gets 503.
additionalProperties: false
```

<a id="capabilityselldefaults"></a>
## CapabilitySellDefaults

```yaml
type: object
description: >
  `GET /v1/capabilities/buy_sell?side=sell` — where a sell starts for this country (A1 §2.1): the
  payout currency by the D-65 rule over sell routes, USDC on Base, the first selectable payout
  method and its limits. Omitted on a degraded answer.
required:
  - country
  - default_fiat
  - fiat_reason
  - default_asset
  - default_network
  - primary_method
  - selectable_methods
  - fallback_fiats
properties:
  country:
    type: string
    description: ISO 3166-1 alpha-2, the country asked for.
  default_fiat:
    type: string
    description: The payout currency the sell starts on.
  fiat_reason:
    type: string
    x-swaps-open-enum: true
    enum:
      - local_currency
      - fallback_fiat
      - requested
    description: >-
      Why `default_fiat` is the one shown (D-65): `local_currency` — the country's own currency;
      `fallback_fiat` — the country's own currency has no sell route, so this is the first fallback
      that has one (its configured fallback, then USD, EUR, GBP); name it as not the local currency.
      A1-2: open.
  default_asset:
    type: string
    description: The asset a sell starts on — `USDC`.
  default_network:
    type: string
    description: The network a sell starts on — `base`.
  primary_method:
    type:
      - string
      - 'null'
    description: >-
      The first selectable payout method for `default_fiat`, or null when the country has no
      sellable route in this snapshot.
  selectable_methods:
    type: array
    description: Every selectable payout method for `default_fiat`, `primary_method` first.
    items:
      type: string
  fallback_fiats:
    type: array
    description: The fallback currencies tried, in order, before `default_fiat` because they had no sell route.
    items:
      type: string
  resolved_limits:
    $ref: '#/components/schemas/QuoteResolvedLimits'
    description: >-
      `default_asset` on `default_network` sent to `primary_method`, in that asset (`currency:
      USDC`, `source: payment_method`): Bridge's own minimum for the sell and the single-order
      maximum. Absent when `primary_method` is null.
additionalProperties: false
```

<a id="capabilitysellcoveragecell"></a>
## CapabilitySellCoverageCell

```yaml
type: object
description: >
  One sell route of a country: a payout method and fiat, Bridge as provider, its state from route
  truth, and the assets that may be sent on it. A representation, not anyone's verification: the
  quote and the order re-check.
required:
  - country
  - fiat
  - direction
  - method
  - provider
  - state
  - assets
properties:
  country:
    type: string
  fiat:
    type: string
    description: The fiat paid out.
  direction:
    type: string
    enum:
      - sell
  method:
    type: string
    description: The payout method (`ach`, `wire`, `sepa`, `faster_payments`, `pix`, `spei`, `bre_b`).
  provider:
    type: string
  state:
    type: string
    x-swaps-open-enum: true
    enum:
      - selectable_background
      - selectable_on_selection
      - blocked
      - drift
    description: >-
      The route's state, by the same contract as a buy cell (`coverage[].state`). A1-2: open — treat
      an unrecognized member like `blocked`.
  reason:
    type: string
    x-swaps-open-enum: true
    description: >-
      Present when `state` is `blocked` (or a drift code on `drift`): the same vocabulary as
      `coverage[].reason`. `route_disabled` on Bre-B (COP): Bridge prices it, a sell order cannot
      execute it. A1-2: open.
    enum:
      - policy_blocked
      - provider_disabled
      - country_unsupported
      - coverage_unconfirmed
      - method_unsupported
      - currency_unsupported
      - direction_unsupported
      - route_disabled
      - provider_unavailable
      - state_required
      - ELIGIBLE_NOT_SELECTABLE
      - SELECTABLE_WITHOUT_PROVIDER
      - QUOTE_ON_SELECTION_NOT_SELECTABLE
      - SANCTIONED_ROUTE_SELECTABLE
  eta_seconds:
    type: integer
    minimum: 0
    description: Bridge's own delivery estimate for this payout method, in seconds; absent on a blocked cell.
  assets:
    type: array
    description: >-
      The assets that may be sent on this route, one per asset and network; empty on a blocked cell.
      A currency has a way to get paid when a selectable cell has at least one selectable asset.
    items:
      $ref: '#/components/schemas/CapabilitySellCoverageAsset'
additionalProperties: false
```

<a id="capabilitysellcoverageasset"></a>
## CapabilitySellCoverageAsset

```yaml
type: object
description: One asset on one network a person may send for a sell cell, with Bridge's own limits.
required:
  - asset
  - network
  - state
properties:
  asset:
    type: string
    description: The asset symbol (`USDC`, `USDT`, `USDB`).
  network:
    type: string
    description: The network it is sent on — the `from_network` of a sell estimate or quote.
  state:
    type: string
    x-swaps-open-enum: true
    enum:
      - selectable
      - blocked
    description: >-
      `selectable` — a sell of it may be quoted; `blocked` — not for this country, with `reason`.
      A1-2: open — treat an unrecognized member like `blocked`.
  reason:
    type: string
    x-swaps-open-enum: true
    description: >-
      Present only when `state` is `blocked`: `policy_blocked` — USDT and USDB are not sold for a
      country in the EEA (MiCA). `coverage_unconfirmed` is reserved and not produced today: a payout
      minimum that needs a live rate is held by the sell quote instead (see `min`). A1-2: open.
    enum:
      - policy_blocked
      - coverage_unconfirmed
  min:
    type: string
    pattern: ^[0-9]+(\.[0-9]+)?$
    description: >-
      Present only when selectable: the smallest amount of `asset` a sell of this route accepts —
      Bridge's route minimum grossed up by the 1 % fee, the source minimum the sell quote enforces.
      Never a fiat envelope. Where the payout rail also has a minimum in the payout currency (`pix`:
      20 BRL, `spei`: 50 MXN), that floor depends on the live rate and is not part of this figure:
      below it the sell quote and estimate answer `409 capability_unavailable` `LIMIT_BELOW_MIN`
      with `error.details.minimum` — that floor, or the amount of `asset` that clears both.
  max:
    type: string
    pattern: ^[0-9]+(\.[0-9]+)?$
    description: Present only when selectable — the largest single sell, in `asset`.
additionalProperties: false
```

<a id="buysellcountrysummarylist"></a>
## BuySellCountrySummaryList

```yaml
type: object
description: >
  `GET /v1/capabilities/buy_sell/countries` — a plain `{data}` catalogue, not a cursor list: every
  country of the supported catalog, always returned whole (D-93).
required:
  - data
  - degraded
properties:
  data:
    type: array
    description: One row per catalog country, sorted by `country`.
    items:
      $ref: '#/components/schemas/BuySellCountrySummary'
  as_of:
    type: string
    format: date-time
    description: When the route truth behind the oldest snapshot used was read. Absent when no row is `ok`.
  degraded:
    type: boolean
    description: >-
      `true` when any row is `unknown` (its snapshot is missing, expired or not usable). The `ok`
      rows are still true; report the others as not checked, never as not available.
additionalProperties: false
```

<a id="buysellcountrysummary"></a>
## BuySellCountrySummary

```yaml
type: object
description: >
  One country's buy coverage in brief (or, with `side=sell`, its sell coverage), from its stored
  route snapshot — the same snapshot `GET /v1/capabilities/buy_sell?country=XX` (with the same
  `side`) serves. `currencies`, `ways`, `default_fiat` and `fiat_reason` appear only when `status`
  is `ok`.
required:
  - country
  - status
properties:
  country:
    type: string
    description: ISO 3166-1 alpha-2, from the supported catalog.
  status:
    type: string
    enum:
      - ok
      - unknown
    x-swaps-open-enum: true
    description: >-
      `ok` — built from a fresh snapshot; `unknown` — no usable snapshot, so nothing is counted or
      claimed. A1-2: open — treat an unrecognized member like `unknown`.
  currencies:
    type: array
    description: >-
      The paying currencies with at least one way to pay, in the order a surface should list them:
      `default_fiat`, then the country's fallback currencies, then the rest. Empty: no way to pay
      today.
    items:
      type: string
  ways:
    type: object
    description: >-
      Per currency in `currencies`, the number of distinct payment methods selectable for buying
      (coverage `state` `selectable_background` or `selectable_on_selection`) — with `side=sell`,
      the payout methods whose sell cell is selectable and has at least one selectable asset. A
      country's total is the sum.
    additionalProperties:
      type: integer
      minimum: 1
  default_fiat:
    type: string
    description: >-
      The currency `GET /v1/capabilities/buy_sell` would start this country on
      (`defaults.default_fiat`; with `side=sell`, `sell_defaults.default_fiat`).
  fiat_reason:
    type: string
    x-swaps-open-enum: true
    description: >-
      Why `default_fiat` is the one shown (D-65), as `defaults.fiat_reason`; absent when the
      snapshot does not say. A1-2: open — an unrecognized member is an opaque string.
    enum:
      - local_currency
      - fallback_fiat
      - requested
additionalProperties: false
```

<a id="monthlyvolumeband"></a>
## MonthlyVolumeBand

```yaml
type: string
enum:
  - under_5k
  - 5k_to_20k
  - 20k_to_50k
  - 50k_plus
description: >-
  An enum id, never a raw number — closes the label/midpoint disagreement bug class named in V-G14
  (D-53). RESERVED (L3-2, 2026-09-21): not referenced by `eligibility.get` today — that operation is
  public/ unauthenticated only (D-16 forbids this field on any anonymous path), so this enum is
  kept, unreferenced, for the authenticated `eligibility` variant a future item builds once the
  router's optional-auth route class exists (see `paths/platform.yaml`'s `eligibility.get`
  description).
```

<a id="eligibility"></a>
## Eligibility

```yaml
type: object
description: >
  The compliance-navigator engine's own answer (RESOURCE-MODEL §2.4), published by the PUBLIC,
  unauthenticated `eligibility.get` (L3-2). `country`/`customer_type` echo the normalized request;
  every other axis (`monthly_volume_band`, `purpose`, `regulated_activities`) is a KYB questionnaire
  that never takes an anonymous path (D-16) and is not part of this response.
required:
  - country
  - customer_type
  - products
  - disclaimers
properties:
  country:
    type: string
    description: The normalized (upper-cased) `country` the caller passed.
  customer_type:
    type: string
    enum:
      - individual
      - business
  disclaimers:
    type: array
    items:
      type: string
    description: >-
      Machine-readable disclaimer codes (fix round 1, P2) — mirrors `resolveEligibility`'s own
      `disclaimerKeys` (`src/lib/eligibility/engine.ts`), stripped of the
      `complianceNav.disclaimer.` i18n prefix. `not_advice` is present on every response;
      `approximate` when any product's `confidence` is `approximate`; `eea` for an EEA country;
      `sanctioned`/`unknown_country` replace the other three for those two hard-block branches (fix
      round 2, P3 corrected the spelling — round 1 shipped the reversed `country_unknown`, which is
      actually the SEPARATE `reasons[]` code this same branch publishes; `disclaimers[]` and
      `reasons[]` are deliberately different vocabularies and this was their one accidental
      collision). Never omitted — an empty array would itself be a false "no caveats" claim on the
      operation this `dark-flag` legal sign-off exists to gate.
  products:
    type: array
    items:
      type: object
      required:
        - id
        - status
        - confidence
        - required_customer_type
        - requires_bridge_verification
        - blockers
        - documents
        - reasons
        - notes
      properties:
        id:
          type: string
          enum:
            - exchange
            - wallet
            - payment_links
            - payouts
            - payroll
          description: >-
            The compliance-navigator engine's own five products (CAPABILITIES-CANON §1.5) —
            corrected from a copy-paste of the unrelated `/v1/capabilities` product enum
            (`payment_links|payouts|payroll| buy_sell|wallet_bank`), which answers a different,
            authenticated-caller question (L3-2 fix).
        status:
          type: string
          enum:
            - likely_available
            - conditional
            - unavailable
            - unknown
          description: >-
            The engine's own four statuses, verbatim — never collapse `likely_available` and
            `conditional` into one `available` (RESOURCE-MODEL §2.4).
        confidence:
          type: string
          enum:
            - exact
            - approximate
            - unknown
          description: >-
            How much this verdict can be trusted — `exact` mirrors a hard rule in code,
            `approximate` a client- visible mirror of a backend-owned truth (e.g. EEA membership),
            `unknown` insufficient data. Corrected from a numeric 0-1 stub (L3-2 fix) — the engine's
            own vocabulary is this 3-value enum (`src/lib/eligibility/types.ts`'s `Confidence`),
            never a probability.
        required_customer_type:
          type: string
          enum:
            - individual
            - business
            - either
          description: >-
            Added the missing `either` value (L3-2 fix) — every product but `payment_links` accepts
            either customer type; the stub previously could not express that.
        requires_bridge_verification:
          type: boolean
          description: >-
            Added fix round 1 (P1) as `requires_verification`, renamed fix round 2 (P2) — mirrors
            the compliance-navigator engine's own `requiresBridge`
            (`src/lib/eligibility/products.ts`'s `REQUIRES_BRIDGE`): `true` for `payment_links`,
            `payouts` and `payroll` (every product gated on a Bridge customer + endorsement the
            caller has not necessarily earned yet), `false` for `wallet` (non-custodial, no KYC) and
            `exchange`. Before this field, a Bridge-gated product at `status: likely_available` with
            an empty `blockers`/`reasons` read byte-identical to `wallet`, the one product the
            engine documents as its true unconditional "yes" — this flag (together with the
            `bridge_customer_missing` reason code those three products now lead `reasons[]` with) is
            the machine-readable signal that distinguishes them. The bare name
            `requires_verification` over-claimed for `exchange`: its `false` means only "no BRIDGE
            customer needed" — the canon engine still pairs that `false` with a
            `complianceNav.note.fiat_provider_kyc` note (a channel this API does not publish),
            because the fiat on/off-ramp runs the CARD PROVIDER's own KYC; only the DEX path is
            truly KYC-free. Renamed so the field says precisely the one fact it can back — whether a
            Bridge customer/endorsement is the gate — and nothing wider.
        blockers:
          type: array
          items:
            type: object
            required:
              - code
              - confidence
            properties:
              code:
                type: string
                description: >-
                  A machine-readable blocker code (e.g. `business_account_required`) — never prose;
                  a caller supplies its own copy by code.
              confidence:
                type: string
                enum:
                  - exact
                  - approximate
                  - unknown
            additionalProperties: false
          description: >-
            Corrected from a bare string array (L3-2 fix) — a blocker's own confidence must travel
            with it, exactly like a product's.
        documents:
          type: array
          items:
            type: object
            required:
              - slug
              - method_hint
              - display_label
            properties:
              slug:
                type: string
              method_hint:
                type: string
                enum:
                  - form
                  - hosted_kyc
                  - tos
                  - info
                description: >-
                  The SAME resolution vocabulary `customers.get`'s `requirements_due[].resolution`
                  already publishes (RESOURCE-MODEL §2.4 v2 amendments) — one vocabulary across both
                  resources, never a second UI-facing set of ids (settles V-G13/D-53: publish the
                  machine value, keep the display string in `display_label`, no free-text `copy`
                  field).
              display_label:
                type: string
                description: >-
                  A short, factual, plain-English label (e.g. "Government-issued ID") — never
                  marketing copy.
            additionalProperties: false
        limits:
          type: object
          description: >-
            Populated ONLY when a real, numeric SSOT backs it (today: `payouts`' per-currency
            corridor minimum, and only for a country the corridor's rail actually covers — see
            `reasons`' `no_local_payout_rail`). Every other product OMITS this field entirely rather
            than publish an approximate or fabricated figure — omitted, never null (RESOURCE-MODEL's
            own "omitted, never null, never fabricated" convention). `min` is a `Money` object, not
            a bare number (fix round 1, P1 — corrected from a scale-less major-unit float, the one
            float on an otherwise Money-typed `/v1` surface; `currency` is dropped as a redundant
            sibling field now that `Money` itself carries it). There is no `max` (fix round 2, P3
            dropped it — no producer here ever set one; publishing it would have documented "a knob
            nothing can ever turn," the exact shape this operation's own path description already
            argues against for the three dropped authenticated-only query parameters). Re-add `max`
            in the PR that first has a real per-corridor maximum to publish.
          required:
            - confidence
          properties:
            min:
              $ref: '#/components/schemas/Money'
            confidence:
              type: string
              enum:
                - exact
                - approximate
                - unknown
          additionalProperties: false
        reasons:
          type: array
          items:
            type: string
          description: >-
            Machine-readable reason codes — never prose. `country_sanctioned`/`country_unknown`
            uniformly replace every other code for those two hard-block branches.
            `bridge_customer_missing` (fix round 1, P1) leads `reasons[]` for every product whose
            `requires_bridge_verification` is `true` (`payment_links`/`payouts`/`payroll`),
            regardless of country or customer type — the caller has not created a Bridge customer
            yet, which is the CTA, not a `blockers[]` entry. `eea_verification_required` marks an
            EEA-country downgrade on those same three products (also published as a `blockers[]`
            entry at the same `approximate` confidence — fix round 1, P2) AND, since fix round 2
            (P3), on `exchange` too: round 1 gave `exchange` its own invented code
            (`eea_asset_restrictions`) on the theory that its EEA fact is a MiCA asset restriction
            (USDT/USDB), not an account-verification one — but the canon engine's own
            `resolveExchange` (`src/lib/eligibility/products.ts`) does not emit a matching code
            either; it pushes this SAME `eea_verification_required` and carries the MiCA-specific
            fact separately, in its own `noteKeys` — the same channel this operation's own `notes[]`
            below now mirrors. `exchange` shares the reason code for canon parity; `notes[]` is what
            tells the two facts (a MiCA asset restriction vs. a missing Bridge customer,
            `requires_bridge_verification: false` for this product) apart. `no_local_payout_rail`
            marks a `payouts` corridor whose currency exists but whose rail does not cover the
            requested country (e.g. Zimbabwe uses USD but has no US-domestic ACH/wire rail — fix
            round 1, P1; Monaco/San Marino/Vatican City are EUR-currency SEPA members this code no
            longer wrongly applies to as of fix round 2, P2 — see `paths/platform.yaml`).
        notes:
          type: array
          items:
            type: string
          description: >-
            Machine-readable note codes — a SECOND vocabulary from `reasons[]`, for a fact that
            isn't a status/blocker reason. Mirrors the compliance-navigator engine's own `noteKeys`
            (`src/lib/eligibility/products.ts`), stripped of the `complianceNav.note.` prefix
            exactly as `disclaimers[]` strips `complianceNav.disclaimer.`. Never omitted — empty for
            every product with none today. `exchange` always carries
            `dex_no_kyc`/`fiat_provider_kyc` (the DEX-vs-provider-KYC split its
            `requires_bridge_verification: false` alone cannot express) and adds `eea_assets` for an
            EEA country (the MiCA asset-restriction fact `reasons[]`' shared
            `eea_verification_required` code cannot, on its own, distinguish from the other three
            products' real Bridge-verification gate).
      additionalProperties: false
additionalProperties: false
```

<a id="customer"></a>
## Customer

```yaml
description: >
  Verification status only — never a document, a submitted KYC form value, a bank detail or a
  beneficial owner's name (RESOURCE-MODEL §2.4, R17 PII rule).
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - customer_type
      - verification_state
    properties:
      id:
        type: string
        pattern: ^cus_
        example: cus_01J9ZK3Q8M2F5A7C9E1G3H5J7K
      object:
        type: string
        enum:
          - customer
      customer_type:
        type: string
        enum:
          - individual
          - business
      verification_state:
        type: string
        x-swaps-open-enum: true
        enum:
          - not_started
          - pending
          - under_review
          - approved
          - rejected
          - stale_mapping
          - action_required
        description: >-
          Swaps' own verification-state vocabulary (R17 §3): `not_started` (verification not yet
          begun), `pending` (submitted, awaiting review), `under_review` (a review is actively in
          progress), `approved` (cleared), `rejected` (declined — see
          `rejection_outcome`/`rejection_guidance`), `stale_mapping` (our record of this customer
          needs an operator to repair it before verification can continue), `action_required` (a
          `requirements_due[]` entry is blocking progress). Normalized to these seven values today,
          but the field is marked forward-compatible (`x-swaps-open-enum`) — treat an unrecognized
          member as opaque rather than a deserialization failure.
      requirements_outstanding:
        type: integer
        description: Count backing `requirements_due[]`.
      requirements_due:
        type: array
        items:
          type: object
          required:
            - slug
            - resolution
          properties:
            slug:
              type: string
              description: >-
                An identifier for this requirement, in our verification partner's own vocabulary
                today — not yet a closed Swaps enum. Route on `resolution` below, render
                `display_label` for copy; never show this raw slug to a user (D-53, V-G2).
            resolution:
              type: string
              enum:
                - form
                - hosted_kyc
                - tos
                - info
              description: >-
                Swaps' own resolution vocabulary for this requirement (D-53, V-G2): `form` (fill it
                in inside Swaps), `hosted_kyc` (finish it on a hosted verification link), `tos`
                (accept terms), `info` (informational only, no action to take). Publishes the
                BEHAVIOR a client must reproduce, never the raw `slug` above.
            display_label:
              type: string
            due_at:
              type: string
              format: date-time
              description: >-
                The verification deadline, when one applies (D-53, V-G12) — e.g. an EEA
                re-verification window. Not read live on this request, so it can trail a
                `requirements_due[]` set that changed very recently. Never present for a
                non-verification slug (e.g. `external_account`), once the account's KYC is already
                approved, or once the deadline itself has passed.
          additionalProperties: false
      endorsements:
        type: array
        items:
          type: string
        description: >-
          The payment rails this customer is cleared for (R17 §3) — Swaps' own rail codes: `base`
          (the crypto/stablecoin base rail every customer needs first), `sepa` (EUR via SEPA),
          `spei` (MXN via SPEI), `pix` (BRL via Pix), `faster_payments` (GBP via Faster Payments),
          `cop` (COP via bank transfer) — the array is open: a rail code outside this list is
          possible and must be ignored, never treated as an error.
      endorsement_statuses:
        type: array
        description: >-
          Per-rail status — one entry per rail for which the upstream snapshot carries a per-rail
          status object, including rails not yet approved; it can be empty even when
          `endorsements[]` is not. Each item names its own rail in `endorsement`; never pair by
          index with `endorsements[]`, which lists only the approved subset and can be shorter or
          differently ordered.
        items:
          type: object
          properties:
            endorsement:
              type: string
            status:
              type: string
          additionalProperties: false
      next_step:
        type: string
        description: >-
          The single next step that unblocks verification (source text for
          `get_verification_status`).
      tos_status:
        type: string
        description: >-
          Terms-of-service acceptance status, as our verification partner reports it today — not yet
          normalized onto a closed Swaps enum (unlike `verification_state` above); treat any
          non-`approved` value as "not yet accepted" rather than enumerating every possible string.
      kyc_status:
        type: string
        description: >-
          KYC/identity-verification status, as our verification partner reports it today — already
          folded into `verification_state` above for the canonical read; not yet normalized onto its
          own closed Swaps enum.
      available_actions:
        type: array
        items:
          type: string
      stale:
        type: boolean
        description: Restart onboarding — survives to the wire, never collapsed into another state.
      degraded:
        type: boolean
        description: Cached — we could not reach the provider. Survives to the wire, never collapsed.
      verification_checked_at:
        type: string
        format: date-time
        description: >-
          When the verification facts on this object were last checked with the verification
          provider: the moment of a live read, or, with `verification=snapshot`, when Swaps last
          received the customer's whole state from the provider (never more than 24 hours earlier).
          Show it next to the status ("Checked …"). Absent for a degraded read of a check whose time
          is unknown.
      rejection_outcome:
        type: string
        description: >-
          An outcome enum converted server-side from `developer_reason` — never the raw provider
          reason (RESOURCE-MODEL §2.4). Known values include `verification_unavailable` and
          `terminal`; the full set is not yet published upstream.
      rejection_guidance:
        type: array
        items:
          type: string
        description: Bounded, safe-copy only — never `developer_reason` passthrough (V-G3).
      verification_attempts_exhausted:
        type: boolean
        description: >-
          `true` when verifying is this customer's next step (`verification_state` `not_started`,
          `action_required` or `rejected`) and it has used every verification attempt Swaps starts
          (two rejections): `POST /customers/{id}/verification_links` with `kind: kyc` mints no
          further link, and support is the next step — do not offer a retry. Absent otherwise; never
          published for an `approved`, `pending` or `under_review` customer. The count itself is not
          published.
      compliance_review_state:
        type: string
      associated_persons_count:
        type: integer
        description: A count, never a name — the only UBO fact on the base object (V-G4).
    additionalProperties: false
unevaluatedProperties: false
```

<a id="customercreaterequest"></a>
## CustomerCreateRequest

```yaml
description: >
  Start verification for THIS account's own holder — never a customer-of-a-customer (RESOURCE-MODEL
  D-3). Never a name, email, date of birth, address, document or tax id, with ONE exception: the
  server resolves the caller's account owner internally and reads its own email, and — for
  `customer_type: individual` — its own name, when a real first+last pair is already on file, never
  a merchant/brand name or the email local part. **Round-2 architect ruling (2026-09-21)**: when no
  trustworthy name is on file, verification starts WITHOUT one — our verification partner's hosted
  KYC reads the legal name off the identity document itself. This closes the round-1 `409
  customer_name_required` dead end (fix-pack, independent review, P1), which is removed.
  `legal_name` below is the one field this body may carry: a registered company name is not personal
  data.
type: object
required:
  - customer_type
properties:
  customer_type:
    type: string
    enum:
      - individual
      - business
    description: >-
      Which kind of verification to start: a natural person, or a business. Never written to the
      account: `Account.customer_type` follows the type Swaps records for the customer, which is
      this one for a new customer and the customer's own type when our verification partner already
      holds a customer for the account's owner (the `201` body's `customer_type` shows it).
  country:
    type: string
    pattern: ^[A-Z]{2}$
    description: >-
      ISO 3166-1 alpha-2, when known — validated against the SAME market registry `POST
      /v1/accounts` validates it against (`400 invalid_request`, `param: country`, for a well-formed
      but unrecognised code — fix-pack, independent review, P2), then persisted ONLY when the
      account has no country on file yet; a country the account already declared is never silently
      overwritten. Does not yet select a payment rail for this customer — a future PR can map
      country to a rail once product confirms which one.
  legal_name:
    type: string
    minLength: 1
    maxLength: 200
    description: >-
      `customer_type: business` ONLY — the registered company name, trimmed, forwarded to our
      verification partner as the entity's own legal name. Not personal data (round-2 architect
      ruling, 2026-09-21): a business's registered name is fine to accept from the caller, unlike a
      natural person's legal name, which this endpoint never accepts from any request body. `400
      invalid_request` (`param: legal_name`) when `customer_type` is `individual`. Omitted, a
      business customer is created without a name on file — same treatment an individual with no
      name on file gets.
additionalProperties: false
```

<a id="customerverificationlinkrequest"></a>
## CustomerVerificationLinkRequest

```yaml
type: object
required:
  - kind
  - return_to
properties:
  kind:
    type: string
    enum:
      - tos
      - kyc
      - business_questionnaire
      - business_ubo
      - remediation
    description: >-
      One polymorphic mint across every hosted verification hand-off (D-52, V-G17): `tos` (terms of
      service), `kyc` (identity/business verification), `business_questionnaire` (compliance
      questionnaire), `business_ubo` (beneficial-owner check), `remediation` (a requested fix to a
      prior submission).
  return_to:
    type: string
    format: uri
    description: >-
      Required, and validated against an origin allowlist (`https://swaps.app`,
      `https://www.swaps.app`, `https://staging.swaps.app`; anything else is `400 invalid_request`,
      `param: return_to`). It is the hosted hand-off's return target: the verification partner sends
      the holder back to this URL when the terms or verification page is done (widget v2 D-35,
      closing the dead end where every hand-off landed on `/confirm` instead of the hub — D-52,
      V-G18, prod P-B3). Swaps appends nothing to it; keep personal data out of it. A `kyc` link
      that cannot be minted with this return target fails rather than returning a link that would
      send the holder elsewhere.
  rail:
    type: string
    enum:
      - ach
      - wire
      - sepa
      - faster_payments
      - pix
      - spei
      - bre_b
      - co_bank_transfer
    description: >-
      Optional, and only with `kind: kyc` (with any other kind it is `400 invalid_request`, `param:
      rail`): the rail the holder is verifying for — the payout rail of a sell, or the pay-in rail
      of a buy. For an individual customer, `sepa`, `faster_payments`, `pix`, `spei`, `bre_b` and
      `co_bank_transfer` ask our verification partner for that rail's approval (its endorsement —
      `sepa`, `faster_payments`, `pix`, `spei`, and `cop` for both Colombian rails) when the
      customer does not carry it yet, and the link then opens on that approval's own requirements.
      `ach` and `wire` ride the approval every customer is created with, so the link is the same as
      without `rail`. A business customer's link is never scoped by `rail`. Absent: the link is
      minted exactly as before this field existed. Read `Customer.endorsement_statuses` for the
      outcome — a link is not an approval. When the approval cannot be requested right now the
      answer is `503 temporarily_unavailable`; retry with a new `Idempotency-Key`.

      Once an individual's verification is approved (before that the link starts identity
      verification and asks for the rail as before), and before anything is asked of our
      verification partner, their live approval for a buy on that rail is read (`pix` reads
      `pix_onramp`; `spei`, `faster_payments`, `sepa` their own; both Colombian rails `cop`). When a
      hosted verification link cannot move it, nothing is requested and no link is minted: `403
      permission_error`, `code: endorsement_required`, `details: {payment_rail, endorsement, next}`,
      where `next` is `accept_terms` (mint a `kind: tos` link first), `complete_profile` (a
      questionnaire the hosted flow does not collect), `provider_review` (the partner is holding it;
      nothing to do) or `not_available` (the partner does not offer the rail to this customer). For
      `pix` the payout side counts too: the link is minted when either direction (`pix_onramp`, or
      `pix_offramp` / `pix`) has a step the hosted flow takes or is approved, and otherwise names
      the most actionable step of either. A status that could only be read from a cached copy does
      not decide that step: the link is requested as before. The customer's country of residence
      counts too, on our verification partner's live read, whatever the verification state
      (`purpose`, below).
  purpose:
    type: string
    enum:
      - buy
      - sell
    description: >-
      Optional, and only beside a `rail` (otherwise `400 invalid_request`, `param: purpose`): what
      the rail-scoped link is for — `buy` when the holder pays in on the rail, `sell` when they are
      paid out on it. The customer's country of residence as our verification partner holds it is
      read before anything is requested, whatever the verification state, and a rail the partner
      does not open to residents of that country is refused — `403 permission_error`, `code:
      endorsement_required`, `next: not_available`, no link: `bre_b` / `co_bank_transfer` for
      residents of the EU/EEA whatever `purpose` says (the partner does not offer COP to EU
      customers in either direction), and with `buy`, `pix` outside Brazil. An approval the customer
      already holds is never refused on residence; `spei`, `faster_payments` and `sepa` never are. A
      residence the partner does not hold decides nothing until the individual's verification is
      approved; after that, a `buy` link on `pix` or any link on `bre_b` / `co_bank_transfer` with
      no residence on file is refused with `next: provider_review`. With `buy`, the `pix` payout
      side (`pix_offramp` / `pix`) does not count either: only the buy approval (`pix_onramp`)
      decides. A `pix` link with `sell` or without this field is not refused on residence (a payout
      on `pix` is open to residents of other countries). New values may be added.
additionalProperties: false
```

<a id="customerverificationlink"></a>
## CustomerVerificationLink

```yaml
type: object
required:
  - url
  - expires_at
properties:
  url:
    type: string
    format: uri
  expires_at:
    type: string
    format: date-time
additionalProperties: false
```

<a id="associatedperson"></a>
## AssociatedPerson

```yaml
type: object
description: >
  Name-free by policy (D-49) for a business key or a third-party agent; `name` is published only for
  the holder's own delegated session.
required:
  - label
properties:
  label:
    type: string
    description: >-
      `"Owner 1"`, `"Owner 2"`, … — never a raw internal object id, never a name (CAPABILITIES-CANON
      owner-task rule).
  name:
    type:
      - string
      - 'null'
    description: >-
      Present only when the caller is the holder's own delegated session (`dashboard_session`);
      always null for a business key or a third-party agent.
  ownership_percent:
    type: number
    minimum: 0
    maximum: 100
    description: >-
      Omitted, never defaulted to 0, when our verification partner's raw per-person payload does not
      carry a recognized ownership field. A client must render the absence honestly (e.g. "—"),
      never as an observed 0% — the contract trap issues #3086/#3088 name (a server default is not
      an observation).
  status:
    type: string
    description: >-
      Per-owner verification status. Not yet given a fixed Swaps enum — optional and omitted until a
      confirmed per-person status field exists upstream; today no source in this codebase reads one
      (only `associated_persons.length` is read). A follow-up derives it from the same endorsement
      `missing[]` object-scoped bundles the Verification Hub already reads, once a live upstream
      response confirms the shape.
additionalProperties: false
```

<a id="associatedpersonlist"></a>
## AssociatedPersonList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/AssociatedPerson'
unevaluatedProperties: false
```

<a id="complianceprofilerequest"></a>
## ComplianceProfileRequest

```yaml
type: object
description: >
  Every field below is Swaps' own compliance-profile vocabulary — every field optional, only the
  ones present are submitted. Enum fields must match the published list for that field EXACTLY — an
  unrecognized value is `400`, never silently dropped or coerced. The three free-text fields
  (`account_purpose_other`, `business_description`, `source_of_funds_description`) publish a
  `maxLength: 1024`, and a value past it is `400` by the SAME generated-schema gate this route
  validates every request against — never silent truncation. One exception remains within the cap: a
  whitespace-only free-text value is trimmed and dropped entirely (never stored as an empty string)
  rather than rejected.
properties:
  account_purpose:
    type: string
    enum:
      - charitable_donations
      - ecommerce_retail_payments
      - investment_purposes
      - operating_a_company
      - other
      - payments_to_friends_or_family_abroad
      - payroll
      - personal_or_living_expenses
      - protect_wealth
      - purchase_goods_and_services
      - receive_payment_for_freelancing
      - receive_payments_for_goods_and_services
      - receive_salary
      - tax_optimization
      - third_party_money_transmission
      - treasury_management
    description: >-
      The union of the individual (11) and business (13) `account_purpose` values (16 unique — some
      overlap). Which of these are valid is further restricted by the customer's own
      `customer_type`; a mismatched-but-listed value still gets `400`.
  account_purpose_other:
    type: string
    maxLength: 1024
    description: >-
      Free text — only meaningful (and only read) when `account_purpose` is `other`. `400` past 1024
      characters (never silently truncated) — see this schema's own description.
  source_of_funds:
    type: string
    enum:
      - business_loans
      - company_funds
      - ecommerce_reseller
      - gambling_proceeds
      - gifts
      - government_benefits
      - grants
      - inheritance
      - inter_company_funds
      - investment_proceeds
      - investments_loans
      - legal_settlement
      - owners_capital
      - pension_retirement
      - salary
      - sale_of_assets
      - sale_of_assets_real_estate
      - sales_of_goods_and_services
      - savings
      - someone_elses_funds
      - third_party_funds
      - treasury_reserves
    description: >-
      The union of the individual (12) and business (11) `source_of_funds` enum values (22 unique —
      one overlap, `pension_retirement`).
  source_of_funds_description:
    type: string
    maxLength: 1024
    description: >-
      Free text. `400` past 1024 characters (never silently truncated) — see this schema's own
      description.
  employment_status:
    type: string
    enum:
      - employed
      - homemaker
      - retired
      - self_employed
      - student
      - unemployed
    description: Individual only — current employment status, Swaps' own six-value vocabulary.
  expected_monthly_payments_usd:
    type: string
    enum:
      - '0_4999'
      - '5000_9999'
      - '10000_49999'
      - 50000_plus
    description: Expected monthly payment volume through Swaps, banded in USD.
  business_description:
    type: string
    maxLength: 1024
    description: >-
      Free text — the business's own description of what it does. `400` past 1024 characters (never
      silently truncated) — see this schema's own description.
  business_type:
    type: string
    enum:
      - cooperative
      - corporation
      - llc
      - other
      - partnership
      - sole_prop
      - trust
    description: Business only — legal structure, Swaps' own seven-value vocabulary.
  business_industry:
    type: string
    pattern: ^[0-9]{6}$
    description: >-
      A single 2022 NAICS six-digit code identifying the business's industry — a public US federal
      classification standard, not a Swaps- or partner-specific list. Too large to enumerate here,
      so only the `^[0-9]{6}$` shape is checked at this layer; the closed set of valid codes is
      enforced server-side, never free text.
  estimated_annual_revenue_usd:
    type: string
    enum:
      - '0_99999'
      - '100000_999999'
      - '1000000_9999999'
      - '10000000_49999999'
      - '50000000_249999999'
      - 250000000_plus
    description: Business only — estimated annual revenue, banded in USD.
  high_risk_activities:
    type: array
    items:
      type: string
      enum:
        - adult_entertainment
        - gambling
        - hold_client_funds
        - investment_services
        - lending_banking
        - marijuana_or_related_services
        - money_services
        - nicotine_tobacco_or_related_services
        - operate_foreign_exchange_virtual_currencies_brokerage_otc
        - pharmaceuticals
        - precious_metals_precious_stones_jewelry
        - safe_deposit_box_rentals
        - third_party_payment_processing
        - weapons_firearms_and_explosives
        - none_of_the_above
    description: Zero or more of 15 regulated-activity values (includes `none_of_the_above`).
additionalProperties: false
```

<a id="complianceprofileresult"></a>
## ComplianceProfileResult

```yaml
type: object
description: >-
  Write-only confirmation — deliberately does NOT echo any submitted field back (V-G5, V-G15): the
  caller's own form already holds what it just typed, and re-displaying a "previous answer" on next
  open would contradict the screen's own promise that Swaps does not store or show it.
required:
  - accepted
properties:
  accepted:
    type: boolean
    enum:
      - true
additionalProperties: false
```

<a id="customereventlist"></a>
## CustomerEventList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Event'
unevaluatedProperties: false
```

<a id="customerlist"></a>
## CustomerList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Customer'
unevaluatedProperties: false
```

<a id="supportcasemessageauthor"></a>
## SupportCaseMessageAuthor

```yaml
type: string
description: >-
  Who wrote a message: `customer` is the signed-in person, `swaps_support` is Swaps Support. No
  operator is ever named. An open enum: treat a value you do not recognise as an opaque string.
enum:
  - customer
  - swaps_support
x-swaps-open-enum: true
```

<a id="supportcase"></a>
## SupportCase

```yaml
description: >-
  One support request a signed-in person made, and the conversation that followed it. It belongs to
  the person, never to an account. `livemode` is always `true` (support is not available in test
  mode) and `created_at` is when the request was received. There is no `updated_at`: internal work
  must not show through a timestamp.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - status
      - status_group
      - topic
      - subject
      - latest_message
      - closed_at
      - reply_email
      - related_object
      - accepts_attachments
      - attachments_left
    properties:
      id:
        type: string
        pattern: ^sc_
      object:
        type: string
        enum:
          - support_case
      status:
        type: string
        description: >-
          What the person sees. `waiting_on_customer` is "Waiting for your answer": the case is
          waiting and the newest visible message is from Swaps Support. `with_support` is "With our
          team", every other open request. `closed` is "Closed". An open enum: treat a value you do
          not recognise as an opaque string.
        enum:
          - waiting_on_customer
          - with_support
          - closed
        x-swaps-open-enum: true
      status_group:
        type: string
        description: The bucket of `status`, and the filter `GET /v1/support_cases` takes.
        enum:
          - needs_you
          - in_progress
          - done
      topic:
        type:
          - object
          - 'null'
        description: >-
          The intake registry keys the request was filed under, shown through the
          `support_intake.products.<key>` and `.intents.<key>` strings. `null` for a request from
          before intake v2.
        required:
          - product
          - intent
        properties:
          product:
            type: string
          intent:
            type: string
      subject:
        type: string
        description: >-
          The request's subject: what the person typed in the older form, or the registry label pair
          of a newer request. Show the topic label when `topic` is set.
      latest_message:
        type: object
        description: The newest visible message, or the opening request when nothing followed.
        required:
          - author
          - created_at
        properties:
          author:
            $ref: '#/components/schemas/SupportCaseMessageAuthor'
          created_at:
            type: string
            format: date-time
      closed_at:
        type:
          - string
          - 'null'
        format: date-time
        description: >-
          Set while `status` is `closed`: the resolution time, else the request's last update (a
          request closed before the case workflow).
      reply_email:
        type:
          - string
          - 'null'
        format: email
        description: >-
          Where Swaps Support's replies are also emailed: the person's own address as stored with
          the request; `null` when none is deliverable.
      related_object:
        description: >-
          The object the request is about: the one Swaps Support linked it to, else the one the
          person chose when they wrote. Checked again against the person on every read: `null` once
          the object is no longer theirs, and for a request about no object, a verification request,
          a request written without signing in and every request from before objects existed.
        oneOf:
          - $ref: '#/components/schemas/SupportCaseRelatedObject'
          - type: 'null'
      accepts_attachments:
        type: boolean
        description: >-
          Whether this request takes files now. `false` while files are switched off, on the
          Identity verification and Business verification topics and on a verification partner's
          request (sending one is `409 attachments_not_accepted`).
      attachments_left:
        type: integer
        minimum: 0
        maximum: 25
        description: >-
          How many more files this request can take: 25 less the files on it and the uploads of it
          still in flight. `0` whenever `accepts_attachments` is `false`.
unevaluatedProperties: false
```

<a id="supportcasemessage"></a>
## SupportCaseMessage

```yaml
description: >-
  One visible message of a support request. `livemode` is always `true`; `created_at` is when the
  message joined the conversation, and for Swaps Support, when it was sent.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - support_case
      - author
      - body
      - attachments
    properties:
      id:
        type: string
        pattern: ^scm_
      object:
        type: string
        enum:
          - support_case_message
      support_case:
        type: string
        pattern: ^sc_
        description: The support request the message belongs to.
      author:
        $ref: '#/components/schemas/SupportCaseMessageAuthor'
      body:
        type: string
        description: Plain text, line breaks kept. Never HTML or Markdown.
      attachments:
        type: array
        maxItems: 5
        description: >-
          The files the person sent with this message, in the order they put them; `[]` for none,
          and always `[]` on a message from Swaps Support. The opening request (`scm_` + the
          request's id) carries the files that went with the request. Inside a message a file has no
          `support_case` and no `upload`.
        items:
          $ref: '#/components/schemas/SupportCaseAttachment'
unevaluatedProperties: false
```

<a id="supportcaseattachment"></a>
## SupportCaseAttachment

```yaml
description: >-
  One file a signed-in person sent, or is sending, to Swaps Support. It belongs to the person, never
  to an account. `livemode` is always `true` (support is not available in test mode) and
  `created_at` is when its slot was made. There is no `updated_at`: internal work must not show
  through a timestamp. The bytes are not here: a file that is `ready` or `attached` is read through
  `download_url`.
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    required:
      - status
      - filename
      - content_type
      - kind
      - size
      - width
      - height
      - message
      - rejection
      - download_url
      - download_url_expires_at
    properties:
      id:
        type: string
        pattern: ^sca_
      object:
        type: string
        enum:
          - support_case_attachment
      status:
        type: string
        description: >-
          Where the file is. `awaiting_upload`: the slot is open and no bytes have been checked.
          `checking`: Swaps is checking them. `ready`: checked and stored, not yet on a message.
          `attached`: on a message. `rejected`: the check refused the bytes (see `rejection`).
          `removed`: deleted by the person, by time or by Swaps Support. An open enum: treat a value
          you do not recognise as an opaque string.
        enum:
          - awaiting_upload
          - checking
          - ready
          - attached
          - rejected
          - removed
        x-swaps-open-enum: true
      filename:
        type:
          - string
          - 'null'
        description: >-
          The person's own name for the file, cleaned (no control or direction characters, no path,
          at most 200 characters): show it escaped. `null` once the file is `rejected` or `removed`,
          and in the answer to a replay of the same `Idempotency-Key` of `create` or `complete`,
          which keeps no name: read it with `GET /v1/support_cases/attachments/{id}`. It is never in
          a URL, a storage key or a log line.
      content_type:
        type:
          - string
          - 'null'
        description: The type Swaps found in the bytes, never the one declared; `null` until the check has run.
        enum:
          - image/png
          - image/jpeg
          - image/webp
          - image/heic
          - image/heif
          - application/pdf
          - null
      kind:
        type:
          - string
          - 'null'
        description: '`image` or `pdf`, from `content_type`; `null` until the check has run.'
        enum:
          - image
          - pdf
          - null
      size:
        type:
          - integer
          - 'null'
        minimum: 1
        maximum: 10485760
        description: The size in bytes Swaps stored; `null` until the check has run.
      width:
        type:
          - integer
          - 'null'
        minimum: 1
        maximum: 30000
        description: Pixels, for an image once checked; `null` for a PDF and before the check.
      height:
        type:
          - integer
          - 'null'
        minimum: 1
        maximum: 30000
        description: Pixels, for an image once checked; `null` for a PDF and before the check.
      support_case:
        type:
          - string
          - 'null'
        pattern: ^sc_
        description: >-
          The request the file is for; `null` for a file that goes with a new request until that
          request is opened. Absent inside a message's `attachments`, which the message already
          names.
      message:
        type:
          - string
          - 'null'
        pattern: ^scm_
        description: The message the file is on once it is `attached`; `null` before.
      rejection:
        description: Why the check refused the bytes; `null` unless `status` is `rejected`.
        oneOf:
          - type: object
            required:
              - reason
            properties:
              reason:
                type: string
                description: >-
                  The same reason `422 attachment_rejected` carries in `details.reason`. An open
                  enum: treat a value you do not recognise as an opaque string.
                enum:
                  - type_not_allowed
                  - type_mismatch
                  - size_mismatch
                  - empty
                  - incomplete
                  - image_dimensions
                  - image_unreadable
                  - pdf_unreadable
                  - pdf_encrypted
                  - pdf_active_content
                x-swaps-open-enum: true
          - type: 'null'
      download_url:
        type:
          - string
          - 'null'
        format: uri
        description: >-
          A link that downloads the file (it carries `Content-Disposition: attachment`), valid until
          `download_url_expires_at`. `null` unless `status` is `ready` or `attached` and files are
          switched on, and when signing the link failed. A bearer link: do not store or share it.
      download_url_expires_at:
        type:
          - string
          - 'null'
        format: date-time
        description: When `download_url` stops working (10 minutes after it was made); `null` with it.
      upload:
        description: >-
          Where to put the bytes. Present only on the answer of `POST
          /v1/support_cases/attachments`, and `null` there when the answer is a replay of the same
          `Idempotency-Key`, because the token is never stored. Absent everywhere else.
        oneOf:
          - type: object
            required:
              - url
              - method
              - headers
              - expires_at
            properties:
              url:
                type: string
                format: uri
                description: A one-time upload address for exactly this file. Do not log or share it.
              method:
                type: string
                enum:
                  - PUT
              headers:
                type: object
                description: Send these headers, unchanged, with the `PUT`.
                additionalProperties:
                  type: string
              expires_at:
                type: string
                format: date-time
                description: When the slot ends (15 minutes after it was made).
          - type: 'null'
unevaluatedProperties: false
```

<a id="supportcaseattachmentcreaterequest"></a>
## SupportCaseAttachmentCreateRequest

```yaml
type: object
additionalProperties: false
required:
  - filename
  - content_type
  - size
properties:
  filename:
    type: string
    minLength: 1
    maxLength: 255
    description: >-
      The file's name as the person has it. Swaps cleans it (control and direction characters
      removed, anything up to the last `/` or `\` dropped, spaces collapsed, at most 200 characters
      keeping the extension); a name that is empty after cleaning is `400
      attachment_filename_invalid`. It is shown, escaped, to the person and to Swaps Support, and is
      never put in a URL, a storage key or a log line.
  content_type:
    type: string
    description: >-
      The type the client says the file has. A closed set: any other value is `400
      attachment_type_not_allowed`. Swaps decides the stored type from the bytes, so this never sets
      it.
    enum:
      - image/png
      - image/jpeg
      - image/webp
      - image/heic
      - image/heif
      - application/pdf
  size:
    type: integer
    minimum: 1
    maximum: 10485760
    description: >-
      The size in bytes the client says the file has, 1 to 10,485,760 (10 MiB); a larger one is `400
      attachment_too_large`. The check compares it with the bytes that arrive (`size_mismatch`).
  support_case:
    type: string
    pattern: ^sc_
    maxLength: 64
    description: >-
      The request the file is for, when it goes with a reply. Omit it for a file that goes with a
      new request. Another person's request and one that does not exist are `404`.
```

<a id="supportcaseattachmentcompleterequest"></a>
## SupportCaseAttachmentCompleteRequest

```yaml
type: object
additionalProperties: false
description: An empty object. The file is the one in the path; nothing else is sent.
```

<a id="supportattachmentpolicy"></a>
## SupportAttachmentPolicy

```yaml
type: object
description: >-
  What files a support request can take. The same for every person; whether one request takes files
  now is `SupportCase.accepts_attachments`.
required:
  - max_file_bytes
  - max_files_per_message
  - max_files_per_case
  - content_types
  - topics_without_attachments
properties:
  max_file_bytes:
    type: integer
    minimum: 1
    description: The largest file, in bytes (10 MiB).
  max_files_per_message:
    type: integer
    minimum: 1
    description: The most files on one message.
  max_files_per_case:
    type: integer
    minimum: 1
    description: The most files on one request, all its messages together.
  content_types:
    type: array
    description: >-
      The types a file may be. An open list, so treat a value you do not recognise as an opaque
      string.
    items:
      type: string
      enum:
        - image/png
        - image/jpeg
        - image/webp
        - image/heic
        - image/heif
        - application/pdf
      x-swaps-open-enum: true
  topics_without_attachments:
    type: array
    description: >-
      The topics (as `SupportCase.topic` names them) whose requests take no files: every branch of
      Identity verification and Business verification, whatever the intent. The field there says:
      «ID documents go through Verification.»
    items:
      type: object
      required:
        - product
        - intent
      properties:
        product:
          type: string
        intent:
          type: string
unevaluatedProperties: false
```

<a id="supportcasesummary"></a>
## SupportCaseSummary

```yaml
type: object
required:
  - by_status_group
properties:
  by_status_group:
    type: object
    description: >-
      Unbounded counts over every support request the person owns, like `ActivitySummary`: a cursor
      list never carries a total.
    required:
      - needs_you
      - in_progress
      - done
    properties:
      needs_you:
        type: integer
        minimum: 0
        description: Waiting for the person's answer.
      in_progress:
        type: integer
        minimum: 0
        description: With our team.
      done:
        type: integer
        minimum: 0
        description: Closed.
unevaluatedProperties: false
```

<a id="supportcaselist"></a>
## SupportCaseList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/SupportCase'
unevaluatedProperties: false
```

<a id="supportcasemessagelist"></a>
## SupportCaseMessageList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/SupportCaseMessage'
unevaluatedProperties: false
```

<a id="supportcasemessagecreaterequest"></a>
## SupportCaseMessageCreateRequest

```yaml
type: object
additionalProperties: false
required:
  - body
properties:
  body:
    type: string
    minLength: 1
    maxLength: 10000
    description: >-
      Plain text, 1 to 10,000 characters (Unicode code points), at least one that is not a space; no
      control characters but tab, line feed and carriage return. Never include passwords, API keys,
      recovery phrases, documents or bank details. With files the text is still required.
  attachments:
    type: array
    maxItems: 5
    uniqueItems: true
    description: >-
      Up to 5 files the person uploaded for this request and had checked (`ready`), by their ids
      from `POST /v1/support_cases/attachments`, in the order they should show. Screenshots,
      receipts and PDFs help. Never send passwords, keys or recovery phrases. Omit it, or send `[]`,
      for a reply without files. A file that cannot go on this message is `409
      attachment_unavailable` with `param: attachments[i]`.
    items:
      type: string
      pattern: ^sca_
      maxLength: 64
```

<a id="supportcasecreaterequest"></a>
## SupportCaseCreateRequest

```yaml
type: object
additionalProperties: false
required:
  - topic
  - body
properties:
  topic:
    type: object
    additionalProperties: false
    description: >-
      The intake registry branch the request is filed under, as `SupportCase.topic`: the topic the
      person picked, or the `topic` of the related object's row.
    required:
      - product
      - intent
    properties:
      product:
        type: string
        minLength: 1
        maxLength: 64
      intent:
        type: string
        minLength: 1
        maxLength: 64
  related_object:
    type:
      - string
      - 'null'
    maxLength: 64
    description: >-
      The prefixed id of one of the person's own orders (`ord_`), payment links (`pl_`), invoice
      payments (`po_`) or pay runs (`pr_`), from `GET /v1/support_cases/related_objects`; omitted or
      `null` for a request about no object. Another person's object, one that does not exist and a
      malformed id are `404`, never `400`.
  body:
    type: string
    minLength: 3
    maxLength: 10000
    description: >-
      Plain text, 3 to 10,000 characters (Unicode code points) after trimming, at least one that is
      not a space, and no control character but tab and line feed; the request as stored is at most
      32 KiB. Never include passwords, API keys, recovery phrases, documents or bank details.
  locale:
    type: string
    minLength: 2
    maxLength: 16
    description: The language the person wrote in, one the dashboard offers; default `en`.
  entrypoint:
    type: string
    description: Where the person wrote from; default `dashboard`. The set is closed.
    enum:
      - dashboard
      - product_detail
  attachments:
    type: array
    maxItems: 5
    uniqueItems: true
    description: >-
      Up to 5 files the person uploaded without a `support_case` and had checked (`ready`), by their
      ids from `POST /v1/support_cases/attachments`, in the order they should show. They go on the
      new request, or on the reply when the text joins an open one. A request with files still needs
      words. Screenshots, receipts and PDFs help. Never send passwords, keys or recovery phrases.
      The Identity verification and Business verification topics take no files (`409
      attachments_not_accepted`).
    items:
      type: string
      pattern: ^sca_
      maxLength: 64
```

<a id="supportrelatedobjectdisplay"></a>
## SupportRelatedObjectDisplay

```yaml
type: object
description: >-
  The facts a title is built from; the client composes the title ("Buy · 0.0042 BTC", "Sell · 120.00
  USDC to EUR", "Payment link · Logo design", "Pay run · October"; an invoice payment reads "Payout
  · {amount}" in a request and "Invoice payment · {name}" where the person picks it). Names are the
  person's own text: no third party's email, provider id or internal id is ever a field.
required:
  - label
  - amount
  - to_currency
  - name
properties:
  label:
    type: string
    description: >-
      What the object is: an order's side (`buy`, `sell`, `swap`), `payment_link`,
      `invoice_payment`, `pay_run`, or a verification row's `identity_verification` or
      `business_verification`. An open enum: treat a value you do not recognise as an opaque string.
    enum:
      - buy
      - sell
      - swap
      - payment_link
      - invoice_payment
      - pay_run
      - identity_verification
      - business_verification
    x-swaps-open-enum: true
  amount:
    description: >-
      A buy's receive leg, a sell's or a swap's paid leg, an invoice payment's invoice amount;
      `null` for every other kind and when the amount's currency scale cannot be resolved (never a
      guessed figure).
    oneOf:
      - $ref: '#/components/schemas/Money'
      - type: 'null'
  to_currency:
    type:
      - string
      - 'null'
    description: The currency a sell pays out or the asset a swap receives; `null` elsewhere.
  name:
    type:
      - string
      - 'null'
    description: >-
      A payment link's or a pay run's title, an invoice payment's beneficiary as the person entered
      it (`null` before one is added); `null` for every other kind.
unevaluatedProperties: false
```

<a id="supportcaserelatedobject"></a>
## SupportCaseRelatedObject

```yaml
type: object
description: The object a support request is about, as its owner sees it now.
required:
  - type
  - id
  - display
  - status
  - needs_attention
properties:
  type:
    type: string
    description: The object's resource. An open enum.
    enum:
      - order
      - payment_link
      - payout
      - payroll_run
    x-swaps-open-enum: true
  id:
    type: string
    description: Its prefixed id (`ord_`, `pl_`, `po_`, `pr_`).
  display:
    $ref: '#/components/schemas/SupportRelatedObjectDisplay'
  status:
    type:
      - string
      - 'null'
    description: >-
      The object's own status as its product resource publishes it (show it through that product's
      status vocabulary); `null` when it has none.
  needs_attention:
    type: boolean
    description: An invoice payment's own `needs_attention`; `false` for every other kind.
unevaluatedProperties: false
```

<a id="supportrelatedobject"></a>
## SupportRelatedObject

```yaml
type: object
description: >-
  One thing the person can write to Support about. A row with an `id` is accepted by `POST
  /v1/support_cases` as `related_object` under the row's `topic`; a verification row carries only
  its open request.
required:
  - type
  - id
  - topic
  - display
  - status
  - needs_attention
  - created_at
  - open_case
properties:
  type:
    type: string
    description: The object's resource, or one of the two verification rows. An open enum.
    enum:
      - order
      - payment_link
      - payout
      - payroll_run
      - verification
      - business_verification
    x-swaps-open-enum: true
  id:
    type:
      - string
      - 'null'
    description: The prefixed id; `null` for the two verification rows, whose object the server derives.
  topic:
    type: object
    description: The intake registry branch a request about this row is filed under.
    required:
      - product
      - intent
    properties:
      product:
        type: string
      intent:
        type: string
  display:
    $ref: '#/components/schemas/SupportRelatedObjectDisplay'
  status:
    type:
      - string
      - 'null'
    description: >-
      The object's own status as its product resource publishes it; always `null` on the two
      verification rows, which carry no verification state.
  needs_attention:
    type: boolean
    description: An invoice payment's own `needs_attention`; always `false` on the two verification rows.
  created_at:
    type:
      - string
      - 'null'
    format: date-time
    description: When the object was created; `null` for the two verification rows.
  open_case:
    description: >-
      The person's newest open request with this row's question, which a request about it would
      join; `null` when there is none. The date is the request's.
    oneOf:
      - type: object
        required:
          - id
          - status
          - status_group
          - created_at
        properties:
          id:
            type: string
            pattern: ^sc_
          status:
            type: string
            description: As `SupportCase.status`, never `closed`. An open enum.
            enum:
              - waiting_on_customer
              - with_support
            x-swaps-open-enum: true
          status_group:
            type: string
            enum:
              - needs_you
              - in_progress
          created_at:
            type: string
            format: date-time
      - type: 'null'
unevaluatedProperties: false
```

<a id="supportrelatedobjectlist"></a>
## SupportRelatedObjectList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/SupportRelatedObject'
unevaluatedProperties: false
```

<a id="wallet"></a>
## Wallet

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    description: >
      The holder's non-custodial Tempo wallet (RESOURCE-MODEL §2.3). Nothing here can sign or move
      funds — the passkey on the holder's own device is the only signer (RESOURCE-MODEL §0.10).
    required:
      - address
      - chain
      - network
      - status
    properties:
      id:
        type: string
        pattern: ^wlt_
        description: Prefixed id (`wlt_`). See RESOURCE-MODEL §0.2.
      object:
        type: string
        enum:
          - wallet
      address:
        type: string
        description: Checksum-cased Tempo address. Never lower-cased on read or write.
      chain:
        type: string
        description: The chain this wallet lives on. Today always Tempo.
      network:
        type: string
        x-swaps-open-enum: true
        enum:
          - tempo-mainnet
          - tempo-moderato
        description: >
          The Tempo network this wallet lives on, from the wallet record. `tempo-moderato` is
          Tempo's test network: what it holds is test money (pathUSD), never real funds. A project
          serves only wallets on its own network, so every wallet one environment returns carries
          the same value, and it is the same value `WalletBalances.network` reports. Use it to pick
          the token a balance, send or signature is about; never infer the network from the
          environment's name or a client build setting. `chain` stays `tempo`.
      status:
        type: string
        enum:
          - active
        description: RESOURCE-MODEL §2.3 names only one live state.
      label:
        type:
          - string
          - 'null'
      passkey:
        type: object
        description: >
          The wallet's passkey — the signer on the holder's own device (RESOURCE-MODEL §0.10).
          Nothing here can sign, recover or replace it.
        required:
          - name
        properties:
          name:
            type:
              - string
              - 'null'
            maxLength: 64
            description: >
              The name the holder's device shows for this passkey (in the password manager or the
              passkey prompt), as recorded when the passkey was registered — so the holder can find
              the right one. Display only. Returned to the passkey's owner only, reading their own
              wallet with their own session; `null` for a business key. `null` also when no name was
              recorded — every passkey registered before 2026-10-28 — and when it cannot be read
              right now. A replay of `POST /v1/wallet/wallets` with the same `Idempotency-Key`
              answers `null` too: the name is never kept in the replay store; read it with `GET`.
              Never a guessed or default name: a `null` name is not knowable here, not an empty one.
        additionalProperties: false
    additionalProperties: false
unevaluatedProperties: false
```

<a id="walletcreaterequest"></a>
## WalletCreateRequest

```yaml
type: object
description: >
  Registers or re-syncs the wallet the client already derived from a passkey credential — the
  WebAuthn ceremony itself (challenge + credential storage) has no `/v1` operation and runs
  client-side against the passkey key-manager directly (RESOURCE-MODEL §0.10: "Sign in with passkey"
  never touches `/v1`). The server independently re-derives the address from the stored credential's
  public key and rejects a mismatch (400) — `address` is a cross-check, never trusted alone.
required:
  - address
properties:
  address:
    type: string
    description: The Tempo address the client derived from its passkey credential. Case-preserved verbatim.
  credential_id:
    type: string
    description: >-
      The WebAuthn credential id already registered via the passkey key-manager, when more than one
      exists for this holder.
  label:
    type: string
additionalProperties: false
```

<a id="walletlist"></a>
## WalletList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/Wallet'
unevaluatedProperties: false
```

<a id="walletbalances"></a>
## WalletBalances

```yaml
type: object
description: >
  Scoped to the caller's own wallets only (RESOURCE-MODEL §2.3 D-9) — authenticating does not
  authorize reading another holder's address. `usd_total` excludes tokens whose decimals are not 6
  rather than inflating the figure; those are named in `skipped_non_usd` instead.
required:
  - chain_id
  - network
  - balances
  - usd_total
  - skipped_non_usd
properties:
  chain_id:
    type: string
    description: The chain this balance snapshot was read from. Today always Tempo.
  network:
    type: string
    x-swaps-open-enum: true
    enum:
      - tempo-mainnet
      - tempo-moderato
    description: >
      The Tempo network these balances were read on: the caller's wallet network (`Wallet.network`),
      or, when the caller has no wallet yet, the network this environment serves. `tempo-moderato`
      is Tempo's test network, so its balances (pathUSD) are test money. The primary token is USDC.e
      on `tempo-mainnet` and pathUSD on `tempo-moderato`; a client decides which balance can pay
      from this field, never from its own build.
  balances:
    type: array
    items:
      type: object
      required:
        - token
        - decimals
        - amount
        - symbol
      properties:
        token:
          type: string
          description: The token's contract or ledger identifier.
        decimals:
          type: integer
          minimum: 0
          maximum: 18
          description: The token's own decimal precision (USDC.e is 6).
        amount:
          $ref: '#/components/schemas/Money'
        symbol:
          type: string
          example: USDC.e
      additionalProperties: false
  usd_total:
    $ref: '#/components/schemas/Money'
  skipped_non_usd:
    type: array
    items:
      type: string
    description: >
      Token symbols excluded from `usd_total` because their decimals are not 6 — named so a client
      never reads the total as complete.
additionalProperties: false
```

<a id="wallettransactionlist"></a>
## WalletTransactionList

```yaml
type: object
description: >
  A bounded recent window of on-chain transfers for the caller's own wallet — never full history
  (RESOURCE-MODEL §2.3). `degraded: true` with an empty `transfers[]` means the chain read failed,
  and must never be read as "no activity" or a zero balance.
required:
  - chain_id
  - transfers
  - degraded
properties:
  chain_id:
    type: string
  transfers:
    type: array
    items:
      type: object
      description: One on-chain transfer row.
      required:
        - from
        - to
        - amount
        - symbol
        - direction
        - block_number
        - timestamp
        - origin
      properties:
        from:
          type: string
        to:
          type: string
        amount:
          $ref: '#/components/schemas/Money'
        symbol:
          type: string
        direction:
          type: string
          enum:
            - in
            - out
          description: Relative to the caller's own wallet.
        block_number:
          type: integer
        timestamp:
          type: string
          format: date-time
          description: >
            Required on every row (RESOURCE-MODEL v2 fixes the v1 draft's `timestamp?` — R13-wallet
            §2 W-G8 — so the hub's calendar-day spend chart can bucket every transfer).
        counterparty_label:
          type:
            - string
            - 'null'
          description: >
            The caller's own `address_book` label for the counterparty address of this row (`to` on
            an `out` transfer, `from` on an `in` one) — §52 C4-D14 / K5c, closing W1 PR #2960's open
            ruling #1. `null` when the caller has no saved entry for that address on a Tempo network
            (`address_book.chain` matching `TEMPO_NETWORKS` —
            `tempo`/`tempo-mainnet`/`tempo-moderato`, `_shared/services/tempo_settlement.ts`). Never
            a fabricated name.
        network:
          type:
            - string
            - 'null'
          description: >
            The network this transfer happened on (§52 C4-D14 / K5c). Always `"tempo"` today — the
            same public name `Wallet.chain` uses, never the internal
            `tempo-mainnet`/`tempo-moderato` split (WALLET-CANON §1: "the chain is never shown to
            the user").
        origin:
          description: >-
            Where this incoming transfer came from, when Swaps' own records name it (W-R34). A bank
            deposit is this same one row, never a second row. `null` on every other transfer, on
            every `out` transfer, and when the origin cannot be named for this caller or read right
            now — never a guess. Always present.
          oneOf:
            - $ref: '#/components/schemas/WalletTransactionOrigin'
            - type: 'null'
      additionalProperties: false
  degraded:
    type: boolean
  degraded_reason:
    type:
      - string
      - 'null'
additionalProperties: false
```

<a id="wallettransaction"></a>
## WalletTransaction

```yaml
type: object
description: One on-chain transaction, looked up by hash (`GET /v1/wallet/transactions/{hash}`).
required:
  - hash
  - chain_id
  - status
properties:
  hash:
    type: string
  chain_id:
    type: string
  status:
    type: string
    enum:
      - success
      - reverted
      - pending
      - not_found
    description: >
      `not_found` is a 200 body naming the lookup result, not itself an HTTP 404 — RESOURCE-MODEL
      §2.3 names it as a state of this resource.
  from:
    type: string
  to:
    type: string
  amount:
    $ref: '#/components/schemas/Money'
    description: >
      Present only on a transaction the wallet did not send whose decoded TIP-20 `Transfer` logs of
      the accepted tokens hold exactly one transfer touching the wallet; absent on a transaction the
      wallet sent.
  symbol:
    type: string
    description: The accepted Swaps-dollar token of `amount`; present exactly when `amount` is.
  direction:
    type: string
    enum:
      - in
      - out
  block_number:
    type: integer
  timestamp:
    type:
      - string
      - 'null'
    format: date-time
  counterparty_label:
    type:
      - string
      - 'null'
    description: >
      The caller's own `address_book` label for the counterparty address — same `address_book`
      lookup as `WalletTransactionList.transfers[].counterparty_label` (§52 C4-D14 / K5c). On a
      transaction the wallet did not send, the counterparty is the decoded TIP-20 Transfer log's
      sender (`in`) or recipient (`out`), as on the list, and `null` when several transfers touch
      the wallet. On a transaction the wallet sent it is the receipt-level `to`, which for every
      TIP-20 send is the TOKEN CONTRACT, not the recipient (`WalletSendModal.tsx`) — so that `out`
      lookup is `null` for essentially every real send (Codex review of #2988, P2: fail-closed,
      never a wrong guess). `null` when unsaved or when there is no counterparty address; absent
      only when `status: not_found`.
  network:
    type:
      - string
      - 'null'
    description: >
      The network this transaction happened on — same convention as
      `WalletTransactionList.transfers[].network` (§52 C4-D14 / K5c). Absent when `status:
      not_found` (no transaction to place on a network).
  origin:
    description: >-
      Same as `WalletTransactionList.transfers[].origin`, for a mined (`success`) `in` transaction;
      `null` on every other one. Absent only when `status: not_found`.
    oneOf:
      - $ref: '#/components/schemas/WalletTransactionOrigin'
      - type: 'null'
additionalProperties: false
```

<a id="wallettransactionorigin"></a>
## WalletTransactionOrigin

```yaml
type: object
description: >-
  The recorded origin of an incoming wallet transfer (W-R34). `kind: bank_deposit`: the transfer
  delivered a bank deposit made into one of the caller's own wallet-funding virtual accounts (`GET
  /v1/wallet/virtual_accounts`) — matched on the delivery transaction hash that account's provider
  events recorded, never inferred from the sender, the amount or the timing. Read only for a bearer
  caller who may read that account (owner or admin); `null` for a business key. A Payment links
  collection account is never a wallet transfer's origin.
required:
  - kind
  - virtual_account_id
  - currency
  - rail
properties:
  kind:
    type: string
    x-swaps-open-enum: true
    enum:
      - bank_deposit
  virtual_account_id:
    type: string
    description: The `VirtualAccount.id` (`va_…`) the deposit was made into. Never a provider id.
  currency:
    type: string
    description: >-
      The currency the bank deposit was made in, uppercase — the account's `destination.currency`
      (e.g. `USD`).
  rail:
    type: string
    x-swaps-open-enum: true
    enum:
      - ach
      - sepa
      - spei
      - faster_payments
      - pix
      - wire
      - bre_b
    description: >-
      The bank rail the account receives on — the same value and spelling as that account's
      `destination.rail` (`ach` for a USD account). Open enum: a client tolerates a value it does
      not know.
additionalProperties: false
examples:
  - kind: bank_deposit
    virtual_account_id: va_3f0c9a52-6d1e-4b8a-9c3e-2a7b5d1e8f40
    currency: USD
    rail: ach
```

<a id="walletroutestatus"></a>
## WalletRouteStatus

```yaml
type: string
description: >
  The 4-value funding-route status, exact match confirmed by R13-wallet §3 — it backs both
  `deposit_routes` and `send_routes`. A cross-network route is `unsupported` on a deployment whose
  wallets live on a Tempo test network; one that would otherwise be open is
  `temporarily_unavailable` while the server cannot confirm which Tempo network it serves.
enum:
  - available
  - coming_soon
  - unsupported
  - temporarily_unavailable
```

<a id="depositroute"></a>
## DepositRoute

```yaml
type: object
description: A candidate cross-network path onto the holder's Tempo wallet (RESOURCE-MODEL §2.3).
required:
  - id
  - source_chain
  - source_asset
  - target_network
  - target_asset
  - status
  - enabled
properties:
  id:
    type: string
  source_chain:
    type: string
  source_asset:
    type: string
  target_network:
    type: string
  target_asset:
    type: string
  status:
    $ref: '#/components/schemas/WalletRouteStatus'
  enabled:
    type: boolean
    description: >
      `true` only when the server opens this route now; never on a Tempo test network, whatever the
      route's proof says.
  proof_status:
    type: string
  estimated_time:
    type: string
    description: A human-readable ETA hint for this route.
  fee_hint:
    type: string
    description: A human-readable fee hint for this route — not a computed quote.
  message:
    type:
      - string
      - 'null'
    description: >
      A short server-computed line about this route's `status`; on a Tempo test network every route
      is `unsupported` and this names that network as the reason.
additionalProperties: false
```

<a id="depositroutelist"></a>
## DepositRouteList

```yaml
type: object
description: >
  A1-6: a plain `{data}` catalogue, not a cursor list — the operation declares no `limit`/`cursor`
  (the full candidate-route catalogue is always returned) and previously composed `ListMeta`,
  publishing `has_more`/ `next_cursor` the handler always hardcoded `false`/`null` with no way to
  ever page. Dropped rather than wired up: this catalogue is small and bounded by the source-chain
  set, not account data.
required:
  - data
properties:
  data:
    type: array
    items:
      $ref: '#/components/schemas/DepositRoute'
additionalProperties: false
```

<a id="depositquoterequest"></a>
## DepositQuoteRequest

```yaml
type: object
description: >
  Nothing is written on Swaps' side and no `deposit_intent` is created — but Relay itself still
  allocates a fresh, funds-accepting deposit address per call (the response never returns it), so
  this is NOT provider-side side-effect-free: `Idempotency-Key` is required for the same reason it
  is on `deposit_intents.create`.
required:
  - source_chain
  - source_asset
  - amount
properties:
  source_chain:
    type: string
  source_asset:
    type: string
  amount:
    $ref: '#/components/schemas/MoneyPositive'
additionalProperties: false
```

<a id="depositquote"></a>
## DepositQuote

```yaml
type: object
description: >
  Dark-flag resource (R13-wallet §2 W-G1) — a live "you send X, receive ~Y" preview for one
  candidate network, ahead of minting a real deposit intent.
required:
  - amount_out
  - haircut_pct
properties:
  amount_out:
    $ref: '#/components/schemas/Money'
  haircut_pct:
    type: string
    description: >
      Decimal string, percent (e.g. `"0.15"` means 0.15%), never a float — mirrors
      `developer_fee_percent`'s representation elsewhere in this file.
additionalProperties: false
```

<a id="depositintentstatus"></a>
## DepositIntentStatus

```yaml
type: string
description: >
  9 values (7 base + 2 recovery), confirmed exact by R13-wallet §2 W-G11 against the DB CHECK
  constraint — RESOURCE-MODEL §2.3's v1 draft omitted `cancelled`.
enum:
  - awaiting_deposit
  - source_seen
  - bridging
  - settled
  - expired
  - failed
  - cancelled
  - recovering
  - manual_recovery_required
```

<a id="depositintent"></a>
## DepositIntent

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    description: >
      A one-time, amount-bound, cross-network deposit address (RESOURCE-MODEL §2.3).
      `Idempotency-Key` is mandatory on create — a repeat POST would otherwise mint a second address
      and a second provider quote (D-2).
    required:
      - status
      - source_chain
      - source_asset
      - target_chain
      - target_asset
      - deposit_instructions
      - expires_at
      - timeline
    properties:
      id:
        type: string
        pattern: ^din_
        description: Prefixed id (`din_`). See RESOURCE-MODEL §0.2.
      object:
        type: string
        enum:
          - deposit_intent
      status:
        $ref: '#/components/schemas/DepositIntentStatus'
      source_chain:
        type: string
      source_asset:
        type: string
      target_chain:
        type: string
      target_asset:
        type: string
      deposit_instructions:
        $ref: '#/components/schemas/DepositInstructions'
        description: >-
          Single-use, amount-bound — minted once per intent (RESOURCE-MODEL §2.3 D-2).
          Case-preserved verbatim.
      source_tx_hash:
        type:
          - string
          - 'null'
      target_tx_hash:
        type:
          - string
          - 'null'
      fail_reason:
        type:
          - string
          - 'null'
        description: >
          Set ONLY when the intent reached `failed` — §52 C4-D14 / K5c. Mirrors the already-recorded
          `wallet_deposit_intents.fail_reason` column (added 20260625150000, populated by the
          wallet-funding watcher). An OPAQUE, provider-defined (Relay) string: the raw `failReason`
          Relay attached to the request (e.g. `SLIPPAGE` on a tiny-deposit auto-refund) when it sent
          one, otherwise the Relay status word itself (`refund`/`refunded`/`failure`/`failed`) as a
          fallback so this is never blank. The watcher maps ALL FOUR of those Relay states to the
          SAME `failed` intent status — this field does NOT encode whether Relay auto-refunded the
          sender or hard-failed the request, and no vocabulary is guaranteed stable; do not
          pattern-match a specific string to infer "refunded" (Codex review of #2988, P2). `null` in
          every other state, including `expired`, `cancelled` and `manual_recovery_required` — the
          watcher only ever writes it while setting `status: 'failed'`.
      expires_at:
        type: string
        format: date-time
      timeline:
        type: array
        description: Ordered transition history for this intent.
        items:
          type: object
          required:
            - state
            - occurred_at
          properties:
            state:
              type: string
            occurred_at:
              type: string
              format: date-time
          additionalProperties: false
    additionalProperties: false
unevaluatedProperties: false
```

<a id="depositintentcreaterequest"></a>
## DepositIntentCreateRequest

```yaml
type: object
required:
  - source_chain
  - source_asset
  - amount
properties:
  source_chain:
    type: string
  source_asset:
    type: string
  amount:
    $ref: '#/components/schemas/MoneyPositive'
additionalProperties: false
```

<a id="depositintentlist"></a>
## DepositIntentList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/DepositIntent'
unevaluatedProperties: false
```

<a id="sendroute"></a>
## SendRoute

```yaml
type: object
description: A candidate cross-network path off the holder's Tempo wallet (RESOURCE-MODEL §2.3).
required:
  - id
  - kind
  - source_chain
  - source_asset
  - source_token_address
  - destination_chain
  - destination_asset
  - status
  - enabled
  - intent_required
  - message
properties:
  id:
    type: string
  kind:
    type: string
  source_chain:
    type: string
  source_asset:
    type: string
  source_token_address:
    type: string
    description: >
      The source-leg TIP-20 contract address on Tempo. A `direct_tempo` send signs a
      `transferWithMemo(address,uint256,bytes32)` call against this address client-side (a plain
      send uses the 32-byte zero memo), since that route never touches `/v1` at all; a `cross_chain`
      send never needs it directly, but it is exposed uniformly rather than splitting the schema by
      `kind`. This is always the live mainnet contract address, independent of any test-mode
      credential used to call this operation — route discovery does not vary by livemode. Returned
      exactly as stored, never lower-cased by the server — though for an EVM contract address this
      is a cosmetic guarantee, not a money-safety one: unlike BTC/Tron/Solana addresses, an EVM
      address is case-insensitive at the protocol level (EIP-55 checksum casing is a display
      convention, not part of the address).
  destination_chain:
    type: string
  destination_asset:
    type: string
  status:
    $ref: '#/components/schemas/WalletRouteStatus'
  enabled:
    type: boolean
    description: >
      `true` only when the server opens this route for the caller now; a `cross_chain` route is
      never enabled on a Tempo test network.
  intent_required:
    type: boolean
    description: >
      `false` for a direct same-chain Tempo send, which never touches `/v1` at all (RESOURCE-MODEL
      §0.10, R13-wallet §0) — `true` for every cross-network send, which must go through
      `send_intents`.
  estimated_time:
    type:
      - string
      - 'null'
    description: >
      A human-readable ETA hint for this route. `null` for every route in the current catalogue — no
      entry in `wallet-send/handler.ts`'s static route table populates it yet, and no approved
      design surface reads it today (`docs/api/consumers/R13-wallet.md` §4 sources every send-flow
      ETA elsewhere: a client-side chain-gas estimate for `direct_tempo`, Relay `/quote/v2` for
      `cross_chain`). Published nullable, not omitted, and always present, unlike
      `DepositRoute.estimated_time` (a non-nullable string the deposit projection omits when unset)
      — reserved for a future route (e.g. a provider-quoted cross-chain ETA) that can set it.
  fee_hint:
    type:
      - string
      - 'null'
    description: >
      A human-readable fee hint for this route — not a computed quote. `null` for every route today,
      for the same reason as `estimated_time` above; not currently read by any approved surface.
      Published nullable and always present, unlike `DepositRoute.fee_hint` (non-nullable, omitted
      when unset).
  message:
    type: string
    description: >
      A short per-route status string the server always computes — e.g. why a `coming_soon`
      cross-chain route isn't live yet, or that a route is non-custodial. Published for parity with
      `DepositRoute.message` (same shape and purpose) and populated on every route without
      exception, so it is required and never `null` here (`DepositRoute.message` stays
      nullable+optional for its own producer, which can omit it). No approved design surface reads
      this field today — `docs/api/consumers/R13-wallet.md` §1 Group S and the approved
      `wallet-send.html` design both render disabled cross-network rows from a fixed client-side
      sub-label plus a `status`/`enabled`-driven "Behind flag" tag, not this string — and the text
      itself is server-generated English, not routed through the dashboard's i18n shards, so a
      screen must not render it verbatim in a localized surface. Exposed as a diagnostic/parity
      field now so a future screen (or a non-dashboard API caller) has it without a second contract
      change. On a Tempo test network a `cross_chain` route is `unsupported` and this names that
      network as the reason.
additionalProperties: false
```

<a id="sendroutelist"></a>
## SendRouteList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/SendRoute'
unevaluatedProperties: false
```

<a id="sendintentstatus"></a>
## SendIntentStatus

```yaml
type: string
description: >
  8 values, confirmed exact by R13-wallet §2 W-G11 — RESOURCE-MODEL §2.3's v1 draft omitted
  `cancelled`.

  `awaiting_signature`: a payout-funding send `payouts.fund` handed to one session, not yet
  recorded.
enum:
  - created
  - awaiting_signature
  - source_submitted
  - bridging
  - settled
  - expired
  - failed
  - cancelled
```

<a id="sendintent"></a>
## SendIntent

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    description: >
      A cross-network send off the holder's Tempo wallet. The API never signs (RESOURCE-MODEL
      §0.10): `send_instructions.steps[]` are unsigned calls the holder's own passkey signs; a
      direct same-chain Tempo send never creates one of these at all. Sign only a send that
      `send_intents.create` or `payouts.fund` handed to your session: reading one (`get`, `list`, a
      payout's funding instructions) never hands it out, and a payout-funding send's `source_tx.set`
      is accepted only from the session it was handed to. When that record never lands, the server
      records the source transaction itself from the routing provider's request, once the chain
      shows the holder's own exact transfer.
    required:
      - status
      - amount
      - destination_chain
      - destination_asset
      - destination_address
      - send_instructions
      - expires_at
      - timeline
    properties:
      id:
        type: string
        pattern: ^sin_
        description: Prefixed id (`sin_`). See RESOURCE-MODEL §0.2.
      object:
        type: string
        enum:
          - send_intent
      status:
        $ref: '#/components/schemas/SendIntentStatus'
      amount:
        $ref: '#/components/schemas/Money'
      amount_out:
        oneOf:
          - $ref: '#/components/schemas/Money'
          - type: 'null'
        description: >
          The quoted "recipient gets" amount. Null until the routing provider has quoted it — a
          client renders "exact amount shown next" while it is null (R13-wallet §2 W-G2).
      destination_chain:
        type: string
      destination_asset:
        type: string
      destination_address:
        type: string
      send_instructions:
        type: object
        description: Unsigned. The API cannot sign; the holder's passkey signs every step.
        required:
          - steps
          - request_id
        properties:
          steps:
            type: array
            items:
              $ref: '#/components/schemas/UnsignedStep'
          request_id:
            type: string
        additionalProperties: false
      source_tx_hash:
        type:
          - string
          - 'null'
        description: >
          The source leg's transaction hash, verbatim. Recorded by `source_tx.set`, or by the server
          for a payout-funding send whose client record never landed; a later `source_tx.set` with
          the same hash is a no-op and a different hash is refused.
      expires_at:
        type: string
        format: date-time
      timeline:
        type: array
        items:
          type: object
          required:
            - state
            - occurred_at
          properties:
            state:
              type: string
            occurred_at:
              type: string
              format: date-time
          additionalProperties: false
    additionalProperties: false
unevaluatedProperties: false
```

<a id="sendintentcreaterequest"></a>
## SendIntentCreateRequest

```yaml
type: object
required:
  - amount
  - destination_chain
  - destination_asset
  - destination_address
properties:
  amount:
    $ref: '#/components/schemas/MoneyPositive'
  destination_chain:
    type: string
  destination_asset:
    type: string
  destination_address:
    type: string
additionalProperties: false
```

<a id="sendintentsourcetxrequest"></a>
## SendIntentSourceTxRequest

```yaml
type: object
description: >
  Records the transaction hash the holder's wallet just broadcast for the intent's source leg. Call
  once, immediately after signing; resending the same hash is a safe no-op, and a different hash for
  the same intent is refused rather than accepted as a correction.
required:
  - source_tx_hash
properties:
  source_tx_hash:
    type: string
additionalProperties: false
```

<a id="sendintentlist"></a>
## SendIntentList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/SendIntent'
unevaluatedProperties: false
```

<a id="unsignedcall"></a>
## UnsignedCall

```yaml
type: object
description: >
  A raw, unsigned chain call. The API never signs (RESOURCE-MODEL §0.10) — the holder's own passkey
  is the only signer.
required:
  - to
  - data
  - value
properties:
  to:
    type: string
    description: The call's target address.
  data:
    type: string
    description: Hex-encoded calldata.
  value:
    type: string
    pattern: ^[0-9]+$
    description: Minor-unit integer as a string, mirroring `Money.amount`'s representation.
additionalProperties: false
```

<a id="unsignedstep"></a>
## UnsignedStep

```yaml
type: object
description: >
  One step in an ordered, unsigned ceremony. Conversions name `approve` and `swap` (R13-wallet §2
  W-G9); a resource that needs a different vocabulary documents its own `kind` values at the field
  that carries it — this shape is shared, the vocabulary is not.
required:
  - kind
  - call
properties:
  kind:
    type: string
  call:
    $ref: '#/components/schemas/UnsignedCall'
additionalProperties: false
```

<a id="offrampquoterequest"></a>
## OfframpQuoteRequest

```yaml
type: object
description: Compute-only — nothing is persisted.
required:
  - destination_currency
  - destination_payment_rail
  - amount
properties:
  destination_currency:
    type: string
  destination_payment_rail:
    type: string
  amount:
    $ref: '#/components/schemas/MoneyPositive'
additionalProperties: false
```

<a id="offrampquote"></a>
## OfframpQuote

```yaml
type: object
required:
  - destination_currency
  - destination_payment_rail
  - amount
  - expected_source
properties:
  destination_currency:
    type: string
  destination_payment_rail:
    type: string
  amount:
    $ref: '#/components/schemas/Money'
  expected_source:
    $ref: '#/components/schemas/Money'
    description: >
      The estimated amount and asset the holder's wallet will send. Indicative, and quantised to 2
      decimals — `decimals` is `2` here even though USDC itself is a 6-decimal asset, because the
      quote engine rounds this figure to cents before it is published; a finer scale would state
      precision that was never quoted. The binding figure is re-priced at `offramp_intents.create`.
    example:
      amount: '10812'
      currency: usdc
      decimals: 2
additionalProperties: false
```

<a id="externalaccount"></a>
## ExternalAccount

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    description: >
      A masked bank-account destination the holder attested is their own (RESOURCE-MODEL §2.3) — an
      agent may never add one on a user's behalf.
    required:
      - currency
      - rail
      - bank_name
      - last4
    properties:
      id:
        type: string
        pattern: ^ba_
        description: Prefixed id (`ba_`). See RESOURCE-MODEL §0.2.
      object:
        type: string
        enum:
          - external_account
      currency:
        type: string
      rail:
        type: string
        x-swaps-open-enum: true
        enum:
          - ach
          - wire
          - sepa
          - spei
          - faster_payments
          - pix
        description: >
          The rail the account is paid on. `faster_payments` (GBP) and `pix` (BRL) accounts are
          payout destinations for a sell (`POST /v1/orders`, `external_account_id`); the wallet's
          own bank withdrawal (`offramp_intents`) pays only `ach`, `wire`, `sepa` and `spei`. This
          enum may gain members within `/v1`; treat an unknown value as opaque and do not fail
          deserialization.
      bank_name:
        type: string
      last4:
        type: string
        minLength: 1
        description: >
          The last 4 characters of the account/IBAN/CLABE identifier — bare characters, never a
          masking marker such as `••`; a client formats its own display string. Matches `payroll`'s
          identically-named `destination_snapshot.last4`. For `faster_payments`, the account
          number's last 4. For `pix`, the provider shows only a masked preview of the key: this is
          the preview's unmasked tail (at most 4 characters) when the preview ends unmasked,
          otherwise the last 4 digits of the CPF or CNPJ the key belongs to; a `pix` account with
          neither is not listed.
    additionalProperties: false
unevaluatedProperties: false
```

<a id="externalaccountaddress"></a>
## ExternalAccountAddress

```yaml
type: object
description: >
  The account holder's own address, as the provider takes it for the account's beneficiary. Needed
  on `ach` and `wire` — a US account — unless the holder's verification record carries a complete
  one; optional on `sepa`, `faster_payments`, `pix` and `spei`, where the provider uses the verified
  customer's own. Without it, a create the provider cannot complete for want of an address is `422
  invalid_request` with code `address_required` and `param: address`; an address the provider
  refuses is `422 address_invalid`, `param` naming the field when it named one. A malformed field is
  `400 invalid_request` naming it (`param: address.city`, …) before any provider call. The holder's
  name is never part of it. Not stored by Swaps beyond the provider's own record; never echoed in an
  error.
required:
  - street_line_1
  - city
  - postal_code
  - country
properties:
  street_line_1:
    type: string
    minLength: 4
    maxLength: 35
    description: Street and number, 4 to 35 characters (the provider's limits).
    example: Unter den Linden 77
  street_line_2:
    type: string
    maxLength: 35
    description: Optional second line (apartment, floor), at most 35 characters.
  city:
    type: string
    minLength: 1
    maxLength: 64
    example: Berlin
  postal_code:
    type: string
    minLength: 1
    maxLength: 16
    example: '10117'
  state:
    type: string
    minLength: 1
    maxLength: 6
    description: >
      The ISO 3166-2 subdivision code without the country prefix (`NY`, not `US-NY`; a prefix is
      removed). Required when `country` is the United States: a two-letter state, DC or territory
      code. Optional elsewhere.
    example: NY
  country:
    type: string
    pattern: ^[A-Za-z]{2,3}$
    description: >
      Any ISO 3166-1 country whose residents the provider onboards, alpha-3 (`DEU`, `USA`) as the
      provider takes it; an alpha-2 code (`DE`, `US`) is accepted and converted. An unknown code, or
      a country in the provider's Prohibited tier (which holds the sanctioned list), is `400
      invalid_request`, `param: address.country`.
    example: DEU
additionalProperties: false
```

<a id="externalaccountcreaterequest"></a>
## ExternalAccountCreateRequest

```yaml
description: >
  Full bank details plus the holder's own-account attestation (RESOURCE-MODEL §2.3), discriminated
  by `rail` — exactly one fully-specified rail shape (R13-wallet §1: account+routing+account_type,
  IBAN+BIC, CLABE, sort code+account number, or a Pix key). REST only, never an MCP tool argument —
  most clients log tool arguments verbatim. The account holder's name is never sent: it is the
  verified legal name on the holder's verification record. The holder's own `address` is optional on
  every rail and needed on `ach` and `wire` (see `ExternalAccountAddress`).
oneOf:
  - $ref: '#/components/schemas/ExternalAccountCreateRequestAch'
  - $ref: '#/components/schemas/ExternalAccountCreateRequestWire'
  - $ref: '#/components/schemas/ExternalAccountCreateRequestSepa'
  - $ref: '#/components/schemas/ExternalAccountCreateRequestSpei'
  - $ref: '#/components/schemas/ExternalAccountCreateRequestFasterPayments'
  - $ref: '#/components/schemas/ExternalAccountCreateRequestPix'
discriminator:
  propertyName: rail
  mapping:
    ach: '#/components/schemas/ExternalAccountCreateRequestAch'
    wire: '#/components/schemas/ExternalAccountCreateRequestWire'
    sepa: '#/components/schemas/ExternalAccountCreateRequestSepa'
    spei: '#/components/schemas/ExternalAccountCreateRequestSpei'
    faster_payments: '#/components/schemas/ExternalAccountCreateRequestFasterPayments'
    pix: '#/components/schemas/ExternalAccountCreateRequestPix'
```

<a id="externalaccountcreaterequestach"></a>
## ExternalAccountCreateRequestAch

```yaml
type: object
description: '`rail: ach` — US domestic ACH transfer, account plus routing number.'
required:
  - attest_own_account
  - currency
  - rail
  - account_number
  - routing_number
  - account_type
properties:
  attest_own_account:
    type: boolean
    enum:
      - true
    description: Must be `true`. Re-verified against the provider again at spend time.
  currency:
    type: string
    enum:
      - usd
  rail:
    type: string
    enum:
      - ach
  bank_name:
    type: string
  account_number:
    type: string
  routing_number:
    type: string
  account_type:
    type: string
    enum:
      - checking
      - savings
  address:
    $ref: '#/components/schemas/ExternalAccountAddress'
additionalProperties: false
```

<a id="externalaccountcreaterequestwire"></a>
## ExternalAccountCreateRequestWire

```yaml
type: object
description: '`rail: wire` — domestic or international wire, account plus routing number.'
required:
  - attest_own_account
  - currency
  - rail
  - account_number
  - routing_number
  - account_type
properties:
  attest_own_account:
    type: boolean
    enum:
      - true
    description: Must be `true`. Re-verified against the provider again at spend time.
  currency:
    type: string
    enum:
      - usd
  rail:
    type: string
    enum:
      - wire
  bank_name:
    type: string
  account_number:
    type: string
  routing_number:
    type: string
  account_type:
    type: string
    enum:
      - checking
      - savings
  address:
    $ref: '#/components/schemas/ExternalAccountAddress'
additionalProperties: false
```

<a id="externalaccountcreaterequestsepa"></a>
## ExternalAccountCreateRequestSepa

```yaml
type: object
description: '`rail: sepa` — EUR SEPA transfer, IBAN plus BIC.'
required:
  - attest_own_account
  - currency
  - rail
  - iban
  - bic
properties:
  attest_own_account:
    type: boolean
    enum:
      - true
    description: Must be `true`. Re-verified against the provider again at spend time.
  currency:
    type: string
    enum:
      - eur
  rail:
    type: string
    enum:
      - sepa
  bank_name:
    type: string
  iban:
    type: string
  bic:
    type: string
  address:
    $ref: '#/components/schemas/ExternalAccountAddress'
additionalProperties: false
```

<a id="externalaccountcreaterequestspei"></a>
## ExternalAccountCreateRequestSpei

```yaml
type: object
description: '`rail: spei` — Mexican SPEI transfer, CLABE.'
required:
  - attest_own_account
  - currency
  - rail
  - clabe
properties:
  attest_own_account:
    type: boolean
    enum:
      - true
    description: Must be `true`. Re-verified against the provider again at spend time.
  currency:
    type: string
    enum:
      - mxn
  rail:
    type: string
    enum:
      - spei
  bank_name:
    type: string
  clabe:
    type: string
  address:
    $ref: '#/components/schemas/ExternalAccountAddress'
additionalProperties: false
```

<a id="externalaccountcreaterequestfasterpayments"></a>
## ExternalAccountCreateRequestFasterPayments

```yaml
type: object
description: '`rail: faster_payments` — UK Faster Payments (GBP), sort code plus account number.'
required:
  - attest_own_account
  - currency
  - rail
  - sort_code
  - account_number
properties:
  attest_own_account:
    type: boolean
    enum:
      - true
    description: Must be `true`. Re-verified against the provider again at spend time.
  currency:
    type: string
    enum:
      - gbp
  rail:
    type: string
    enum:
      - faster_payments
  bank_name:
    type: string
  sort_code:
    type: string
    pattern: ^[0-9]{6}$
    description: The six-digit sort code, digits only — no hyphens or spaces.
    example: '040004'
  account_number:
    type: string
    pattern: ^[0-9]{8}$
    description: The eight-digit UK account number.
    example: '12345678'
  address:
    $ref: '#/components/schemas/ExternalAccountAddress'
additionalProperties: false
```

<a id="externalaccountcreaterequestpix"></a>
## ExternalAccountCreateRequestPix

```yaml
type: object
description: '`rail: pix` — Brazilian Pix (BRL), a Pix key.'
required:
  - attest_own_account
  - currency
  - rail
  - pix_key
properties:
  attest_own_account:
    type: boolean
    enum:
      - true
    description: Must be `true`. Re-verified against the provider again at spend time.
  currency:
    type: string
    enum:
      - brl
  rail:
    type: string
    enum:
      - pix
  bank_name:
    type: string
  pix_key:
    type: string
    minLength: 1
    maxLength: 77
    description: >
      The Pix key the payout is sent to, in one of five forms: a CPF (11 digits) or a CNPJ (14
      digits) — punctuation such as `.`, `-` and `/` is removed and the check digits are verified —,
      a phone number written as `+55` followed by 11 digits (spaces, hyphens and parentheses are
      removed), an email address, or a random key (EVP, a UUID). Anything else is `400
      invalid_request` with `param: pix_key`. 11 bare digits are read as a CPF, never as a phone
      number.
  pix_key_type:
    type: string
    enum:
      - cpf
      - cnpj
      - email
      - phone
      - evp
    description: >
      Optional. Which of the five forms `pix_key` is. When given, the key must be of that form (`400
      invalid_request`, `param: pix_key`, otherwise); when absent the form is read from the key
      itself. Used only to check the key — it is not stored and not sent to the provider.
  address:
    $ref: '#/components/schemas/ExternalAccountAddress'
additionalProperties: false
```

<a id="externalaccountlist"></a>
## ExternalAccountList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/ExternalAccount'
unevaluatedProperties: false
```

<a id="offrampintentstatus"></a>
## OfframpIntentStatus

```yaml
type: string
description: >
  11 values, American spelling `canceled`, exact match confirmed by R13-wallet §3 against
  RESOURCE-MODEL §2.3.
enum:
  - quoted
  - awaiting_funds
  - in_review
  - funds_received
  - payment_submitted
  - payment_processed
  - refund_in_flight
  - refunded
  - refund_failed
  - canceled
  - failed
```

<a id="offrampintent"></a>
## OfframpIntent

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    description: >
      A withdrawal from the holder's wallet to one of their own external bank accounts
      (RESOURCE-MODEL §2.3). The transit deposit into `deposit_instructions` is a direct same-chain
      Tempo call the holder's own passkey signs — never a `/v1` write.
    required:
      - status
      - external_account_id
      - destination_currency
      - destination_payment_rail
      - amount
      - developer_fee_percent
      - deposit_instructions
    properties:
      id:
        type: string
        pattern: ^ofr_
        description: >
          Prefixed id (`ofr_`). See RESOURCE-MODEL §0.2 — R13-wallet §2 W-G13 flags a design fixture
          using `wof_`, which is wrong and must not ship.
      object:
        type: string
        enum:
          - offramp_intent
      status:
        $ref: '#/components/schemas/OfframpIntentStatus'
      external_account_id:
        type: string
        pattern: ^ba_
      destination_currency:
        type: string
      destination_payment_rail:
        type: string
      amount:
        $ref: '#/components/schemas/Money'
      developer_fee_percent:
        type: string
        example: '1.0'
      deposit_instructions:
        $ref: '#/components/schemas/DepositInstructions'
        description: >
          The transit deposit's address, amount and on-chain memo (R13-wallet §2 W-G12) — `chain` is
          always `tempo` and the memo, when the rail needs one, is carried on `memo`. This is a
          direct same-chain Tempo call the holder's own passkey signs; it is never a `/v1` write.
      settled_amount:
        $ref: '#/components/schemas/Money'
        description: >
          The provider-observed amount actually settled to the bank account, in
          `destination_currency`. Set only once the intent reaches `payment_processed` — this is
          Bridge's flexible-amount off-ramp, so rate movement between quote and deposit (or a
          partial/rounded deposit) can make this genuinely differ from `amount`, the originally
          requested figure. Absent (never a fabricated copy of `amount`) before settlement, and on
          an intent whose provider payload never carried a settlement receipt.
    additionalProperties: false
unevaluatedProperties: false
```

<a id="offrampintentcreaterequest"></a>
## OfframpIntentCreateRequest

```yaml
type: object
required:
  - external_account_id
  - destination_currency
  - destination_payment_rail
  - amount
properties:
  external_account_id:
    type: string
    pattern: ^ba_
  destination_currency:
    type: string
  destination_payment_rail:
    type: string
  amount:
    $ref: '#/components/schemas/MoneyPositive'
additionalProperties: false
```

<a id="offrampintentlist"></a>
## OfframpIntentList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/OfframpIntent'
unevaluatedProperties: false
```

<a id="virtualaccount"></a>
## VirtualAccount

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    description: >
      A billable, provider-issued bank-deposit destination (RESOURCE-MODEL §2.3) — never
      auto-created. `purpose` says which product owns it; `settles_to` alone says where deposits
      land, and any account whose `settles_to.kind` is `wallet` (a `collection` account included)
      credits this wallet. A wallet's bank-details screen still lists `purpose=wallet_funding` only.
      The "in your name" rail guard (`docs/wallet/WALLET-CANON.md` §4) is compliance-load-bearing:
      USD and MXN details are issued in the holder's own name, EUR SEPA IBANs are held by the
      provider's EEA entity and are not — `destination.holder_kind` makes that machine-checkable
      (R13-wallet §2 W-G4).
    required:
      - status
      - purpose
      - settles_to
      - destination
      - source_deposit_instructions
      - developer_fee_percent
    properties:
      id:
        type: string
        pattern: ^va_
        description: Prefixed id (`va_`). See RESOURCE-MODEL §0.2.
      object:
        type: string
        enum:
          - virtual_account
      status:
        type: string
        enum:
          - activated
          - deactivated
      purpose:
        type: string
        x-swaps-open-enum: true
        enum:
          - wallet_funding
          - collection
          - legacy_wallet
        example: wallet_funding
        description: >-
          Which product owns this account. `wallet_funding`: the wallet's own "receive by bank"
          account. `collection`: a pay-in account for Payment links and Buy orders, one per
          customer, source currency and settlement destination — it belongs to Payment links, not to
          the wallet screen. `legacy_wallet` (W-R25): a wallet-purpose account whose stored
          destination is not this holder's Tempo wallet — an account of the wallet's previous
          settlement destination (e.g. Solana, Ethereum) from before the Tempo cutover, shown
          honestly rather than folded into `collection`. Read-only through `/v1`: the wallet API
          manages only accounts that settle to this holder's Tempo wallet; while `status` is
          `activated`, deposits still settle to `settles_to`. `virtual_accounts.deactivate` /
          `.reactivate` refuse it with `409 conflict` (`legacy_wallet_read_only`). Filter with `GET
          /v1/wallet/virtual_accounts?purpose=wallet_funding`.
      settles_to:
        description: >-
          Where deposits to this account are credited, read from the account's stored settlement
          destination. `null` when that destination is unknown — never guessed.
        oneOf:
          - $ref: '#/components/schemas/VirtualAccountSettlesTo'
          - type: 'null'
      destination:
        type: object
        required:
          - currency
          - rail
          - payment_rails
          - holder_name
          - holder_kind
          - bank_name
          - masked
          - details
        properties:
          currency:
            type: string
          rail:
            type: string
            x-swaps-open-enum: true
            enum:
              - ach
              - sepa
              - spei
              - faster_payments
              - pix
              - wire
              - bre_b
            description: >-
              The bank rail this account receives on, one spelling per rail: the provider's own
              `ach_push` is published as `ach` (the spelling `ExternalAccount.rail` uses); every
              other value, `wire` included, is published as stored.
              `source_deposit_instructions.rail` carries the same value. Open enum: a client
              tolerates a value it does not know — a stored rail outside this list is published as
              stored, never mapped onto one of these.
          payment_rails:
            type:
              - array
              - 'null'
            items:
              type: string
              x-swaps-open-enum: true
              enum:
                - ach
                - sepa
                - spei
                - faster_payments
                - pix
                - wire
                - bre_b
                - fednow
            description: >-
              Every bank rail this account accepts, as the provider listed them when it issued the
              account (a USD account can list `ach`, `fednow` and `wire`). Each value is spelled
              like `rail` (the provider's `ach_push` is `ach`), in the stored order, without
              duplicates. `rail` is unchanged and still names one rail. `null` when Swaps holds no
              list for the account (never guessed from the currency): read `null` as unknown, not as
              `rail` alone. Open enum: a client tolerates a value it does not know.
          holder_name:
            type: string
          holder_kind:
            type: string
            enum:
              - own_name
              - provider_name
              - developer_name
            description: >-
              Per record, read from the provider's beneficiary name — never derived from the
              currency. `own_name`: the holder's own name (USD, MXN and, since 2026-09-02, EUR).
              `provider_name`: the cached beneficiary is still the provider's entity (legacy EUR
              accounts until Bridge's propagation completes; deposits addressed to it are accepted
              until about 2026-10-02). `developer_name`: memo-based transfer onramp accounts, which
              carry the developer's name ("Swaps"). GBP `faster_payments` transfer on-ramps are the
              exception among memo-based accounts: Bridge returns no holder name, its risk engine
              returns any deposit whose beneficiary is not the customer's registered name, so Swaps
              supplies that name and it reads `own_name`.

              '
          bank_name:
            type: string
          masked:
            type: object
            description: >
              Masked display fields for this rail (e.g. IBAN tail, account tail, routing/BIC) —
              shown, never enough on its own to originate a transfer.
            additionalProperties:
              type: string
          details:
            type: object
            description: The full named field set for this rail (e.g. IBAN, BIC, account, routing).
            additionalProperties:
              type: string
        additionalProperties: false
      source_deposit_instructions:
        $ref: '#/components/schemas/BankDepositInstructions'
        description: >-
          Provider-issued instructions for funding this VA from an external source. Masked on read
          where the canon requires it.
      developer_fee_percent:
        type: string
        example: '0.0'
        description: >-
          Zero on receive-by-bank today — the holder-identity distinction is the load-bearing fact,
          not price.
      provider_environment:
        type:
          - string
          - 'null'
        enum:
          - sandbox
          - production
          - null
        description: >-
          The provider environment this account was issued in, read only from Swaps' stored record
          of the account — never from the caller's `livemode` or from the server's configuration.
          `sandbox`: a provider test account — bank details are test data and no real bank transfer
          arrives. `production`: a live provider account. `null`: not recorded — never evidence of a
          live account. Every live account today answers `null`, and so, in the provider sandbox,
          does any account not created through `POST /v1/wallet/virtual_accounts` (for example a
          Payment links collection account, or one created by the dashboard's own action or found by
          a provider sync) and any account created before 2026-09-29. Only `sandbox` marks a test
          account: show a test-mode marker on the bank details for `sandbox` and for nothing else.
      events:
        type: array
        description: Redacted event history for this VA.
        items:
          type: object
          properties:
            type:
              type: string
            created_at:
              type: string
              format: date-time
          additionalProperties: true
    additionalProperties: false
unevaluatedProperties: false
```

<a id="virtualaccountsettlesto"></a>
## VirtualAccountSettlesTo

```yaml
type: object
description: >-
  The settlement destination of a virtual account. `kind: wallet` only when the destination is an
  active wallet of this holder on the network the destination settles on (Tempo mainnet for a live
  account); any other on-chain destination is `address`; a fiat destination rail is `bank`.
  `masked_address` shows the first six and last four characters, case preserved. `address` is the
  full address, for a live session whose role on the account is `owner` or `admin` only.
required:
  - kind
  - label
properties:
  kind:
    type: string
    x-swaps-open-enum: true
    enum:
      - wallet
      - address
      - bank
  label:
    type: string
    description: Display label, e.g. `Swaps Wallet`, `USDC on Ethereum`, `Bank account (SEPA)`.
  network:
    type: string
    description: The destination network for `wallet` and `address` kinds, e.g. `tempo`, `ethereum`.
  asset:
    type: string
    description: >-
      The destination asset ticker (e.g. `USDC`), for `kind: address` — read from the account's
      stored destination currency (W-R25: makes a `legacy_wallet` account's destination
      machine-readable alongside `network` and `masked_address`, not just the `label` text). Absent
      when the stored currency is unknown.
  masked_address:
    type: string
    description: The destination address, masked. Absent for `bank`.
  address:
    type: string
    description: >-
      The full settlement address. Present only for a live session (bearer) whose role on the
      account is `owner` or `admin`, which are the only callers these operations admit (a business
      key is refused with `403 scope_denied`). Until per-client binding (K12) exists, a session
      token presented by any client counts as that session. Exactly as stored, case preserved
      (Solana, Tron and BTC addresses are case-sensitive). Absent for `bank`.
additionalProperties: false
examples:
  - kind: wallet
    label: Swaps Wallet
    network: tempo
    masked_address: 0x1a2B…9fE0
  - kind: address
    label: USDC on Ethereum
    network: ethereum
    asset: USDC
    masked_address: 0x7cD4…31aB
  - kind: address
    label: USDC on Ethereum
    network: ethereum
    asset: USDC
    masked_address: 0x7cD4…31aB
    address: '0x7cD4a3b5E1f09c2D8e6A4B7F1c3E5a9D000031aB'
  - kind: bank
    label: Bank account (SEPA)
```

<a id="virtualaccountcreaterequest"></a>
## VirtualAccountCreateRequest

```yaml
type: object
description: >
  An explicit confirm — a VA is a billable provider object and is never auto-created (RESOURCE-MODEL
  §2.3 invariants; R13-wallet `layer-w-va-create`).
required:
  - currency
properties:
  currency:
    type: string
    description: >-
      `usd`, `eur` or `mxn` (case-insensitive) — the matching `wallet_bank_virtual_account`
      corridor.
additionalProperties: false
```

<a id="virtualaccountlist"></a>
## VirtualAccountList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    required:
      - data
    properties:
      data:
        type: array
        items:
          $ref: '#/components/schemas/VirtualAccount'
unevaluatedProperties: false
```

<a id="virtualaccounthistorylist"></a>
## VirtualAccountHistoryList

```yaml
allOf:
  - $ref: '#/components/schemas/ListMeta'
  - type: object
    description: >
      "Money in" rows for one VA — a pull read over `virtual_account.deposit_received`
      (RESOURCE-MODEL §3), itself redacted; the real push producer is the Bridge webhook. One row
      per bank deposit, whatever became of it: `status` says whether the money is still being
      checked, was returned to the sender's bank, or was delivered to the wallet. A returned deposit
      never reached the wallet; its `amount` is the fiat that went back.
    required:
      - data
    properties:
      data:
        type: array
        items:
          type: object
          required:
            - type
            - status
            - amount
            - occurred_at
          properties:
            type:
              type: string
              enum:
                - deposit_received
            status:
              type: string
              description: >
                `checking` — received by the bank partner, not yet delivered (its `funds_received`,
                `in_review` or `payment_submitted` step); `returned` — sent back to the sender's
                bank (`refund_in_flight`, `refunded`), nothing arrived in the wallet;
                `return_failed` — the bank partner could not send it back either (`refund_failed`:
                the sender's account was closed or deactivated), nothing arrived in the wallet and
                the holder should contact support; `delivered` — paid into the wallet
                (`payment_processed`), `amount` is then the delivered stablecoin net of fees.
              enum:
                - checking
                - returned
                - return_failed
                - delivered
            amount:
              $ref: '#/components/schemas/Money'
            occurred_at:
              type: string
              format: date-time
          additionalProperties: false
unevaluatedProperties: false
```

<a id="conversiontrackingstatus"></a>
## ConversionTrackingStatus

```yaml
type: string
description: >
  The tracked leg's status (RESOURCE-MODEL §2.3). A `kind: dex` conversion — the only kind this API
  builds (D-25: G cut the proposed `kind: cex` branch, 2026-09-08, Exolix removed) — reaches only
  `pending` (built, unsigned) and `submitted` (its `POST .../source_tx` recorded and
  on-chain-verified the swap's own broadcast transaction); `settled`/`failed` are not yet advanced
  automatically past that point.
enum:
  - pending
  - submitted
  - settled
  - failed
```

<a id="conversion"></a>
## Conversion

```yaml
allOf:
  - $ref: '#/components/schemas/ObjectBase'
  - type: object
    description: >
      Builds a same-chain conversion (RESOURCE-MODEL §2.3, v2 amendments; D-25). `kind: dex` builds
      an unsigned swap on Tempo — the API never signs: `steps[]` are unsigned calls the holder's own
      passkey signs. D-25's proposed `kind: cex` no-wallet exchange leg (deposit-address branch) is
      withdrawn — G cut it 2026-09-08 (Exolix removed; Swap stays wallet-signed OKX DEX) — so
      `conversions` never carries a CEX projection.

      A1-6: `id` reserves the `cnv_` prefix (RESOURCE-MODEL §0.2) — this was the one `available`
      operation family with no prefixed resource id and no `livemode`. `tracking_id` is kept as an
      additive alias of the SAME identity for one version (`id` minus its `cnv_` prefix):
      `claude/dashboard-v2`'s already-built Convert flow (`WalletConvertScreen.tsx`,
      `useWalletConversions.ts`) reads `tracking_id` and passes it straight back as `GET/POST
      /v1/wallet/conversions/{id}(/source_tx)`'s path parameter, which still accepts the bare value
      (see that parameter's own description) — do not remove `tracking_id` before that caller
      migrates to `id`.
    required:
      - kind
      - provider_id
      - chain_id
      - from_token
      - to_token
      - amount_in
      - amount_out
      - min_amount_out
      - rate
      - slippage_bps
      - tracking_id
    properties:
      id:
        type: string
        pattern: ^cnv_
        description: >-
          Prefixed id (`cnv_`). See RESOURCE-MODEL §0.2. Equal to `tracking_id` with that prefix
          added.
      object:
        type: string
        enum:
          - conversion
      kind:
        type: string
        enum:
          - dex
        description: >
          Always `dex` — on-chain, returns `steps[]` for the holder's passkey to sign. The proposed
          `cex` (no-wallet exchange leg) branch was withdrawn by G on 2026-09-08 (Exolix removed);
          this field is kept, pinned to `dex`, so a future rail can extend the enum without a
          breaking projection change.
      provider_id:
        type: string
      chain_id:
        type: string
      from_token:
        type: string
      to_token:
        type: string
      amount_in:
        $ref: '#/components/schemas/Money'
      amount_out:
        $ref: '#/components/schemas/Money'
        description: The quoted receive amount, before the slippage bound (R13-wallet §2 W-G9).
      min_amount_out:
        $ref: '#/components/schemas/Money'
      rate:
        type: string
        description: Decimal string, never a float (R13-wallet §2 W-G9). Example `"0.9998"`.
      slippage_bps:
        type: integer
        minimum: 0
      steps:
        type: array
        description: >
          Ordered, unsigned — a two-step ceremony (`approve` then `swap`) is two entries, not one
          `call{}` — structurally explicit rather than inferred from calling `create` twice
          (R13-wallet §2 W-G9).
        items:
          $ref: '#/components/schemas/UnsignedStep'
      tracking_id:
        type: string
        description: >
          A1-6: additive alias of `id` (see this schema's own description) — kept for one version
          for `claude/dashboard-v2`'s existing caller. New callers should prefer `id`.
      tracking_status:
        $ref: '#/components/schemas/ConversionTrackingStatus'
    additionalProperties: false
unevaluatedProperties: false
```

<a id="conversioncreaterequest"></a>
## ConversionCreateRequest

```yaml
type: object
required:
  - from_token
  - to_token
  - amount_in
properties:
  chain_id:
    type: string
  from_token:
    type: string
  to_token:
    type: string
  amount_in:
    $ref: '#/components/schemas/MoneyPositive'
  slippage_bps:
    type: integer
    minimum: 0
additionalProperties: false
```

<a id="conversionsourcetxrequest"></a>
## ConversionSourceTxRequest

```yaml
type: object
description: >
  Records the transaction hash the holder's wallet just broadcast for the swap step. Call once,
  immediately after signing; resending the same hash is a safe no-op, and a different hash for the
  same conversion is refused rather than accepted as a correction.
required:
  - source_tx_hash
properties:
  source_tx_hash:
    type: string
additionalProperties: false
```

<a id="conversionpair"></a>
## ConversionPair

```yaml
type: object
description: Dark-flag (R13-wallet §2 W-G10) — the verified convertible token set with a liquidity flag.
required:
  - from_token
  - to_token
  - has_liquidity
properties:
  from_token:
    type: string
  to_token:
    type: string
  has_liquidity:
    type: boolean
additionalProperties: false
```

<a id="conversionpairlist"></a>
## ConversionPairList

```yaml
type: object
description: >
  A1-6: a plain `{data}` catalogue, not a cursor list, matching DepositRouteList — the operation
  declares no `limit`/`cursor` and previously composed `ListMeta`, publishing
  `has_more`/`next_cursor` the handler always hardcoded `false`/`null` with no way to ever page.
required:
  - data
properties:
  data:
    type: array
    items:
      $ref: '#/components/schemas/ConversionPair'
additionalProperties: false
```
