Headless Payments: One-Call Deposit-Based Checkout
If you’re building your own checkout — a mobile app, a partner platform, a custom storefront — the hosted /pay/{id} page isn’t always the right fit. You already have a wallet flow. You already know what chain and token your customer wants to pay with. What you actually want is one API call that hands back a deposit address and the exact amount to send.
That’s what POST /api/payments/headless is for.
The shape of it
One call in. One call out.
curl -X POST https://mutopay.com/api/payments/headless \
-H "X-API-Key: ep_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 25.00,
"currency": "USD",
"description": "Order #1042",
"external_id": "order_1042",
"src_token_symbol": "USDT",
"src_token_address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
"src_chain_id": "137",
"src_decimals": 6,
"src_address": "0xCustomerWallet..."
}'
{
"id": "pay_abc123...",
"status": "awaiting_payment",
"route_type": "bridge",
"protocol": "rubic",
"deposit": {
"address": "0xRubicDepositAddress...",
"chain_id": "137",
"chain_type": "evm",
"token_symbol": "USDT",
"token_address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
"token_decimals": 6,
"amount_raw": "25100000",
"amount_human": "25.10",
"payment_uri": "ethereum:0x...@137?value=25100000"
},
"bridge": {
"name": "Across",
"estimated_time_ms": 60000,
"src_usd": "25.10",
"bridge_fee_usd": "0.10"
},
"order_id": "rubic_exchange_id...",
"expires_at": "2026-04-14T15:30:00Z"
}
Your app now has everything it needs: tell the customer to send exactly amount_raw of the source token to address. Render a QR from payment_uri if you like. Poll GET /api/payments/{id}/status until terminal.
Two route types, one shape
- Direct — same token, same chain (customer pays USDT on Polygon, you settle USDT on Polygon). Deposit address is your wallet. No bridge fees, no waiting.
- Swap / bridge deposit — a cross-token or cross-chain route where the customer sends one transaction to an intake address and the settlement token arrives in your wallet. No order signing involved.
Routes that require the customer to sign a typed order or submit an on-chain swap transaction are not supported by this endpoint — if that’s the only route available, you get a 422 back. For those, use the hosted /pay/{id} page or the lower-level /quotes + /build-order + /submit-order trio.
What you still have to do
Two things:
1. Get the tx hash if you can. For swap/bridge deposits, you don’t need to do anything — the background monitor picks up the deposit and marks the payment complete. For direct routes, the monitor only verifies once it has a tx hash, so call POST /api/payments/{id}/confirm with { "tx_hash": "0x..." } after the customer broadcasts. If your wallet UI gives you the hash (most do), this is a one-liner.
2. Poll status. GET /api/payments/{id}/status returns the current state plus a tx hash once the payment lands. The webhook you configured on your channel fires the same events (payment.completed, payment.failed, etc.) — use whichever fits your architecture.
When to use which
| You’re building | Use |
|---|---|
| A WooCommerce / Shopify-style integration with a redirect | POST /api/payments → /pay/{id} |
| A custom checkout where you control the wallet flow | POST /api/payments/headless |
| Need to offer multiple swap routes and let users pick | POST /api/payments + /quotes + /build-order |
The headless endpoint is narrower by design. It picks the best deposit-capable route automatically, commits to it, and returns you a deposit address. If you need control beyond that — speed vs price tradeoffs, multiple route options, signed-order flows — the granular endpoints are still there.
Full reference in the API docs. Questions welcome.