Skip to main content
POST
/
payouts
/
quote
Create Payout Quote
curl --request POST \
  --url https://api.zopay.cash/connect/v1/payouts/quote \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "external_ref": "<string>",
  "destination": "<string>",
  "asset": "<string>",
  "amount": "<string>",
  "network_hint": "<string>"
}
'
{
  "quote_id": "<string>",
  "expires_at": "2023-11-07T05:31:56Z",
  "user_sends": "<string>",
  "recipient_gets": "<string>",
  "fee_total_usd": "<string>",
  "route": {
    "from_network": "<string>",
    "to_network": "<string>",
    "estimated_seconds": 1
  },
  "recommendation": {
    "message": "<string>",
    "alternative_route": {
      "network": "<string>",
      "fee_total_usd": "<string>",
      "savings_usd": "<string>"
    }
  },
  "insufficient_funds": {
    "needed": "<string>",
    "asset": "<string>",
    "network_with_lowest_top_up_fee": "<string>"
  }
}
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.

Authorizations

Authorization
string
header
required

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.

Headers

Idempotency-Key
string | null
authorization
string | null

Body

application/json

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.

external_ref
string
required

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.

Required string length: 1 - 255
destination
string
required

On-chain destination address. Must be a valid address for the asset's natural network OR for network_hint when set.

Required string length: 1 - 128
asset
string
required

Asset code to send (USDT, XAUT). Case-insensitive.

Required string length: 2 - 16
amount
string
required

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

Required string length: 1 - 64
network_hint
string | null

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.

Required string length: 2 - 16

Response

Successful Response

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.

quote_id
string
required

Opaque, signed, single-use. Expires at expires_at.

expires_at
string<date-time>
required

When the quote becomes invalid. Standard TTL is 60s from issuance.

user_sends
string
required

What gets debited from the partner-user (decimal string).

recipient_gets
string
required

What lands at destination after fees (decimal string).

fee_total_usd
string
required

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
ConnectPayoutRoute · object
required

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.

recommendation
ConnectPayoutRecommendation · object

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

insufficient_funds
ConnectPayoutInsufficientFunds · object

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.