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

# Create Payout Quote

> Mint a signed payout quote.

Mint a signed payout quote.

The response includes the priced route (native or bridge),
total USD fee, and — where applicable — a recommendation
pointing at a cheaper alternative the partner can suggest to
their user (the spec's "User could save \$24 by funding from
Solana" surface).

The `quote_id` is opaque and single-use; pass it to
`POST /payouts` within 60s to execute. Production HMAC-
signs it server-side; the sandbox derives it deterministically
from the request fingerprint so partner tests stay stable.

Idempotency: the quote endpoint is idempotency-required even
though it doesn't move money. Partners retrying after a
network blip should get the same quote, not a fresh one with
different pricing — that's a worse UX than the small extra
cost of replay storage.


## OpenAPI

````yaml POST /payouts/quote
openapi: 3.1.0
info:
  title: ZoPay Connect API
  description: >-
    B2B partner API for crypto custody, deposits, and payouts. Authenticate
    every request with a Bearer token (``sk_live_…`` for production,
    ``sk_test_…`` for sandbox).
  version: v1
servers:
  - url: https://api.zopay.cash/connect/v1
    description: Production
  - url: https://dev.api.zopay.cash/connect/v1
    description: Development
security:
  - BearerAuth: []
tags:
  - name: capabilities
    description: >-
      Static product catalog: which assets, networks, and fiat valuations this
      tenant can use. Poll on app startup to drive every UI surface (network
      pickers, fee warnings, fiat selection).
  - name: addresses
    description: >-
      Per-user deposit addresses. Mint with ``external_ref`` to attribute to one
      of your users; omit for a tenant-treasury (pooled) address.
  - name: payment-intents
    description: >-
      Single-use payment requests. Mint an intent for a specific amount +
      currency set; we expand it to per-network deposit addresses. Pass
      ``external_ref`` to attribute the intent to one of your users for
      downstream webhook reconciliation.
  - name: balances
    description: >-
      Per-user or tenant-wide balance snapshot with multi-fiat valuation (USD,
      PEN, EUR, MXN, ARS, COP, DOP).
  - name: transactions
    description: >-
      Unified ledger covering deposits, payouts, internal transfers, and (after
      Phase 4) conversions and ramps.
  - name: payouts
    description: >-
      Outbound money movement: get a signed quote, then execute it. Quote IDs
      are single-use and expire in 60s.
  - name: conversions
    description: '[Coming soon] Crypto-to-crypto conversions.'
  - name: ramps
    description: '[Coming soon] Fiat <-> crypto hosted-checkout sessions.'
  - name: webhooks
    description: '[Coming soon] Subscribe to event streams + replay past events.'
  - name: health
    description: >-
      Auth + connectivity probe. Use to validate your API key works before
      depending on a priced endpoint.
  - name: test-helpers
    description: >-
      Sandbox-only simulators. Let you exercise webhook handlers and downstream
      state transitions without real testnet activity. Routes return 404 to
      ``sk_live_…`` keys.
paths:
  /payouts/quote:
    post:
      tags:
        - payouts
      summary: Create Payout Quote
      description: |-
        Mint a signed payout quote.

        The response includes the priced route (native or bridge),
        total USD fee, and — where applicable — a recommendation
        pointing at a cheaper alternative the partner can suggest to
        their user (the spec's "User could save $24 by funding from
        Solana" surface).

        The ``quote_id`` is opaque and single-use; pass it to
        ``POST /payouts`` within 60s to execute. Production HMAC-
        signs it server-side; the sandbox derives it deterministically
        from the request fingerprint so partner tests stay stable.

        Idempotency: the quote endpoint is idempotency-required even
        though it doesn't move money. Partners retrying after a
        network blip should get the same quote, not a fresh one with
        different pricing — that's a worse UX than the small extra
        cost of replay storage.
      operationId: create_payout_quote_payouts_quote_post
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            title: Idempotency-Key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectPayoutQuoteRequest'
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectPayoutQuoteResponse'
        '400':
          description: >-
            Invalid request. The body or query parameters failed validation.
            ``error.code`` names the specific failure (e.g. ``invalid_request``,
            ``catalog_invalid``, ``min_usdt_amount_requires_solana``);
            ``error.param`` names the offending field when applicable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectErrorEnvelope'
        '401':
          description: >-
            Authentication failed. ``error.code`` distinguishes
            ``authentication_required`` (missing / malformed / unknown key) from
            ``expired_credential`` (key matched but past its ``expires_at``).
            Rotate the key in the admin panel for the latter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectErrorEnvelope'
        '403':
          description: >-
            The key is valid but lacks permission for this endpoint.
            ``error.code`` is ``forbidden_scope`` when the key is missing a
            scope; ``account_pending_approval`` when the org is not yet active
            for Connect.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectErrorEnvelope'
        '404':
          description: >-
            Resource not found in this tenant. Same envelope shape is returned
            for genuinely-missing IDs and for cross-tenant accesses -- existence
            under other tenants is not leakable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectErrorEnvelope'
        '409':
          description: >-
            Conflict. Most commonly ``idempotency_conflict`` -- the same
            ``Idempotency-Key`` was reused with a different body. Generate a
            fresh key or replay the original body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectErrorEnvelope'
        '422':
          description: >-
            Validation failed. Runtime translates FastAPI / Pydantic validation
            errors into the same Connect envelope you see on 400s; this status
            is emitted when the request shape is structurally wrong (missing
            required field, wrong type).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectErrorEnvelope'
        '429':
          description: >-
            Rate limited. Back off and retry. ``error.code`` is
            ``rate_limited``; per-key throughput is documented via
            ``rate_limit_rps`` on the API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectErrorEnvelope'
        '500':
          description: >-
            Internal server error. ``error.code`` is ``internal_error``. The
            request can usually be safely retried with the same
            ``Idempotency-Key``.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectErrorEnvelope'
        '501':
          description: >-
            Endpoint is declared in the router but its implementation has not
            shipped yet (``error.code = not_yet_available``). Pinned in the
            schema so SDK generators emit the endpoint with a typed error rather
            than skipping it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConnectErrorEnvelope'
      security:
        - BearerAuth: []
components:
  schemas:
    ConnectPayoutQuoteRequest:
      properties:
        external_ref:
          type: string
          maxLength: 255
          minLength: 1
          title: External Ref
          description: >-
            The partner-user whose balance funds the payout. Must already exist
            (have been seen by some prior Connect call). Unknown refs → 404
            ``external_ref_unknown``.
        destination:
          type: string
          maxLength: 128
          minLength: 1
          title: Destination
          description: >-
            On-chain destination address. Must be a valid address for the
            asset's natural network OR for ``network_hint`` when set.
        asset:
          type: string
          maxLength: 16
          minLength: 2
          title: Asset
          description: Asset code to send (USDT, XAUT). Case-insensitive.
        amount:
          type: string
          maxLength: 64
          minLength: 1
          title: Amount
          description: >-
            Amount of ``asset`` to send, in the asset's user-facing unit, as a
            decimal string. The recipient receives ``amount`` minus
            ``fee_total_usd`` (rendered as ``recipient_gets`` in the response).
        network_hint:
          anyOf:
            - type: string
              maxLength: 16
              minLength: 2
            - type: 'null'
          title: Network Hint
          description: >-
            Optional destination-chain hint when the address shape is ambiguous
            (e.g. EVM addresses are valid on ethereum / polygon / arbitrum /
            base / avalanche / optimism — without this we'd guess). For native
            addresses (solana, tron, bitcoin) the chain is unambiguous from the
            address shape and this field is ignored.
      additionalProperties: false
      type: object
      required:
        - external_ref
        - destination
        - asset
        - amount
      title: ConnectPayoutQuoteRequest
      description: |-
        Request body for POST /payouts/quote.

        The asset + amount is what the user wants to *send*. We
        determine routing (native vs bridge) by combining the asset
        with the destination address's chain (auto-detected from the
        address shape, or hinted via ``network_hint``).

        Notably absent: a separate ``from_network`` field. The partner
        doesn't specify source — they specify the *asset* and we pick
        the cheapest source chain that asset is available on. If that
        yields a different chain than the destination (cross-chain),
        we route through the bridge automatically.

        Why ``external_ref`` is required here (vs. optional on
        ``/addresses``): payouts always debit a specific end-user's
        balance. There's no "treasury payout" mode in Phase 3 — that
        would require an org-level balance the partner has explicitly
        funded, which isn't on the roadmap.
    ConnectPayoutQuoteResponse:
      properties:
        quote_id:
          type: string
          title: Quote Id
          description: Opaque, signed, single-use. Expires at ``expires_at``.
        expires_at:
          type: string
          format: date-time
          title: Expires At
          description: When the quote becomes invalid. Standard TTL is 60s from issuance.
        user_sends:
          type: string
          title: User Sends
          description: What gets debited from the partner-user (decimal string).
        recipient_gets:
          type: string
          title: Recipient Gets
          description: What lands at ``destination`` after fees (decimal string).
        fee_total_usd:
          type: string
          title: Fee Total Usd
          description: >-
            Sum of network + provider + Zopay fees for this route, in USD as a
            decimal string. Always rendered in USD regardless of the asset
            because partners use this to compare alternative routes.
        route:
          $ref: '#/components/schemas/ConnectPayoutRoute'
        recommendation:
          anyOf:
            - $ref: '#/components/schemas/ConnectPayoutRecommendation'
            - type: 'null'
        insufficient_funds:
          anyOf:
            - $ref: '#/components/schemas/ConnectPayoutInsufficientFunds'
            - type: 'null'
      type: object
      required:
        - quote_id
        - expires_at
        - user_sends
        - recipient_gets
        - fee_total_usd
        - route
      title: ConnectPayoutQuoteResponse
      description: |-
        Response body for POST /payouts/quote.

        The quote is signed server-side (HMAC-SHA256 + 60s TTL +
        single-use) — partners treat ``quote_id`` as an opaque bearer
        string. Forging or tampering raises at consume time
        (POST /payouts), not at quote read.
    ConnectErrorEnvelope:
      properties:
        error:
          $ref: '#/components/schemas/ConnectErrorBody'
          description: >-
            Error details. Always present on a non-2xx response from
            /connect/v1.
      type: object
      required:
        - error
      title: ConnectErrorEnvelope
      description: |-
        Top-level wrapper for every /connect/v1 failure response.

        Wire shape::

            {"error": {"type": ..., "code": ..., "message": ..., ...}}

        The wrapper is intentional: it keeps error bodies syntactically
        distinct from success bodies, so polymorphic partner code paths
        that read the same JSON for both states can branch on the
        presence of the ``error`` key.
    ConnectPayoutRoute:
      properties:
        type:
          type: string
          enum:
            - native
            - bridge
          title: Type
        from_network:
          type: string
          title: From Network
          description: >-
            Lowercase network code we'll *send from* (where the user's funds are
            held). For native payouts equals ``to_network``.
        to_network:
          type: string
          title: To Network
          description: Lowercase network code where the recipient receives.
        estimated_seconds:
          type: integer
          minimum: 0
          title: Estimated Seconds
          description: >-
            Expected time from execute to delivery. Native is tens of seconds;
            bridge is 60-180s depending on destination chain finality.
      type: object
      required:
        - type
        - from_network
        - to_network
        - estimated_seconds
      title: ConnectPayoutRoute
      description: |-
        Routing summary baked into every quote.

        Partners surface this to their user as "via Solana → Tron,
        ~90s" so the user understands the path before confirming.
    ConnectPayoutRecommendation:
      properties:
        message:
          type: string
          title: Message
          description: Human-readable string the partner can show verbatim.
        alternative_route:
          $ref: '#/components/schemas/ConnectPayoutAlternativeRoute'
      type: object
      required:
        - message
        - alternative_route
      title: ConnectPayoutRecommendation
      description: |-
        Optional nudge surfaced when a cheaper / faster route
        exists for the same user. Absent on the quote response when
        no recommendation is actionable (e.g. the requested route is
        already the cheapest).
    ConnectPayoutInsufficientFunds:
      properties:
        needed:
          type: string
          title: Needed
          description: Additional amount needed to cover the payout.
        asset:
          type: string
          title: Asset
          description: Asset code (uppercase).
        network_with_lowest_top_up_fee:
          anyOf:
            - type: string
            - type: 'null'
          title: Network With Lowest Top Up Fee
          description: >-
            If the user could top up via a cheaper network, the code of that
            network. Null when no top-up is cheap enough to matter.
      type: object
      required:
        - needed
        - asset
      title: ConnectPayoutInsufficientFunds
      description: |-
        Returned inside the quote body (not as an error) when the
        user lacks the balance to cover this payout.

        Partners can render "you need $50 more USDT" without parsing
        an error; the quote_id is still issued (consumable to confirm
        intent) but the partner is expected NOT to call POST /payouts
        in this state. If they do, the execute call 422s with
        ``insufficient_funds``.
    ConnectErrorBody:
      properties:
        type:
          type: string
          title: Type
          description: >-
            Stripe-style error category. One of: ``invalid_request_error``,
            ``authentication_error``, ``not_found_error``, ``rate_limit_error``,
            ``idempotency_error``, ``api_error``. Stable across minor versions;
            branch on this for top-level error routing.
          examples:
            - invalid_request_error
        code:
          type: string
          title: Code
          description: >-
            Machine-stable identifier for the specific failure. More specific
            than ``type``. Examples: ``invalid_request``,
            ``authentication_required``, ``expired_credential``,
            ``forbidden_scope``, ``account_pending_approval``,
            ``resource_not_found``, ``idempotency_conflict``, ``rate_limited``,
            ``internal_error``, ``not_yet_available``. Match on ``code`` in your
            client's error handler -- never parse ``message``.
          examples:
            - authentication_required
        message:
          type: string
          title: Message
          description: >-
            Human-readable explanation. Safe to surface in your own UI but may
            be reworded between releases -- never branch on the string.
          examples:
            - >-
              Missing or malformed Authorization header. Send: Authorization:
              Bearer sk_live_... or sk_test_...
        doc_url:
          type: string
          title: Doc Url
          description: >-
            Link to docs explaining this ``code``. Constructed as
            ``https://docs.zopay.cash/errors/<code>``. Surface this in support
            tickets so we can investigate without a round-trip.
          examples:
            - https://docs.zopay.cash/errors/authentication_required
        param:
          anyOf:
            - type: string
            - type: 'null'
          title: Param
          description: >-
            Names the offending input field when the error is
            input-shape-related (mostly ``invalid_request_error`` from 400 and
            422). Absent on auth, scope, rate-limit, and internal errors.
          examples:
            - currencies
      type: object
      required:
        - type
        - code
        - message
        - doc_url
      title: ConnectErrorBody
      description: |-
        The ``error`` object inside every /connect/v1 failure response.

        Mirrors ``ConnectError.to_envelope()`` in
        ``app/api/connect/errors.py``. Field set is locked -- adding a
        new field is a partner-visible contract change and requires a
        coordinated docs + schema update.
    ConnectPayoutAlternativeRoute:
      properties:
        type:
          type: string
          enum:
            - native
            - bridge
          title: Type
        network:
          type: string
          title: Network
          description: Lowercase network code.
        fee_total_usd:
          type: string
          title: Fee Total Usd
          description: What this alternative would cost (decimal string).
        savings_usd:
          type: string
          title: Savings Usd
          description: >-
            How much cheaper than the primary quote, as a decimal string.
            Positive means the alternative is cheaper.
      type: object
      required:
        - type
        - network
        - fee_total_usd
        - savings_usd
      title: ConnectPayoutAlternativeRoute
      description: |-
        A cheaper or faster route the partner could nudge their
        user toward. Used inside ``recommendation``.

        The spec example shows ``"User could save $24 by funding from
        Solana instead"`` — this is the structural form of that.
        Partners surface the message + savings; the alternative_route
        itself is informational (the partner cannot directly choose
        it from this quote; they re-quote with the alternative
        source).
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Connect API key (sk_live_… or sk_test_…)
      description: >-
        Paste your Connect API key (sk_live_… for production, sk_test_… for
        sandbox) without the ``Bearer `` prefix. Mint and rotate keys from the
        admin panel.

````