> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aropay.aro.media/llms.txt
> Use this file to discover all available pages before exploring further.

# Claim letter

> Redeem a claim code into one of **your** wallets (custodial or
external): the escrow vault sends the letter's cAROUSD to the wallet
with a `confidentialTransfer` (`LETTER_CLAIM`), and the letter moves
`CLAIMING → CLAIMED` once the receipt lands and the encrypted amount
is verified. Returns the letter (sealed `amount` and `memo` now
revealed) and the claim transaction.

Single-use and race-safe: the first claimant wins a guarded status
flip; anyone else gets `409 already_claimed`. Idempotent for the same
claimant, who gets the existing claim back. Senders cannot claim their
own letters (`400 cannot_claim_own_letter`); they void instead. If the
release reverts or the vault cannot cover it, the letter returns to
claimable (`502 claim_failed` / `503 escrow_vault_underfunded`). If
the release was broadcast but bookkeeping failed, the call returns
`502 claim_unsettled`: the letter stays `CLAIMING` for you, and
calling claim again returns the in-progress claim once reconciliation
lands.

Blocks up to ~20 s; a `CLAIMING` letter with a `PENDING` claim leg
finishes on later reads. Limited to 10 attempts per 15 minutes per
principal and IP, separately from the general write budget.




## OpenAPI

````yaml /api-reference/openapi.yaml post /transfer/letter/claim
openapi: 3.1.0
info:
  title: AroPay API
  version: 1.0.0
  description: |
    The AroPay API for orchestrating sealed, private payments on public
    ledgers, built on the Aro Confidential Rails (Zama Protocol FHE). Two
    environments run the same API: Production on Ethereum Mainnet
    (`aropay.aro.media`, real funds) and the Sandbox on Sepolia
    (`dev.aropay.aro.media`, test funds and a faucet). Integrations built
    against the sandbox carry over unchanged; the faucet (`POST /fund`) is
    sandbox-only and returns 404 on production.

    Every response (except the flat Sandbox probes)
    uses the shared envelope: `{ "ok": true, "data": … }` on success and
    `{ "ok": false, "error": { "code", "message", "details?" } }` on failure.

    Amounts are decimal strings in human units (e.g. `"125.50"`). Write
    requests must send `Content-Type: application/json`.
  contact:
    name: Aro Media
    url: https://aro.media
servers:
  - url: https://aropay.aro.media/api/v1
    description: Production (Ethereum Mainnet)
  - url: https://dev.aropay.aro.media/api/v1
    description: Sandbox (Sepolia)
  - url: http://aropay.localhost:3000/api/v1
    description: Local development
security:
  - apiKeyAuth: []
  - sessionCookie: []
tags:
  - name: Auth
    description: Password, TOTP, and passkey login; session inspection and logout.
  - name: Profile & security
    description: Profile, password, two-factor authentication, and passkey management.
    x-group: Profile & security
  - name: API keys
    description: Programmatic credentials. Creation and revocation are session-only.
  - name: Wallets
    description: Custodial and external (watch-only) wallets.
  - name: Balances
    description: Balance triples and lightweight gas reads.
  - name: Money movement
    description: |
      Faucet funding, minting, transfers, and redemptions. All four are
      asynchronous: the request is validated and recorded, the response is
      `201` with the transaction in status `QUEUED` (`txHash: null`), and a
      background worker executes it. Poll `GET /transactions/{id}` until
      `status` is `CONFIRMED`, `SETTLED`, or `FAILED`. Execution failures
      surface as status `FAILED` with `errorCode` on the transaction, never
      as an HTTP error on the POST.
  - name: Aro Mail
    description: >-
      Letters of value: escrow cAROUSD, print or mail through Lob, and let the
      recipient claim by QR or code.
  - name: Transactions
    description: >-
      History, single-transaction reads (pure database reads), and notarized
      receipts.
  - name: Sandbox
    description: Unauthenticated status and echo probes with a flat response shape.
