openapi: 3.1.0
info:
  title: Decaf Business API
  version: 1.0.0
  summary: Pay anyone, anywhere, from code.
  description: |
    Payouts to bank accounts in 40 currencies, mobile payment accounts, wallets, or a phone number.
    Batches with approvals, payment links and virtual accounts to get paid, signed webhooks for every
    state change. Fees, limits and arrival times are returned on every channel and every quote.
  contact: { name: Decaf, email: partners@decaf.so, url: https://decaf.so }
servers:
  - url: https://sandbox.api.decaf.so/v1
    description: Sandbox. Free funds; payouts simulate every state including failure and refund.
  - url: https://api.decaf.so/v1
    description: Live.
security:
  - bearerAuth: []
tags:
  - { name: Channels, description: Where you can pay, required fields, limits, arrival, your fee. }
  - { name: Quotes, description: Price one payout on one channel. Valid 60 seconds. }
  - { name: Recipients, description: Saved destinations. One recipient, one type, one destination. }
  - { name: Payouts, description: One transfer to one recipient on one channel. }
  - { name: Withdrawals, description: Payouts to a bank account in your own company's name. }
  - { name: Disbursements, description: Batches with review, approvals, retries and export. }
  - { name: Payment links, description: Get paid by link, card, crypto or bank transfer. }
  - { name: Payment requests, description: Fixed-amount requests tied to an invoice. }
  - { name: Virtual accounts, description: Named bank details that convert incoming transfers to USDC. }
  - { name: Webhooks, description: Signed events for every state change. }
  - { name: API keys, description: Scoped keys with budgets and approval thresholds. }

paths:
  /channels:
    get:
      tags: [Channels]
      operationId: listChannels
      summary: List channels
      description: "Every destination with status, coverage, recipient type, limits, typical arrival and your fee at your current tier."
      responses:
        "200":
          description: Channels
          content:
            application/json:
              schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Channel" } } } }
              example:
                data:
                  - { id: spei_mxn, status: live, country: MX, currency: MXN, rail: spei, recipient_type: bank_account, arrival: { min: PT0M, max: PT5M }, limits: { min: "30.12", max: "300000.00", currency: USDC }, fee: { percent: "0.35", flat: "0.50", currency: USDC } }
                  - { id: claim_link, status: live, country: null, currency: USDC, rail: claim_link, recipient_type: phone, arrival: { min: PT0M, max: P30D }, limits: { min: "1.00", max: "50000.00", currency: USDC }, fee: { percent: "0", flat: "0", currency: USDC } }
  /channels/{channel}/requirements:
    get:
      tags: [Channels]
      operationId: getChannelRequirements
      summary: Required recipient fields for a channel
      parameters:
        - { name: channel, in: path, required: true, schema: { type: string }, example: spei_mxn }
      responses:
        "200":
          description: Requirements
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChannelRequirements" }
              example:
                channel: spei_mxn
                recipient_type: bank_account
                fields:
                  - { name: holder_name, type: string, required: true }
                  - { name: clabe, type: string, required: true, pattern: "^[0-9]{18}$" }
                  - { name: recipient_kind, type: enum, values: [individual, business], required: true }
                limits: { min: "30.12", max: "300000.00", currency: USDC }

  /quotes:
    post:
      tags: [Quotes]
      operationId: createQuote
      summary: Create a quote
      description: "Price one payout on one channel. Provide source_amount or destination_amount. Valid 60 seconds."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/QuoteCreate" }
            example: { channel: chats_hkd, source_amount: "2500.00" }
      responses:
        "201":
          description: Quote
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Quote" }
              example:
                id: qt_5m2n
                channel: chats_hkd
                source_amount: "2500.00"
                source_currency: USDC
                destination_amount: "19491.72"
                destination_currency: HKD
                rate: "7.8280"
                fees: { provider: "1.25", decaf: "8.75", fx_spread: "3.90", total: "13.90", currency: USDC }
                arrival: { min: PT1M, max: P3D }
                expires_at: "2026-09-16T15:04:05Z"
        "400": { $ref: "#/components/responses/BadRequest" }

  /recipients:
    get:
      tags: [Recipients]
      operationId: listRecipients
      summary: List recipients
      parameters:
        - { name: type, in: query, schema: { $ref: "#/components/schemas/RecipientType" } }
        - { name: country, in: query, schema: { type: string } }
        - { $ref: "#/components/parameters/limit" }
        - { $ref: "#/components/parameters/cursor" }
      responses:
        "200": { description: Recipients, content: { application/json: { schema: { $ref: "#/components/schemas/RecipientList" } } } }
    post:
      tags: [Recipients]
      operationId: createRecipient
      summary: Create a recipient
      parameters: [ { $ref: "#/components/parameters/idempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RecipientCreate" }
            example:
              type: bank_account
              country: HK
              currency: HKD
              external_id: vendor-4471
              name: Brightway Electronics Ltd
              kind: business
              bank_account: { bank_name: HSBC, account_number: "123456789012", swift: HSBCHKHHHKH, id_document_number: 12345678-000-01-23-4 }
      responses:
        "201":
          description: Recipient
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Recipient" }
              example: { id: rcp_9d1a, type: bank_account, country: HK, currency: HKD, name: Brightway Electronics Ltd, kind: business, external_id: vendor-4471, status: ready, channels: [chats_hkd], bank_account: { bank_name: HSBC, account_number: "•••• 9012", swift: HSBCHKHHHKH }, created_at: "2026-09-16T14:59:00Z" }
        "422": { $ref: "#/components/responses/RecipientNotReady" }
  /recipients/check:
    get:
      tags: [Recipients]
      operationId: checkRecipient
      summary: Check whether a phone or email can receive
      description: "Says before sending whether a person in that country can receive, and by which methods."
      parameters:
        - { name: type, in: query, required: true, schema: { type: string, enum: [phone, email] } }
        - { name: number, in: query, schema: { type: string }, example: "+5215512345678" }
        - { name: address, in: query, schema: { type: string } }
      responses:
        "200":
          description: Receivability
          content:
            application/json:
              example: { receivable: true, country: MX, methods: [spei_bank, decaf_wallet], kyc_required_for: [spei_bank] }
  /recipients/{id}:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    get:
      tags: [Recipients]
      operationId: getRecipient
      summary: Get a recipient
      responses: { "200": { description: Recipient, content: { application/json: { schema: { $ref: "#/components/schemas/Recipient" } } } } }
    patch:
      tags: [Recipients]
      operationId: updateRecipient
      summary: Update a recipient
      requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/RecipientCreate" } } } }
      responses: { "200": { description: Recipient, content: { application/json: { schema: { $ref: "#/components/schemas/Recipient" } } } } }
    delete:
      tags: [Recipients]
      operationId: deleteRecipient
      summary: Delete a recipient
      responses: { "204": { description: Deleted } }

  /payouts:
    get:
      tags: [Payouts]
      operationId: listPayouts
      summary: List payouts
      parameters:
        - { name: status, in: query, schema: { $ref: "#/components/schemas/PayoutStatus" } }
        - { name: channel, in: query, schema: { type: string } }
        - { name: since, in: query, schema: { type: string, format: date-time } }
        - { $ref: "#/components/parameters/limit" }
        - { $ref: "#/components/parameters/cursor" }
      responses:
        "200": { description: Payouts, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Payout" } }, next_cursor: { type: [string, "null"] } } } } } }
    post:
      tags: [Payouts]
      operationId: createPayout
      summary: Create a payout
      description: "One transfer to one recipient on one channel. Pass a saved recipient_id or an inline recipient. Either source_amount or destination_amount. quote_id is optional; without it the payout is priced at execution."
      parameters: [ { $ref: "#/components/parameters/idempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PayoutCreate" }
            examples:
              saved_recipient:
                summary: Saved recipient with a quote
                value: { recipient_id: rcp_9d1a, channel: spei_mxn, quote_id: qt_5m2n, source_amount: "1000.00", reference: INV-2026-0912, memo: "Freight, week 37" }
              claim_link:
                summary: Pay a phone number
                value: { recipient: { type: phone, number: "+5215512345678" }, channel: claim_link, source_amount: "80.00", reference: driver-week-37, message: Week 37 deliveries }
      responses:
        "201":
          description: Payout
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payout" }
              example:
                id: po_1b7e
                status: submitted
                channel: spei_mxn
                recipient_id: rcp_9d1a
                source_amount: "1000.00"
                source_currency: USDC
                destination_amount: "16865.35"
                destination_currency: MXN
                rate: "16.86535"
                fees: { provider: "0.50", decaf: "5.00", total: "5.50", currency: USDC }
                reference: INV-2026-0912
                estimated_arrival: "2026-09-16T15:20:00Z"
                provider_reference: null
                attempts: 1
                created_at: "2026-09-16T15:01:12Z"
        "400": { $ref: "#/components/responses/BadRequest" }
        "402": { $ref: "#/components/responses/InsufficientBalance" }
        "409": { $ref: "#/components/responses/IdempotencyConflict" }
        "410": { $ref: "#/components/responses/QuoteExpired" }
  /payouts/{id}:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    get:
      tags: [Payouts]
      operationId: getPayout
      summary: Get a payout
      description: "Includes an attempts array with timing, status and provider error per attempt."
      responses: { "200": { description: Payout, content: { application/json: { schema: { $ref: "#/components/schemas/Payout" } } } } }
  /payouts/{id}/receipt:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    get:
      tags: [Payouts]
      operationId: getPayoutReceipt
      summary: Shareable receipt
      responses: { "200": { description: Receipt, content: { application/json: { example: { url: "https://decaf.so/r/t_8f2a", confirmed_received_at: null } } } } }

  /withdrawals:
    post:
      tags: [Withdrawals]
      operationId: createWithdrawal
      summary: Withdraw to your own bank
      description: "Requires business verification. Uses a saved payout destination in your company name. Provide source_amount or destination_amount."
      parameters: [ { $ref: "#/components/parameters/idempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currency, payout_destination_id]
              properties:
                currency: { type: string, enum: [USD, EUR, MXN] }
                payout_destination_id: { type: string }
                source_amount: { $ref: "#/components/schemas/Amount" }
                destination_amount: { $ref: "#/components/schemas/Amount" }
                reference: { type: string }
            example: { currency: EUR, payout_destination_id: pd_3c9f, source_amount: "5000.00" }
      responses:
        "201": { description: "Payout with channel sepa_eur, ach_usd or spei_mxn", content: { application/json: { schema: { $ref: "#/components/schemas/Payout" } } } }
        "403": { $ref: "#/components/responses/VerificationRequired" }

  /disbursements:
    post:
      tags: [Disbursements]
      operationId: createDisbursement
      summary: Create a disbursement
      description: "A batch of payouts prepared, reviewed, approved and launched together. Up to 5,000 items. Rows are validated before any money moves."
      parameters: [ { $ref: "#/components/parameters/idempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DisbursementCreate" }
            example:
              name: Agents, week 37
              items:
                - { recipient_id: rcp_9d1a, channel: spei_mxn, source_amount: "2500.00", reference: AG-014 }
                - { recipient: { type: phone, number: "+5215512345678" }, channel: claim_link, source_amount: "80.00", reference: driver-week-37 }
                - { recipient: { type: pagomovil, phone_number: "+584141234567", bank_code: "0102", national_id: V-12345678, holder_name: Ana Torres, date_of_birth: "1990-04-12" }, channel: pagomovil_ves, source_amount: "150.00", reference: VE-22 }
      responses:
        "201": { description: Disbursement, content: { application/json: { schema: { $ref: "#/components/schemas/Disbursement" } } } }
  /disbursements/{id}:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    get:
      tags: [Disbursements]
      operationId: getDisbursement
      summary: Get a disbursement with its review summary
      responses:
        "200":
          description: Disbursement
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Disbursement" }
              example:
                id: db_44e0
                name: Agents, week 37
                status: needs_review
                summary: { item_count: 38, ready: 36, needs_review: 2, total_source_amount: "96400.00", total_fees: "412.10", by_type: { bank_account: 31, phone: 5, pagomovil: 2 }, warnings: [ { code: duplicate_recipient, item_ids: [di_18, di_204] } ] }
                balance_check: { required: "96812.10", available: "120000.00", ok: true }
                approval: { required: 1, received: 0, revision_id: null }
  /disbursements/{id}/items/import:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    post:
      tags: [Disbursements]
      operationId: importDisbursementItems
      summary: Import items from CSV
      description: "Required columns `recipient`, `amount`. Optional `memo`, `reference`, `type`, `pagomovil_bank_code`, `pagomovil_national_id`, `pagomovil_holder_name`, `pagomovil_dob`."
      requestBody: { required: true, content: { multipart/form-data: { schema: { type: object, properties: { file: { type: string, format: binary } } } } } }
      responses: { "200": { description: Disbursement, content: { application/json: { schema: { $ref: "#/components/schemas/Disbursement" } } } } }
  /disbursements/{id}/items/{item_id}:
    parameters: [ { $ref: "#/components/parameters/id" }, { name: item_id, in: path, required: true, schema: { type: string } } ]
    patch:
      tags: [Disbursements]
      operationId: updateDisbursementItem
      summary: Fix an item
      description: "Editable while drafting, and for failed items in correction mode. A change to a money-critical field creates a new execution version."
      requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/DisbursementItemCreate" } } } }
      responses: { "200": { description: Item, content: { application/json: { schema: { $ref: "#/components/schemas/DisbursementItem" } } } } }
  /disbursements/{id}/submit-for-approval:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    post: { tags: [Disbursements], operationId: submitDisbursementForApproval, summary: Submit for approval, responses: { "200": { description: Disbursement, content: { application/json: { schema: { $ref: "#/components/schemas/Disbursement" } } } } } }
  /disbursements/{id}/approvals/{revision_id}/approve:
    parameters: [ { $ref: "#/components/parameters/id" }, { name: revision_id, in: path, required: true, schema: { type: string } } ]
    post: { tags: [Disbursements], operationId: approveDisbursement, summary: Approve a revision, description: "The submitter cannot approve their own batch.", responses: { "200": { description: Disbursement } } }
  /disbursements/{id}/approvals/{revision_id}/reject:
    parameters: [ { $ref: "#/components/parameters/id" }, { name: revision_id, in: path, required: true, schema: { type: string } } ]
    post: { tags: [Disbursements], operationId: rejectDisbursement, summary: Reject a revision, requestBody: { content: { application/json: { schema: { type: object, properties: { reason: { type: string } } } } } }, responses: { "200": { description: Disbursement } } }
  /disbursements/{id}/launch:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    post: { tags: [Disbursements], operationId: launchDisbursement, summary: Launch, description: "Refused while items need review, approvals are outstanding, or the balance does not cover the batch.", responses: { "200": { description: Disbursement }, "402": { $ref: "#/components/responses/InsufficientBalance" }, "409": { $ref: "#/components/responses/ApprovalRequired" } } }
  /disbursements/{id}/pause:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    post: { tags: [Disbursements], operationId: pauseDisbursement, summary: Pause, responses: { "200": { description: Disbursement } } }
  /disbursements/{id}/resume:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    post: { tags: [Disbursements], operationId: resumeDisbursement, summary: Resume, responses: { "200": { description: Disbursement } } }
  /disbursements/{id}/items/retry-failed:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    post: { tags: [Disbursements], operationId: retryFailedItems, summary: Retry every retryable failed item, responses: { "200": { description: Disbursement } } }
  /disbursements/{id}/export.csv:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    get: { tags: [Disbursements], operationId: exportDisbursementCsv, summary: Export results as CSV, description: "One row per item: reference, final status, amounts, fees, rate, provider reference, on-chain transaction, attempts, timestamps. Bank and ID fields masked.", responses: { "200": { description: CSV, content: { text/csv: { schema: { type: string } } } } } }

  /payment-links:
    get: { tags: [Payment links], operationId: listPaymentLinks, summary: List payment links, responses: { "200": { description: Links } } }
    post:
      tags: [Payment links]
      operationId: createPaymentLink
      summary: Create a payment link
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PaymentLinkCreate" }
            example: { name: Invoices, methods: [bank_transfer, card, crypto], published: true }
      responses: { "201": { description: Link, content: { application/json: { schema: { $ref: "#/components/schemas/PaymentLink" }, example: { id: pl_7a2c, name: Invoices, url: "https://decaf.so/pay/cargo-ledger", methods: [ { type: bank_transfer, ready: true }, { type: card, ready: true }, { type: crypto, ready: true } ], published: true } } } } }
  /payment-links/{id}:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    get: { tags: [Payment links], operationId: getPaymentLink, summary: Get a payment link, responses: { "200": { description: Link, content: { application/json: { schema: { $ref: "#/components/schemas/PaymentLink" } } } } } }
    patch: { tags: [Payment links], operationId: updatePaymentLink, summary: Update methods or publication, requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/PaymentLinkCreate" } } } }, responses: { "200": { description: Link } } }
  /payment-requests:
    post:
      tags: [Payment requests]
      operationId: createPaymentRequest
      summary: Create a fixed-amount payment request
      description: "Marked paid only when an incoming payment matches amount, currency, timing and owner unambiguously. Unmatched bank transfers still credit your balance as deposits."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PaymentRequestCreate" }
            example: { payment_link_id: pl_7a2c, amount: "1250.00", currency: USD, reference: INV-0912, expires_at: "2026-09-30T00:00:00Z" }
      responses: { "201": { description: Request, content: { application/json: { schema: { $ref: "#/components/schemas/PaymentRequest" }, example: { id: pr_31aa, url: "https://decaf.so/pay/cargo-ledger/pr_31aa", amount: "1250.00", currency: USD, reference: INV-0912, status: open, expires_at: "2026-09-30T00:00:00Z" } } } } }
  /virtual-accounts:
    get: { tags: [Virtual accounts], operationId: listVirtualAccounts, summary: List virtual accounts, responses: { "200": { description: Accounts } } }
    post:
      tags: [Virtual accounts]
      operationId: createVirtualAccount
      summary: Open a virtual account
      description: "Requires business verification and the corridor. Details are returned masked."
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [currency], properties: { currency: { type: string, enum: [USD, EUR, MXN] } } }, example: { currency: EUR } } } }
      responses:
        "201": { description: Account, content: { application/json: { schema: { $ref: "#/components/schemas/VirtualAccount" }, example: { id: va_1f3e, currency: EUR, rail: sepa, status: active, details: { iban: "DE•• •••• 3310", bic: "•••", beneficiary_name: Cargo Ledger GmbH }, reference_required: true } } } }
        "403": { $ref: "#/components/responses/VerificationRequired" }

  /webhooks:
    get: { tags: [Webhooks], operationId: listWebhooks, summary: List webhooks, responses: { "200": { description: Webhooks } } }
    post:
      tags: [Webhooks]
      operationId: createWebhook
      summary: Register a webhook endpoint
      description: "Deliveries retry with backoff for 24 hours. The secret is shown once. Verify Decaf-Signature as HMAC-SHA256 over t + . + raw_body; reject timestamps older than five minutes."
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [url, events], properties: { url: { type: string, format: uri }, events: { type: array, items: { type: string } } } }
            example: { url: "https://example.com/decaf", events: ["payout.*", "disbursement.*", "deposit.*"] }
      responses: { "201": { description: Webhook, content: { application/json: { example: { id: wh_2c1d, url: "https://example.com/decaf", events: ["payout.*", "disbursement.*", "deposit.*"], secret: whsec_9f3e... } } } } }
  /webhooks/{id}:
    parameters: [ { $ref: "#/components/parameters/id" } ]
    delete: { tags: [Webhooks], operationId: deleteWebhook, summary: Delete a webhook, responses: { "204": { description: Deleted } } }

  /api-keys:
    post:
      tags: [API keys]
      operationId: createApiKey
      summary: Create a scoped key
      description: "Owner only. A key carries an optional policy; anything outside it returns policy_exceeded or waits for approval above the threshold."
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ApiKeyCreate" }
            example: { name: support-agent, policy: { daily_budget: "2000.00", per_payout_max: "200.00", channels: [claim_link, spei_mxn], approval_threshold: "150.00" } }
      responses: { "201": { description: Key shown once, content: { application/json: { example: { id: key_7e21, name: support-agent, key: dpc_test_9Qa..., policy: { daily_budget: "2000.00", per_payout_max: "200.00", channels: [claim_link, spei_mxn], approval_threshold: "150.00" } } } } } }

webhooks:
  payout.succeeded:
    post:
      summary: Delivered
      requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/Event" }, example: { id: evt_8ad1, type: payout.succeeded, created_at: "2026-09-16T15:20:11Z", data: { object: { id: po_1b7e, status: succeeded } } } } } }
      responses: { "200": { description: Acknowledged } }
  payout.failed: { post: { summary: Final failure after retries, requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/Event" } } } }, responses: { "200": { description: Acknowledged } } } }
  payout.awaiting_claim: { post: { summary: Claim link sent, responses: { "200": { description: Acknowledged } } } }
  payout.claimed: { post: { summary: Recipient claimed and chose a method, responses: { "200": { description: Acknowledged } } } }
  payout.refunded: { post: { summary: Funds returned after a failed delivery, responses: { "200": { description: Acknowledged } } } }
  disbursement.awaiting_approval: { post: { summary: Submitted for approval, responses: { "200": { description: Acknowledged } } } }
  disbursement.launched: { post: { summary: Execution started, responses: { "200": { description: Acknowledged } } } }
  disbursement.completed: { post: { summary: Every item final and succeeded, responses: { "200": { description: Acknowledged } } } }
  disbursement.completed_with_failures: { post: { summary: "Every item final, at least one failed", responses: { "200": { description: Acknowledged } } } }
  deposit.received: { post: { summary: A bank or on-chain deposit credited your balance, responses: { "200": { description: Acknowledged } } } }
  payment_request.paid: { post: { summary: A fixed-amount request was matched, responses: { "200": { description: Acknowledged } } } }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: "Authorization: Bearer dpc_live_... or dpc_test_... Keys are scoped to one business and one environment."
  parameters:
    id: { name: id, in: path, required: true, schema: { type: string } }
    limit: { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
    cursor: { name: cursor, in: query, schema: { type: string } }
    idempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string }
      description: "Replaying the same key with the same body returns the original result; a different body returns 409. Retained 30 days."
  responses:
    BadRequest: { description: "validation_error, channel_unavailable, amount_out_of_range", content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    InsufficientBalance: { description: insufficient_balance, content: { application/json: { schema: { $ref: "#/components/schemas/Error" }, example: { error: { code: insufficient_balance, message: "Disbursement requires 96812.10 USDC; 90000.00 available.", param: null, request_id: req_8ad1 } } } } }
    IdempotencyConflict: { description: idempotency_conflict, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    QuoteExpired: { description: quote_expired, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    VerificationRequired: { description: verification_required, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    RecipientNotReady: { description: recipient_not_ready, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    ApprovalRequired: { description: approval_required, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  schemas:
    Amount: { type: string, pattern: "^\\d+(\\.\\d{1,6})?$", description: "Decimal string. Up to two decimals for fiat, six for USDC." }
    Fees:
      type: object
      properties: { provider: { $ref: "#/components/schemas/Amount" }, decaf: { $ref: "#/components/schemas/Amount" }, fx_spread: { $ref: "#/components/schemas/Amount" }, total: { $ref: "#/components/schemas/Amount" }, currency: { type: string } }
    Channel:
      type: object
      properties:
        id: { type: string, example: spei_mxn }
        status: { type: string, enum: [live, paused], description: "paused while a provider on the rail is degraded" }
        country: { type: [string, "null"] }
        currency: { type: string }
        rail: { type: string }
        recipient_type: { $ref: "#/components/schemas/RecipientType" }
        arrival: { type: object, properties: { min: { type: string, format: duration }, max: { type: string, format: duration } } }
        limits: { type: object, properties: { min: { $ref: "#/components/schemas/Amount" }, max: { $ref: "#/components/schemas/Amount" }, currency: { type: string } } }
        fee: { type: object, properties: { percent: { type: string }, flat: { $ref: "#/components/schemas/Amount" }, currency: { type: string } } }
    ChannelRequirements:
      type: object
      properties:
        channel: { type: string }
        recipient_type: { $ref: "#/components/schemas/RecipientType" }
        fields: { type: array, items: { type: object, properties: { name: { type: string }, type: { type: string }, required: { type: boolean }, pattern: { type: string }, values: { type: array, items: { type: string } } } } }
        limits: { type: object }
    QuoteCreate:
      type: object
      required: [channel]
      properties:
        channel: { type: string }
        source_amount: { $ref: "#/components/schemas/Amount" }
        destination_amount: { $ref: "#/components/schemas/Amount" }
    Quote:
      type: object
      properties:
        id: { type: string }
        channel: { type: string }
        source_amount: { $ref: "#/components/schemas/Amount" }
        source_currency: { type: string }
        destination_amount: { $ref: "#/components/schemas/Amount" }
        destination_currency: { type: string }
        rate: { type: string }
        fees: { $ref: "#/components/schemas/Fees" }
        arrival: { type: object }
        expires_at: { type: string, format: date-time }
    RecipientType: { type: string, enum: [bank_account, pagomovil, wallet, phone, email, decaf_user] }
    RecipientCreate:
      type: object
      required: [type]
      properties:
        type: { $ref: "#/components/schemas/RecipientType" }
        country: { type: string }
        currency: { type: string }
        name: { type: string }
        kind: { type: string, enum: [individual, business] }
        external_id: { type: string }
        bank_account: { type: object, additionalProperties: true, description: "Fields from the channel requirements." }
        pagomovil: { type: object, properties: { phone_number: { type: string }, bank_code: { type: string }, national_id: { type: string }, holder_name: { type: string }, date_of_birth: { type: string, format: date } } }
        wallet: { type: object, properties: { network: { type: string, enum: [stellar] }, address: { type: string } } }
        phone: { type: object, properties: { number: { type: string, description: "E.164" } } }
        email: { type: object, properties: { address: { type: string, format: email } } }
        decaf_user: { type: object, properties: { handle: { type: string } } }
    Recipient:
      allOf:
        - { $ref: "#/components/schemas/RecipientCreate" }
        - type: object
          properties:
            id: { type: string }
            status: { type: string, enum: [ready, needs_fields] }
            missing_fields: { type: array, items: { type: string } }
            channels: { type: array, items: { type: string } }
            created_at: { type: string, format: date-time }
    RecipientList: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Recipient" } }, next_cursor: { type: [string, "null"] } } }
    InlineRecipient:
      type: object
      required: [type]
      properties:
        type: { $ref: "#/components/schemas/RecipientType" }
        number: { type: string, description: Phone in E.164 }
        address: { type: string, description: "Email, or wallet address" }
        network: { type: string }
        phone_number: { type: string }
        bank_code: { type: string }
        national_id: { type: string }
        holder_name: { type: string }
        date_of_birth: { type: string, format: date }
    PayoutCreate:
      type: object
      required: [channel]
      properties:
        recipient_id: { type: string }
        recipient: { $ref: "#/components/schemas/InlineRecipient" }
        channel: { type: string }
        quote_id: { type: string }
        source_amount: { $ref: "#/components/schemas/Amount" }
        destination_amount: { $ref: "#/components/schemas/Amount" }
        reference: { type: string, maxLength: 140 }
        memo: { type: string, description: "Travels with the payment where the rail supports it." }
        message: { type: string, description: "Claim links only. Shown to the recipient." }
    PayoutStatus: { type: string, enum: [pending, submitted, delivering, awaiting_claim, claimed, succeeded, failed, refunded, canceled] }
    Payout:
      type: object
      properties:
        id: { type: string }
        status: { $ref: "#/components/schemas/PayoutStatus" }
        channel: { type: string }
        recipient_id: { type: [string, "null"] }
        source_amount: { $ref: "#/components/schemas/Amount" }
        source_currency: { type: string }
        destination_amount: { $ref: "#/components/schemas/Amount" }
        destination_currency: { type: string }
        rate: { type: string }
        fees: { $ref: "#/components/schemas/Fees" }
        reference: { type: string }
        estimated_arrival: { type: [string, "null"], format: date-time }
        delivered_at: { type: [string, "null"], format: date-time }
        provider_reference: { type: [string, "null"] }
        failure: { type: [object, "null"], properties: { code: { type: string, enum: [invalid_destination, recipient_rejected_by_provider, limit_exceeded, provider_timeout, provider_declined, refunded_by_provider, claim_expired] }, message: { type: string } } }
        attempts: { type: integer }
        created_at: { type: string, format: date-time }
    DisbursementItemCreate:
      type: object
      required: [channel, source_amount]
      properties:
        recipient_id: { type: string }
        recipient: { $ref: "#/components/schemas/InlineRecipient" }
        channel: { type: string }
        source_amount: { $ref: "#/components/schemas/Amount" }
        reference: { type: string }
        memo: { type: string }
    DisbursementCreate:
      type: object
      required: [items]
      properties:
        name: { type: string }
        items: { type: array, maxItems: 5000, items: { $ref: "#/components/schemas/DisbursementItemCreate" } }
    DisbursementStatus: { type: string, enum: [draft, needs_review, ready, awaiting_approval, approved, changes_required, running, paused, correcting, completed, completed_with_failures, failed, canceled] }
    DisbursementItemStatus: { type: string, enum: [incomplete, needs_review, ready, running, retrying, delivering, succeeded, failed, canceled] }
    DisbursementItem:
      allOf:
        - { $ref: "#/components/schemas/DisbursementItemCreate" }
        - type: object
          properties:
            id: { type: string }
            status: { $ref: "#/components/schemas/DisbursementItemStatus" }
            review: { type: [object, "null"], properties: { field: { type: string }, message: { type: string } } }
            payout_id: { type: [string, "null"] }
            attempts: { type: integer }
    Disbursement:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        status: { $ref: "#/components/schemas/DisbursementStatus" }
        summary: { type: object, properties: { item_count: { type: integer }, ready: { type: integer }, needs_review: { type: integer }, total_source_amount: { $ref: "#/components/schemas/Amount" }, total_fees: { $ref: "#/components/schemas/Amount" }, by_type: { type: object, additionalProperties: { type: integer } }, warnings: { type: array, items: { type: object, properties: { code: { type: string }, item_ids: { type: array, items: { type: string } } } } } } }
        balance_check: { type: object, properties: { required: { $ref: "#/components/schemas/Amount" }, available: { $ref: "#/components/schemas/Amount" }, ok: { type: boolean } } }
        approval: { type: object, properties: { required: { type: integer }, received: { type: integer }, revision_id: { type: [string, "null"] } } }
        created_at: { type: string, format: date-time }
        launched_at: { type: [string, "null"], format: date-time }
        completed_at: { type: [string, "null"], format: date-time }
    PaymentLinkCreate:
      type: object
      properties:
        name: { type: string }
        methods: { type: array, items: { type: string, enum: [bank_transfer, card, crypto] } }
        published: { type: boolean }
    PaymentLink:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        url: { type: string, format: uri }
        methods: { type: array, items: { type: object, properties: { type: { type: string }, ready: { type: boolean } } } }
        published: { type: boolean }
    PaymentRequestCreate:
      type: object
      required: [payment_link_id, amount, currency]
      properties:
        payment_link_id: { type: string }
        amount: { $ref: "#/components/schemas/Amount" }
        currency: { type: string }
        reference: { type: string }
        expires_at: { type: string, format: date-time }
    PaymentRequest:
      allOf:
        - { $ref: "#/components/schemas/PaymentRequestCreate" }
        - type: object
          properties:
            id: { type: string }
            url: { type: string, format: uri }
            status: { type: string, enum: [open, paid, expired, canceled] }
            paid_at: { type: [string, "null"], format: date-time }
    VirtualAccount:
      type: object
      properties:
        id: { type: string }
        currency: { type: string }
        rail: { type: string, enum: [ach_wire, sepa, spei] }
        status: { type: string, enum: [pending, active, disabled] }
        details: { type: object, additionalProperties: true, description: "Masked bank details." }
        reference_required: { type: boolean }
    ApiKeyCreate:
      type: object
      required: [name]
      properties:
        name: { type: string }
        policy:
          type: object
          properties:
            daily_budget: { $ref: "#/components/schemas/Amount" }
            per_payout_max: { $ref: "#/components/schemas/Amount" }
            channels: { type: array, items: { type: string } }
            approval_threshold: { $ref: "#/components/schemas/Amount" }
    Event:
      type: object
      properties:
        id: { type: string }
        type: { type: string }
        created_at: { type: string, format: date-time }
        data: { type: object, properties: { object: { type: object } } }
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, enum: [validation_error, channel_unavailable, amount_out_of_range, unauthorized, verification_required, policy_exceeded, not_found, idempotency_conflict, approval_required, quote_expired, recipient_not_ready, rate_limited, provider_unavailable] }
            message: { type: string }
            param: { type: [string, "null"] }
            request_id: { type: string }
