Zaps¶
Zaps are the simplest integration option. They facilitate market and limit orders across nearly every digital asset with a few API calls. Trades terminate at the end user's registered destination — either a deposit address at a partner custodial platform or a self-custody address.
There is no quote/accept cycle. Market makers compete in an auction and the best is auto-selected. Set min_amount_out_atoms or min_amount_out_ui as a price floor. Set the floor to 0 for pure market behavior. If no market maker can meet your configured floor, the zap fails and funds remain as an open credit.
You may optionally provide expires_at_ms to bound how long the zap remains eligible for execution. If omitted, the zap has no API-level expiry and remains active until it reaches a terminal state (settled or failed). In this default mode, the zap can still be funded later (including hours later) and will be evaluated against then-current market conditions plus your configured floor.
The flow:
- Register a destination for the output asset
- Create a zap — Routes returns a deposit address
- Send funds to the deposit address (or provide a
credit_idfrom an existing deposit) - Routes executes the swap and delivers the output to the registered destination
Deposit-funded matching behavior (credit_id omitted)¶
For zaps created without credit_id, Routes returns a deposit address and executes only after a deposit is verified.
- If the deposited asset matches
from_asset_key/from_symbol, the zap executes using the observed deposited amount (not necessarily the originally requested amount). - If the deposited amount is smaller or larger than requested, output adjusts to market pricing and fees for the observed input amount.
- If the observed amount cannot be executed (for example outside current executable size bounds or below your floor), the zap fails with existing zap failure codes.
- If the deposited asset does not match
from_asset_key/from_symbol, the zap fails withfailure_code=from_asset_mismatch. The deposit is still processed normally for the deposited asset and a standard deposit credit is created. - Zap deposit addresses are single-use. In your end-user UX and communications, instruct customers to send only one deposit per zap address. If additional deposits do arrive, Routes makes a best effort to auto-convert and auto-forward, but outcome is not guaranteed under the original zap terms.
For reconciliation:
amount_in_atoms/amount_in_uion create request is the requested amount.- Execution uses the observed deposited amount once deposit verification completes.
- On deposit-funded zaps, response payloads include
requested_amount_in_atomsand (after verification)observed_amount_in_atoms.
Public recovery integration (optional)¶
For integrations that cannot surface arbitrary assets in product UX, Routes supports a public recovery claim flow using email OTP.
- When recovery is enabled for your organization, failed zaps that leave an open credit include
recovery_idandcredit_idon the failed zap payload. - Integrator activates recovery using
POST /recoveries/{recovery_id}/activate. Routes sends an OTP to the end user's email. - The end user claims the credit using
POST /public/recover-fundswith the OTP and a destination address. - Requires an email on the account — set via
PATCH /accounts/{account_id}. - If no email is configured, the integrator can use
POST /cancelsto move the credit on the user's behalf.
Discoverability:
- If eligible failed zaps never include
recovery_id+credit_id, recovery is not enabled for your organization.
POST /zaps¶
Use this to create a zap.
Auth:
- Bearer token required
Idempotency-Key(required)
Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
account_id |
string | Yes | Account identifier |
from_asset_key |
string | One of from_asset_key or from_symbol required |
Source asset by key |
from_symbol |
string | One of from_asset_key or from_symbol required |
Source asset by partner symbol |
to_asset_key |
string | One of to_asset_key or to_symbol required |
Destination asset by key |
to_symbol |
string | One of to_asset_key or to_symbol required |
Destination asset by partner symbol |
amount_in_atoms |
string | One of amount_in_atoms or amount_in_ui required |
Input amount in atomic units |
amount_in_ui |
string | One of amount_in_atoms or amount_in_ui required |
Input amount in UI units. Enforce decimal precision from asset decimals value |
min_amount_out_atoms |
string | One of min_amount_out_atoms or min_amount_out_ui required |
Minimum acceptable output in atoms. Set "0" for no floor (pure market behavior). |
min_amount_out_ui |
string | One of min_amount_out_atoms or min_amount_out_ui required |
Minimum acceptable output in UI units. Set "0" for no floor (pure market behavior). |
destination_id |
string | Yes | Pre-registered destination for the output asset |
credit_id |
string | No | Credit to source funds from. If omitted, the zap enters awaiting_deposit and returns a deposit address. |
expires_at_ms |
integer | No | Optional zap expiry timestamp (milliseconds since epoch). If reached before execution begins, zap fails with failure_code=zap_expired. |
client_zap_id |
string | No | Client-provided zap identifier for correlation |
integrator_fee_bps |
string | No | Integrator markup in basis points (supports decimals, e.g. "2.5"), added on top of platform fee. Overrides organization default. See Fees. |
side is always exact_in for zaps — it is not a parameter.
Validation rules:
- Exactly one of
from_asset_keyorfrom_symbolmust be provided. - Exactly one of
to_asset_keyorto_symbolmust be provided. - Exactly one of
amount_in_atomsoramount_in_uimust be provided. - Exactly one of
min_amount_out_atomsormin_amount_out_uimust be provided. - Providing both fields in a one-of pair returns
400 invalid_parameter.
HTTP response behavior:
200zap accepted; response includes azapresource inexecutingorawaiting_deposit.400validation error (parameter_missing,invalid_parameter,invalid_enum,ui_precision_exceeded).401missing or invalid bearer auth.403policy block (for exampleforbidden_asset,destination_suspended).404unknown resource (for exampleasset_not_supported).409idempotency conflict (idempotency_key_reuse).429rate limited.
Error cases:
| Code | HTTP | Description |
|---|---|---|
parameter_missing |
400 | Required field missing |
invalid_parameter |
400 | Malformed or conflicting fields |
invalid_enum |
400 | Enum field has unsupported value |
ui_precision_exceeded |
400 | *_ui precision exceeds asset decimals |
invalid_asset_key |
400 | asset_key format is invalid |
invalid_symbol |
400 | Symbol format is invalid |
forbidden_asset |
403 | Asset not permitted for your organization |
asset_not_supported |
404 | Asset is not supported by Routes |
destination_suspended |
403 | Destination is suspended by screening |
idempotency_key_reuse |
409 | Same key reused with different payload |
rate_limited |
429 | Request throttled |
Example request:
curl -X POST https://routes.srcry.xyz/v1/zaps \
-H "Authorization: Bearer $ROUTES_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-key-123" \
-d '{
"account_id": "acct_01J...",
"from_asset_key": "native.btc",
"to_asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount_in_atoms": "100000",
"min_amount_out_atoms": "5000000",
"destination_id": "dest_01J..."
}'
import httpx
response = httpx.post(
"https://routes.srcry.xyz/v1/zaps",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": "unique-key-123",
},
json={
"account_id": "acct_01J...",
"from_asset_key": "native.btc",
"to_asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount_in_atoms": "100000",
"min_amount_out_atoms": "5000000",
"destination_id": "dest_01J..."
},
)
const response = await fetch("https://routes.srcry.xyz/v1/zaps", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": "unique-key-123",
},
body: JSON.stringify({
account_id: "acct_01J...",
from_asset_key: "native.btc",
to_asset_key: "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
amount_in_atoms: "100000",
min_amount_out_atoms: "5000000",
destination_id: "dest_01J..."
}),
});
{
"account_id": "acct_01J...",
"from_asset_key": "native.btc",
"to_asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount_in_atoms": "100000",
"min_amount_out_atoms": "5000000",
"destination_id": "dest_01J..."
}
Execution behavior after create:
- If
credit_idis provided (pre-deposited funds): zap begins executing immediately - If no
credit_id: response includes a deposit address. After deposit verification: - matching deposit asset -> zap executes using observed deposited amount
- non-matching deposit asset -> zap fails with
from_asset_mismatch; deposit credit is still created for the deposited asset - If
expires_at_msis provided and reached before execution begins: zap fails withfailure_code=zap_expired
Example response (with credit_id):
{
"request_id": "req_01J...",
"zap": {
"zap_id": "zap_01J...",
"status": "executing",
"client_zap_id": null,
"requested_amount_in_atoms": "100000",
"poll_after_ms": 5000
}
}
Example response (without credit_id):
{
"request_id": "req_01J...",
"zap": {
"zap_id": "zap_01J...",
"status": "awaiting_deposit",
"requested_amount_in_atoms": "100000",
"deposit_address": {
"deposit_address_id": "depaddr_01J...",
"asset_key": "native.btc",
"address": "bc1q...",
"memo": null,
"tag": null
},
"poll_after_ms": 15000
}
}
Zap deposit addresses are single-use. Do not reuse a deposit address from a previous zap — always use the address returned by the current POST /zaps response.
Execution and settlement timing vary by route and chain conditions. There is no fixed latency SLA. Use poll_after_ms and webhooks as the canonical timing model.
Response fields¶
| Field | Type | Description |
|---|---|---|
request_id |
string | Request identifier |
zap.zap_id |
string | Zap identifier |
zap.status |
string | Current zap status |
zap.client_zap_id |
string | Client-provided identifier |
zap.expires_at_ms |
integer | Zap expiry timestamp when provided |
zap.requested_amount_in_atoms |
string | Requested zap input amount from create request. Present for all zap resources. |
zap.observed_amount_in_atoms |
string | Observed deposited amount used for execution. Present only after deposit verification for deposit-funded zaps; absent for credit-funded zaps and pre-verification awaiting_deposit states. |
zap.deposit_address |
object | Deposit address details (when awaiting deposit) |
zap.failure_code |
string | Failure reason (terminal only): min_amount_out_not_met, execution_failed, settlement_failed, settlement_timeout, settlement_orphaned, deposit_insufficient_confirmations, deposit_flagged, from_asset_mismatch, zap_expired |
zap.recovery.recovery_id |
string | Present when recovery is enabled and failed zap leaves an open credit |
zap.recovery.credit_id |
string | Present when recovery is enabled and failed zap leaves an open credit |
zap.settlement.out_tx_hash |
string | Transaction hash for account → MM leg |
zap.settlement.out_confirmations |
integer | Current confirmation count for out leg |
zap.settlement.out_finalized |
boolean | Whether out leg has reached chain finality |
zap.settlement.in_tx_hash |
string | Transaction hash for MM → account leg |
zap.settlement.in_confirmations |
integer | Current confirmation count for in leg |
zap.settlement.in_finalized |
boolean | Whether in leg has reached chain finality |
zap.fees.platform_bps |
string | Platform fee in basis points |
zap.fees.integrator_bps |
string | Integrator fee in basis points |
zap.fees.total_bps |
string | Combined fee in basis points |
zap.fees.platform_atoms |
string | Platform fee in output asset atoms |
zap.fees.integrator_atoms |
string | Integrator fee in output asset atoms |
zap.fees.total_atoms |
string | Total fee in output asset atoms |
zap.poll_after_ms |
integer | Milliseconds until next poll |
GET /zaps¶
Use this to list zaps. Returns zaps across all accounts in your organization, ordered by creation time descending.
Auth:
- Bearer token required
HTTP response behavior:
200list response.401missing or invalid bearer auth.429rate limited.
Error cases:
| Code | HTTP | Description |
|---|---|---|
invalid_enum |
400 | status filter is not a supported value |
rate_limited |
429 | Request throttled |
Query parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
account_id |
string | No | Filter to zaps for a specific account |
status |
string | No | Filter by status: awaiting_deposit, executing, filled, pending_settle, settled, failed |
limit |
integer | No | Maximum zaps to return (default 50, max 100) |
cursor |
string | No | Pagination cursor from previous response |
Example response:
{
"request_id": "req_01J...",
"zaps": [
{
"zap_id": "zap_01J...",
"account_id": "acct_01J...",
"status": "settled",
"client_zap_id": null,
"requested_amount_in_atoms": "100000",
"from_asset_key": "native.btc",
"to_asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"created_at_ms": 1730000020000
},
{
"zap_id": "zap_01J...",
"account_id": "acct_01J...",
"status": "awaiting_deposit",
"client_zap_id": "myzap_789",
"requested_amount_in_atoms": "50000",
"from_asset_key": "native.btc",
"to_asset_key": "erc20.evm:1_0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"created_at_ms": 1730000010000
}
],
"has_more": false,
"next_cursor": null
}
GET /zaps/{zap_id}¶
Use this to track zap execution. The polling and webhook patterns are the same as for trades.
Auth:
- Bearer token required
HTTP response behavior:
200resource response.401missing or invalid bearer auth.404zap not found (or not in your organization).429rate limited.
Error cases:
| Code | HTTP | Description |
|---|---|---|
rate_limited |
429 | Request throttled |
Non-terminal states:
awaiting_deposit— waiting for funds to arrive at deposit addressexecuting— solver/MM is executing the swapfilled— MM has committed to filling the order; funds credited as pending in ledgerpending_settle— MM has initiated on-chain settlement
Terminal states:
settled— settlement confirmed on-chainfailed— execution or settlement did not complete
On failure, the failure_code field indicates the reason. Zap failures should be rare. When a zap fails, the source credit is not consumed — retry with a new zap, fall back to an RFQ-based trade, or cancel the credit.
Recoverable — retry automatically:
min_amount_out_not_met— no solver could meet the price floor. Retry with a widermin_amount_out_atomsormin_amount_out_ui(including0for no floor), or wait for better market conditions.execution_failed— swap execution did not complete. Retry the zap.deposit_insufficient_confirmations— counterparty requires more confirmations for this deposit. Wait for additional confirmations and retry.zap_expired— caller-provided zap expiry was reached before execution began. Create a new zap with a new expiry (or omit expiry).
Requires user/integrator action:
from_asset_mismatch— deposit asset does not match the zap's requested source asset. The zap does not execute. The deposit is still credited under the deposited asset. If recovery is enabled, failed payloads includerecovery_id+credit_id; activate withPOST /recoveries/{recovery_id}/activate(sends OTP to user's email), then end user claims viaPOST /public/recover-funds. If no email is set, integrator usesPOST /cancels. If recovery is not enabled, create a new zap using the deposited asset or cancel that credit.
Non-recoverable — escalate for manual review:
settlement_failed— settlement transaction failed on-chainsettlement_timeout— settlement not confirmed within expected windowsettlement_orphaned— a previously-confirmed settlement tx was dropped (reorg)deposit_flagged— the deposit's source address failed transaction monitoring screening. No credit was created. See Transaction Monitoring — Flagged deposit recovery.
Zap lifecycle:
┌───────────────────┐
│ awaiting_deposit │
└─────────┬─────────┘
│
┌───────▼───────┐
│ executing │
└───────┬───────┘
│
┌───────▼───────┐
│ filled │
└───────┬───────┘
│
┌─────────▼──────────┐
│ pending_settle │
└────┬────────────┬──┘
│ │
┌────▼─────┐ ┌────▼────┐
│ settled │ │ failed │
└──────────┘ └─────────┘
Note: failed is reachable from any non-terminal state.
Non-terminal example:
{
"request_id": "req_01J...",
"zap": {
"zap_id": "zap_01J...",
"status": "pending_settle",
"poll_after_ms": 15000
}
}
Deposit-funded example (after deposit verification):
{
"request_id": "req_01J...",
"zap": {
"zap_id": "zap_01J...",
"status": "executing",
"requested_amount_in_atoms": "100000",
"observed_amount_in_atoms": "98500",
"poll_after_ms": 5000
}
}
Terminal example (settled):
{
"request_id": "req_01J...",
"zap": {
"zap_id": "zap_01J...",
"status": "settled",
"settlement": {
"out_tx_hash": "0xabc123...",
"out_confirmations": 64,
"out_finalized": true,
"in_tx_hash": "0xdef456...",
"in_confirmations": 42,
"in_finalized": true
},
"fees": {
"platform_bps": "5",
"integrator_bps": "10",
"total_bps": "15",
"platform_atoms": "50000",
"integrator_atoms": "100000",
"total_atoms": "150000"
}
}
}
Terminal example (failed):
{
"request_id": "req_01J...",
"zap": {
"zap_id": "zap_01J...",
"status": "failed",
"failure_code": "from_asset_mismatch",
"recovery": {
"recovery_id": "rcv_01J...",
"credit_id": "cred_01J..."
}
}
}