paths:
  /transfer/letter/claim:
    post:
      tags:
        - Aro Mail
      summary: Claim letter
      description: |
        Redeem a claim code into one of **your** wallets (custodial or
        external): the escrow vault sends the letter's cAROUSD to the wallet
        with a `confidentialTransfer` (`LETTER_CLAIM`), and the letter moves
        `CLAIMING → CLAIMED` once the receipt lands and the encrypted amount
        is verified. Returns the letter (sealed `amount` and `memo` now
        revealed) and the claim transaction.

        Single-use and race-safe: the first claimant wins a guarded status
        flip; anyone else gets `409 already_claimed`. Idempotent for the same
        claimant, who gets the existing claim back. Senders cannot claim their
        own letters (`400 cannot_claim_own_letter`); they void instead. If the
        release reverts or the vault cannot cover it, the letter returns to
        claimable (`502 claim_failed` / `503 escrow_vault_underfunded`). If
        the release was broadcast but bookkeeping failed, the call returns
        `502 claim_unsettled`: the letter stays `CLAIMING` for you, and
        calling claim again returns the in-progress claim once reconciliation
        lands.

        Blocks up to ~20 s; a `CLAIMING` letter with a `PENDING` claim leg
        finishes on later reads. Limited to 10 attempts per 15 minutes per
        principal and IP, separately from the general write budget.
      operationId: claimLetter
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClaimLetterRequest'
            example:
              claimCode: 7K3M-Q9TX-2ABF-H8RD-5CWN-VGP4-0J6Y
              walletId: ckwlt0004abcd
      responses:
        '200':
          description: The claimed letter and the release transaction.
          content:
            application/json:
              schema:
                type: object
                required:
                  - ok
                  - data
                properties:
                  ok:
                    const: true
                  data:
                    type: object
                    required:
                      - letter
                      - transaction
                    properties:
                      letter:
                        $ref: '#/components/schemas/Letter'
                      transaction:
                        $ref: '#/components/schemas/Transaction'
              example:
                ok: true
                data:
                  letter:
                    id: cklet0001abcd
                    status: CLAIMED
                    chainId: 11155111
                    amount: '50.00'
                    token: cAROUSD
                    amountVisibility: sealed
                    memo: Thanks for the referral
                    recipient:
                      name: Jane Doe
                      address:
                        line1: 12 Adeola Odeku Street
                        line2: Victoria Island
                        landmark: Opposite Eko Hotel
                        city: Lagos
                        region: LA
                        postal: null
                        country: NG
                        countryName: Nigeria
                    isDomestic: false
                    expiresAt: '2026-10-22T09:45:00.000Z'
                    sender:
                      walletId: ckwlt0001abcd
                      address: '0x8Ba1f109551bD432803012645Ac136ddd64DBA72'
                    claimCode: null
                    claimUrl: null
                    pdfUrl: >-
                      https://dev.aropay.aro.media/api/v1/transfer/letter/cklet0001abcd/pdf
                    escrow:
                      transactionId: cktxn0005abcd
                      txHash: 0x4d1e…
                      explorerUrl: https://sepolia.etherscan.io/tx/0x4d1e…
                      status: CONFIRMED
                      verified: true
                    claim:
                      transactionId: cktxn0006abcd
                      txHash: 0x7b90…
                      explorerUrl: https://sepolia.etherscan.io/tx/0x7b90…
                      status: CONFIRMED
                      verified: true
                      claimedAt: '2026-09-19T14:20:44.000Z'
                      walletAddress: '0x52aE1b7c86D2f1e9021a1B4a9c3B8d55E37F9021'
                    refund: null
                    mail:
                      provider: lob
                      providerId: ltr_4868c3b5655f1f5c
                      deliveryMethod: tracked
                      status: processed
                      trackingNumber: '9407300000000000000004'
                      expectedDeliveryDate: '2026-09-22'
                      mailedAt: '2026-09-07T10:02:11.000Z'
                      testMode: true
                    needsReview: false
                    error: null
                    createdAt: '2026-09-07T09:45:00.000Z'
                    updatedAt: '2026-09-19T14:20:44.000Z'
                  transaction:
                    id: cktxn0006abcd
                    type: LETTER_CLAIM
                    status: CONFIRMED
                    chainId: 11155111
                    amount: '50.00'
                    token: cAROUSD
                    txHash: 0x7b90…
                    settlementTxHash: null
                    explorerUrl: https://sepolia.etherscan.io/tx/0x7b90…
                    fromAddress: '0x1F2e3D4c5B6a79880716253443526170A1b2C3d4'
                    toAddress: '0x52aE1b7c86D2f1e9021a1B4a9c3B8d55E37F9021'
                    counterparty: aro-mail escrow
                    walletId: ckwlt0004abcd
                    error: null
                    metadata:
                      letterId: cklet0001abcd
                      leg: claim
                    createdAt: '2026-09-19T14:20:20.000Z'
                    updatedAt: '2026-09-19T14:20:44.000Z'
        '400':
          description: |
            Malformed code (`invalid_claim_code`), the sender claiming their
            own letter (`cannot_claim_own_letter`), or validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                ok: false
                error:
                  code: cannot_claim_own_letter
                  message: You cannot claim a letter you sent. Void it instead.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/AccountDisabled'
        '404':
          description: >-
            No letter matches the code (`letter_not_found`), or `walletId` is
            not one of your wallets (`not_found`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                ok: false
                error:
                  code: letter_not_found
                  message: Letter not found.
        '409':
          description: |
            Another principal or wallet already claimed the letter
            (`already_claimed`), or the letter is `DRAFT`, `VOIDED` or `FAILED`
            (`letter_not_claimable`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                ok: false
                error:
                  code: already_claimed
                  message: This letter has already been claimed.
        '410':
          description: The letter expired; the sender is refunded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                ok: false
                error:
                  code: letter_expired
                  message: >-
                    This letter expired and its value was returned to the
                    sender.
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          description: |
            `claim_failed`: the release transfer reverted on-chain; the letter
            is claimable again. `claim_unsettled`: the release transfer was
            broadcast but bookkeeping failed; the letter stays `CLAIMING` for
            this claimant and is reconciled on its next read — the same claim
            call returns the existing claim.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                claim_failed:
                  summary: Release reverted; letter claimable again
                  value:
                    ok: false
                    error:
                      code: claim_failed
                      message: Release transfer reverted on-chain. Try again.
                claim_unsettled:
                  summary: Release broadcast; bookkeeping pending
                  value:
                    ok: false
                    error:
                      code: claim_unsettled
                      message: >-
                        The claim transaction 0x9a41… was submitted but could
                        not be recorded fully; it will be reconciled shortly.
        '503':
          description: >-
            The escrow vault cannot cover the payout right now (or the payout
            clamped to zero); the letter stays claimable. Platform-side.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                ok: false
                error:
                  code: escrow_vault_underfunded
                  message: >-
                    The escrow vault is temporarily under-funded. Try again
                    later.
components:
  schemas:
    ClaimLetterRequest:
      type: object
      required:
        - claimCode
        - walletId
      properties:
        claimCode:
          type: string
          minLength: 1
          maxLength: 64
          description: >-
            The 28-character code from the letter, with or without
            dashes/spaces; case-insensitive.
        walletId:
          type: string
          description: One of your wallets (custodial or external) to receive the cAROUSD.
    Letter:
      type: object
      description: |
        An Aro Mail letter: escrowed cAROUSD, a recipient address, a one-time
        claim code, and the legs that moved value.
      required:
        - id
        - status
        - chainId
        - amount
        - token
        - amountVisibility
        - memo
        - recipient
        - isDomestic
        - expiresAt
        - sender
        - claimCode
        - claimUrl
        - pdfUrl
        - escrow
        - claim
        - refund
        - mail
        - needsReview
        - error
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          examples:
            - cklet0001abcd
        status:
          type: string
          enum:
            - DRAFT
            - ESCROWED
            - MAILED
            - CLAIMING
            - CLAIMED
            - EXPIRED
            - VOIDED
            - FAILED
          description: |
            `DRAFT` (escrow submitted) → `ESCROWED` (escrow verified) →
            optional `MAILED` → `CLAIMING` → `CLAIMED`. `EXPIRED` and `VOIDED`
            carry a refund leg. `FAILED` means the escrow reverted, clamped or
            was never submitted: no value left the sender. Claimable:
            `ESCROWED`, `MAILED`. Terminal: `CLAIMED`, `EXPIRED`, `VOIDED`,
            `FAILED`.
        chainId:
          type: integer
          examples:
            - 11155111
        amount:
          type: string
          description: >-
            Decimal string in human units. Cleartext to the parties; encrypted
            on-chain.
        token:
          type: string
          examples:
            - cAROUSD
        amountVisibility:
          type: string
          enum:
            - sealed
            - visible
          description: >-
            Whether the printed letter shows the amount. Defaults to `visible`
            domestic, `sealed` international.
        memo:
          type:
            - string
            - 'null'
          description: Up to 200 characters, printed on the letter.
        recipient:
          type: object
          required:
            - name
            - address
          properties:
            name:
              type: string
            address:
              $ref: '#/components/schemas/LetterAddress'
        isDomestic:
          type: boolean
          description: >-
            True when the destination is the deployment's home country (`US` by
            default).
        expiresAt:
          type: string
          format: date-time
        sender:
          type: object
          required:
            - walletId
            - address
          properties:
            walletId:
              type:
                - string
                - 'null'
              description: >-
                The sending wallet; `null` if that wallet was later deleted
                (refunds still go to `address`).
            address:
              type: string
              description: EIP-55 address the escrow came from and refunds return to.
        claimCode:
          type:
            - string
            - 'null'
          description: |
            Display form (`XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX`, 28 Crockford
            base32 symbols incl. two check symbols). Owner-only: present on
            create and single reads while the letter is non-terminal; `null`
            in lists and once terminal.
        claimUrl:
          type:
            - string
            - 'null'
          description: >-
            `https://<aropay-host>/claim#<code>`; the QR payload. Same
            visibility as `claimCode`.
        pdfUrl:
          type: string
          description: >-
            Absolute URL of `GET /transfer/letter/{id}/pdf`, built from the
            claim origin fixed at creation.
        escrow:
          description: >-
            The `LETTER_ESCROW` leg (sender → vault); `null` only if the row was
            never created.
          oneOf:
            - $ref: '#/components/schemas/LetterLeg'
            - type: 'null'
        claim:
          description: >-
            The `LETTER_CLAIM` leg (vault → recipient wallet); `null` until
            someone claims.
          oneOf:
            - allOf:
                - $ref: '#/components/schemas/LetterLeg'
                - type: object
                  required:
                    - claimedAt
                    - walletAddress
                  properties:
                    claimedAt:
                      type:
                        - string
                        - 'null'
                      format: date-time
                      description: Set when the letter reaches `CLAIMED`.
                    walletAddress:
                      type:
                        - string
                        - 'null'
                      description: The wallet the value was released to.
            - type: 'null'
        refund:
          description: >-
            The `LETTER_REFUND` leg (vault → sender address); `null` unless the
            letter was voided or expired.
          oneOf:
            - allOf:
                - $ref: '#/components/schemas/LetterLeg'
                - type: object
                  required:
                    - reason
                  properties:
                    reason:
                      type: string
                      enum:
                        - voided
                        - expired
            - type: 'null'
        mail:
          description: >-
            Fulfilment details; `null` until `POST /transfer/letter/{id}/mail`
            is attempted.
          oneOf:
            - type: object
              required:
                - provider
                - providerId
                - deliveryMethod
                - status
                - trackingNumber
                - expectedDeliveryDate
                - mailedAt
                - testMode
              properties:
                provider:
                  type: string
                  examples:
                    - lob
                providerId:
                  type:
                    - string
                    - 'null'
                  description: Lob letter id once accepted.
                deliveryMethod:
                  type:
                    - string
                    - 'null'
                  enum:
                    - standard
                    - tracked
                    - courier
                    - null
                status:
                  type:
                    - string
                    - 'null'
                  description: >-
                    Lob letter status (`processed`, `rendered`, `failed`),
                    refreshed on owner reads.
                trackingNumber:
                  type:
                    - string
                    - 'null'
                  description: >-
                    USPS tracking number for `tracked` letters (dummy in test
                    mode).
                expectedDeliveryDate:
                  type:
                    - string
                    - 'null'
                  description: Lob's estimate, `YYYY-MM-DD`.
                mailedAt:
                  type:
                    - string
                    - 'null'
                  format: date-time
                testMode:
                  type: boolean
                  description: >-
                    True when the deployment uses a Lob test key; nothing is
                    printed or mailed.
            - type: 'null'
        needsReview:
          type: boolean
          description: >-
            True when a leg could not be verified automatically; nothing moves
            until an operator reviews `error`.
        error:
          type:
            - string
            - 'null'
          description: Failure or review context for the most recent leg.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Transaction:
      type: object
      description: |
        A money-movement operation and its on-chain outcome. Created as
        `QUEUED` by the `POST`, then advanced by the background worker.
        Execution failures surface as status `FAILED` with `errorCode` on the
        transaction.
      required:
        - id
        - type
        - status
        - chainId
        - amount
        - token
        - txHash
        - settlementTxHash
        - explorerUrl
        - fromAddress
        - toAddress
        - counterparty
        - walletId
        - error
        - errorCode
        - metadata
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          examples:
            - cktxn0001abcd
        type:
          type: string
          enum:
            - FUND
            - MINT
            - TRANSFER
            - PRIVATE_TRANSFER
            - REDEEM
            - LETTER_ESCROW
            - LETTER_CLAIM
            - LETTER_REFUND
          description: |
            `LETTER_ESCROW`, `LETTER_CLAIM` and `LETTER_REFUND` are the three
            Aro Mail legs (sender → vault, vault → recipient, vault → sender);
            their `metadata` carries `{ letterId, leg }`.
        status:
          $ref: '#/components/schemas/TransactionStatus'
        chainId:
          type: integer
          examples:
            - 11155111
        amount:
          type: string
          description: Decimal string in human units.
        token:
          type: string
          description: Token symbol the amount denominates.
        txHash:
          type:
            - string
            - 'null'
          description: Hash of the primary on-chain leg. `null` while `QUEUED`.
        settlementTxHash:
          type:
            - string
            - 'null'
          description: >-
            Redeems only: hash of the operator's payout leg once submitted.
            `null` for every other type (the faucet's gas top-up hash is in
            `metadata.gasTxHash`).
        explorerUrl:
          type:
            - string
            - 'null'
          description: Block-explorer link for `txHash`; `null` until `txHash` is set.
        fromAddress:
          type:
            - string
            - 'null'
        toAddress:
          type:
            - string
            - 'null'
        counterparty:
          type:
            - string
            - 'null'
        walletId:
          type:
            - string
            - 'null'
        error:
          type:
            - string
            - 'null'
          description: >-
            Human-readable failure context. Non-null only when `status` is
            `FAILED`; may be reworded, match on `errorCode` instead.
        errorCode:
          oneOf:
            - $ref: '#/components/schemas/TransactionErrorCode'
            - type: 'null'
          description: >-
            Machine-readable failure code. Non-null only when `status` is
            `FAILED`.
        metadata:
          description: >-
            Operation-specific context (object or null), e.g. `mode`,
            `approveTxHash`, `gasToppedUp`, `gasTxHash`, `setOperatorTxHash`,
            `redeemId`.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ErrorResponse:
      type: object
      description: The shared failure envelope.
      required:
        - ok
        - error
      properties:
        ok:
          const: false
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code; match on this, not `message`.
            message:
              type: string
            details:
              description: Structured context, e.g. per-field validation issues.
    LetterAddress:
      type: object
      description: Recipient postal address as stored, with the resolved country name.
      required:
        - line1
        - line2
        - landmark
        - city
        - region
        - postal
        - country
        - countryName
      properties:
        line1:
          type: string
        line2:
          type:
            - string
            - 'null'
        landmark:
          type:
            - string
            - 'null'
          description: Printed as `Near …` on the letter.
        city:
          type: string
        region:
          type:
            - string
            - 'null'
        postal:
          type:
            - string
            - 'null'
        country:
          type: string
          description: ISO 3166-1 alpha-2, upper-case.
          examples:
            - NG
        countryName:
          type: string
          examples:
            - Nigeria
    LetterLeg:
      type: object
      description: >-
        One on-chain leg of a letter (escrow, release or refund), linked to its
        transaction row.
      required:
        - transactionId
        - txHash
        - explorerUrl
        - status
        - verified
      properties:
        transactionId:
          type: string
          description: The `Transaction` id; read it with `GET /transactions/{id}`.
          examples:
            - cktxn0005abcd
        txHash:
          type:
            - string
            - 'null'
        explorerUrl:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - PENDING
            - CONFIRMED
            - SETTLING
            - SETTLED
            - FAILED
          description: The linked transaction's status.
        verified:
          type: boolean
          description: |
            True once the leg's `ConfidentialTransfer` event was decrypted and
            matched the letter amount. The letter only changes status after
            verification, so `CONFIRMED` + `verified: false` means the receipt
            is in and verification is pending.
    TransactionStatus:
      type: string
      enum:
        - QUEUED
        - PENDING
        - CONFIRMED
        - SETTLING
        - SETTLED
        - FAILED
      description: >
        Lifecycle of a money-movement transaction, executed by the background

        worker after the `POST` returns:


        - `QUEUED`: accepted and recorded; nothing submitted yet (`txHash:
        null`).

        - `PENDING`: primary transaction broadcast (`txHash` set); awaiting the
        receipt.

        - `CONFIRMED`: receipt success. Terminal for every type except `REDEEM`.

        - `SETTLING`: redeem burn confirmed; operator payout in progress.

        - `SETTLED`: redeem payout confirmed (`settlementTxHash` set). Terminal.

        - `FAILED`: pre-flight rejection or on-chain revert; nothing moved
        on-chain. Terminal. `error` and `errorCode` are set.


        Poll until the status is in the terminal set (`CONFIRMED`, `SETTLED`,

        `FAILED`) and treat unknown values as in flight.
    TransactionErrorCode:
      type: string
      description: >
        Machine-readable reason on a `FAILED` transaction. Every code implies

        that nothing moved on-chain for the row.


        - `insufficient_stable`: backed `MINT`; public stablecoin below `amount`
        when the worker ran.

        - `insufficient_balance`: public `TRANSFER`; stablecoin below `amount`
        when the worker ran.

        - `insufficient_confidential_balance`: `PRIVATE_TRANSFER`/`REDEEM`;
        decrypted cAROUSD (minus other in-flight debits) below `amount` after
        the retry window.

        - `faucet_cap_exceeded`: `FUND`; rolling 24 h cap exceeded at execution
        time.

        - `account_disabled`: account deactivated before execution.

        - `wallet_unavailable`: wallet deleted, not custodial, or missing
        signing material at execution time.

        - `wallet_key_unreadable`: custodial key could not be decrypted
        (platform-side).

        - `invalid_amount`: stored amount not representable for the token (e.g.
        uint64 range).

        - `faucet_failed` / `mint_failed` / `transfer_failed` / `redeem_failed`:
        the primary or an auxiliary on-chain leg reverted.

        - `transaction_dropped`: legacy row (pre-worker) whose transaction
        vanished from the network and cannot be re-broadcast.

        - `job_exhausted`: repeated transient errors (RPC/relayer) exhausted the
        worker's retries before anything was broadcast; safe to resubmit.
      enum:
        - insufficient_stable
        - insufficient_balance
        - insufficient_confidential_balance
        - faucet_cap_exceeded
        - account_disabled
        - wallet_unavailable
        - wallet_key_unreadable
        - invalid_amount
        - faucet_failed
        - mint_failed
        - transfer_failed
        - redeem_failed
        - transaction_dropped
        - job_exhausted
  responses:
    Unauthorized:
      description: Missing or invalid credential.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            ok: false
            error:
              code: invalid_api_key
              message: Invalid API key.
    AccountDisabled:
      description: The account has been deactivated.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            ok: false
            error:
              code: account_disabled
              message: This account is disabled.
    RateLimited:
      description: Rate limit exceeded.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            ok: false
            error:
              code: rate_limited
              message: Too many requests. Try again shortly.
    InternalError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            ok: false
            error:
              code: internal_error
              message: An unexpected error occurred.
  securitySchemes:
    apiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: aro_sk_…
      description: |
        AroPay API key, sent as `Authorization: Bearer aro_sk_…`. Created in
        the dashboard under Settings → API keys. Takes precedence over a
        session cookie when both are present.
      x-default: aro_sk_your_key_here
    sessionCookie:
      type: apiKey
      in: cookie
      name: aropay_session
      description: |
        Browser session cookie minted by `POST /auth/login` (or passkey
        login). httpOnly, `SameSite=Lax`, 24 h TTL. Endpoints marked
        **Session-only** accept only this credential and return
        `403 session_required` for API keys.

````