Getting started¶
Base URL¶
https://routes.srcry.xyz/v1
Routes currently operates a single production environment. There is no sandbox/test environment at this time.
Authentication¶
All partner API requests require a bearer token:
Authorization: Bearer <api_key>
API keys are issued out-of-band per organization. For provisioning, rotation, or revocation, contact Routes support.
Rotation safety: request a replacement key, deploy and verify it in production traffic, then revoke the old key. Do not revoke your only active key before replacement validation.
Exceptions: public endpoints do not use bearer auth. POST /public/recover-funds, POST /public/complete-export, and GET /public/export-status use email OTP; POST /public/initiate-export starts the OTP flow.
Some write endpoints require an Idempotency-Key header (for example: POST /accounts, POST /accounts/{account_id}/deposit-addresses, POST /rfqs, POST /quotes/{quote_id}/accept, POST /zaps, POST /cancels, POST /recoveries/{recovery_id}/activate, and POST /accounts/{account_id}/export-keys). If you retry one of these writes, reuse the same key for the same payload.
Your API key authenticates your organization and grants access to all accounts, symbols, and webhooks within it. Multiple team members may hold API keys for the same organization — they share access to all resources.
Each end user is represented by an account (account_id) provisioned via POST /accounts. Credits, destinations, deposits, and all trading activity are scoped to an account. Pass the relevant account_id to scope operations to that end user.
In these docs, end user refers to the person your application serves. Webhooks for all accounts in your organization are delivered to a single endpoint, tagged with account_id.
Verify your API key by listing accounts:
curl https://routes.srcry.xyz/v1/accounts \
-H "Authorization: Bearer $ROUTES_API_KEY"
import httpx
response = httpx.get(
"https://routes.srcry.xyz/v1/accounts",
headers={
"Authorization": f"Bearer {api_key}",
},
)
const response = await fetch("https://routes.srcry.xyz/v1/accounts", {
headers: {
"Authorization": `Bearer ${apiKey}`,
},
});
You should receive a list of end-user accounts under your API key:
{
"request_id": "req_01J...",
"accounts": [
{
"account_id": "acct_01J...",
"label": "default",
"external_id": null,
"status": "active",
"created_at_ms": 1730000000000
}
]
}
Each end user has their own account_id. Pass the relevant account_id in every subsequent call to scope operations to that user. For this walkthrough, save one account_id to use in the examples below.
Key concepts¶
Credits — all trading is sourced from credits. A credit is a spendable balance created when a deposit is verified (chain finality reached and source address passes screening), when a trade settles, or as change from a partially-used credit. Each trade fully consumes its source credit. See deposit flow and credits.
Destinations — pre-registered delivery addresses for output assets. All trades and zaps reference a destination_id. Register destinations via the address book before trading.
Asset keys — canonical identifiers for assets across chains (native.btc, spl.solana:EPjFWdd5...). See Identifiers.
Symbols — register your own asset aliases (ETH, USDC, BTC-BASE) via PUT /symbols. Use them anywhere an asset key is accepted. See Identifiers.
Execute a trade¶
This walkthrough covers the RFQ flow — the standard path for swapping any two assets across chains with price control.
1. Register a destination¶
Register the address where the end user will receive the output asset. Destinations are scoped to an account and only need to be registered once per asset/address combination.
curl -X POST https://routes.srcry.xyz/v1/accounts/acct_01J.../destinations \
-H "Authorization: Bearer $ROUTES_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"asset_key": "native.btc",
"address": "bc1q...",
"label": "BTC hot wallet"
}'
import httpx
response = httpx.post(
"https://routes.srcry.xyz/v1/accounts/acct_01J.../destinations",
headers={
"Authorization": f"Bearer {api_key}",
},
json={
"asset_key": "native.btc",
"address": "bc1q...",
"label": "BTC hot wallet"
},
)
const response = await fetch("https://routes.srcry.xyz/v1/accounts/acct_01J.../destinations", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
asset_key: "native.btc",
address: "bc1q...",
label: "BTC hot wallet"
}),
});
Save the destination_id from the response:
{
"request_id": "req_01J...",
"destination": {
"destination_id": "dest_01J...",
"asset_key": "native.btc",
"address": "bc1q...",
"memo": null,
"tag": null,
"label": "BTC hot wallet"
}
}
2. Provision a deposit address¶
Provision a deposit address for the account. The end user (or your system on their behalf) sends funds to this address. Deposit addresses are ephemeral — Routes monitors them for incoming deposits of a specific asset for a bounded period.
If your integration applies a bounded local monitoring window for deposit addresses, treat deposits that arrive after that window as manual reconciliation exceptions.
curl -X POST https://routes.srcry.xyz/v1/accounts/acct_01J.../deposit-addresses \
-H "Authorization: Bearer $ROUTES_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-key-xyz" \
-d '{
"asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
}'
import httpx
response = httpx.post(
"https://routes.srcry.xyz/v1/accounts/acct_01J.../deposit-addresses",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": "unique-key-xyz",
},
json={
"asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
},
)
const response = await fetch("https://routes.srcry.xyz/v1/accounts/acct_01J.../deposit-addresses", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": "unique-key-xyz",
},
body: JSON.stringify({
asset_key: "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
}),
});
{
"request_id": "req_01J...",
"deposit_address": {
"deposit_address_id": "depaddr_01J...",
"asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"address": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"memo": null,
"tag": null
}
}
3. Deposit funds¶
The end user sends funds on-chain to the deposit address. Routes detects the transaction and runs chain confirmation and source address screening in parallel. When both resolve and the source passes screening, Routes creates a credit — a spendable balance scoped to the account. See Transaction Monitoring for details on deposit verification.
Wait for the deposit.verified webhook or poll credits:
curl https://routes.srcry.xyz/v1/accounts/acct_01J.../credits?status=open \
-H "Authorization: Bearer $ROUTES_API_KEY"
import httpx
response = httpx.get(
"https://routes.srcry.xyz/v1/accounts/acct_01J.../credits",
params={"status": "open"},
headers={
"Authorization": f"Bearer {api_key}",
},
)
const response = await fetch("https://routes.srcry.xyz/v1/accounts/acct_01J.../credits?status=open", {
headers: {
"Authorization": `Bearer ${apiKey}`,
},
});
When the deposit is verified, the account will have an open credit:
{
"request_id": "req_01J...",
"credits": [
{
"credit_id": "cred_01J...",
"asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"atoms": "5000000000",
"status": "open",
"source_type": "deposit"
}
]
}
Save the credit_id — you'll pass it to the RFQ.
4. Create an RFQ¶
Request quotes from solvers. Include the account's credit_id, destination_id, and the desired asset pair.
curl -X POST https://routes.srcry.xyz/v1/rfqs \
-H "Authorization: Bearer $ROUTES_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-key-123" \
-d '{
"account_id": "acct_01J...",
"credit_id": "cred_01J...",
"from_asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"to_asset_key": "native.btc",
"side": "exact_in",
"amount_in_atoms": "5000000000",
"mode": "firm",
"destination_id": "dest_01J..."
}'
import httpx
response = httpx.post(
"https://routes.srcry.xyz/v1/rfqs",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": "unique-key-123",
},
json={
"account_id": "acct_01J...",
"credit_id": "cred_01J...",
"from_asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"to_asset_key": "native.btc",
"side": "exact_in",
"amount_in_atoms": "5000000000",
"mode": "firm",
"destination_id": "dest_01J..."
},
)
const response = await fetch("https://routes.srcry.xyz/v1/rfqs", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": "unique-key-123",
},
body: JSON.stringify({
account_id: "acct_01J...",
credit_id: "cred_01J...",
from_asset_key: "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
to_asset_key: "native.btc",
side: "exact_in",
amount_in_atoms: "5000000000",
mode: "firm",
destination_id: "dest_01J..."
}),
});
If quotes are ready within wait_ms, you'll get 200 with the best quote:
{
"request_id": "req_01J...",
"rfq": {
"rfq_id": "rfq_01J...",
"status": "ready",
"best_quote_id": "qt_01J..."
}
}
If the RFQ returns 202 pending, wait poll_after_ms milliseconds and poll GET /rfqs/{rfq_id} until status is ready. See RFQs & Quotes for full details.
5. Accept the quote¶
Accept the best quote to begin execution. Set slippage_bps to define the acceptable slippage tolerance.
curl -X POST https://routes.srcry.xyz/v1/quotes/qt_01J.../accept \
-H "Authorization: Bearer $ROUTES_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-key-456" \
-d '{
"slippage_bps": "10"
}'
import httpx
response = httpx.post(
"https://routes.srcry.xyz/v1/quotes/qt_01J.../accept",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": "unique-key-456",
},
json={
"slippage_bps": "10"
},
)
const response = await fetch("https://routes.srcry.xyz/v1/quotes/qt_01J.../accept", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": "unique-key-456",
},
body: JSON.stringify({
slippage_bps: "10"
}),
});
{
"request_id": "req_01J...",
"trade": {
"trade_id": "trd_01J...",
"status": "executing",
"quote_id": "qt_01J...",
"poll_after_ms": 5000
}
}
6. Track the trade¶
Poll GET /trades/{trade_id} using poll_after_ms as your interval. The hint is route-aware — local swaps return shorter intervals, cross-chain routes return longer ones.
curl https://routes.srcry.xyz/v1/trades/trd_01J... \
-H "Authorization: Bearer $ROUTES_API_KEY"
import httpx
response = httpx.get(
"https://routes.srcry.xyz/v1/trades/trd_01J...",
headers={
"Authorization": f"Bearer {api_key}",
},
)
const response = await fetch("https://routes.srcry.xyz/v1/trades/trd_01J...", {
headers: {
"Authorization": `Bearer ${apiKey}`,
},
});
Terminal states:
settled— settlement confirmed on-chain; funds delivered to the registered destinationfailed— execution or settlement did not complete; source credit is not consumed
{
"request_id": "req_01J...",
"trade": {
"trade_id": "trd_01J...",
"status": "settled",
"settlement": {
"out_tx_hash": "0xabc123...",
"out_finalized": true,
"in_tx_hash": "0xdef456...",
"in_finalized": true
},
"fees": {
"platform_bps": "5",
"integrator_bps": "10",
"total_bps": "15",
"platform_atoms": "50000",
"integrator_atoms": "100000",
"total_atoms": "150000"
}
}
}
Use webhooks as the primary path for real-time notifications — subscribe to trade.settled and trade.failed. Use polling as a fallback.
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, or set it to 0 for no floor.
If expires_at_ms is omitted, a zap remains active until terminal state and can still be funded later (including hours later). Execution is evaluated when funds are verified, using then-current market conditions and your configured floor.
Create the zap, then have the end user send funds to the deposit address it returns:
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-789" \
-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-789",
},
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-789",
},
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..."
}),
});
The response includes a deposit address — send funds there to trigger execution:
{
"request_id": "req_01J...",
"zap": {
"zap_id": "zap_01J...",
"status": "awaiting_deposit",
"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. 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.
Once funds arrive and the deposit is verified, Routes evaluates the zap against the observed deposit (asset + amount) and either executes or fails with an explicit failure_code. Track it via GET /zaps/{zap_id} or subscribe to zap.settled / zap.failed webhooks. If you already have a credit_id from a previous deposit, pass it to skip the deposit step — execution begins immediately.
If your integration enables public failed-zap recovery, failed zaps that leave an open credit include a recovery_id + credit_id pair. The partner activates the record with POST /recoveries/{recovery_id}/activate, which sends an OTP to the end user's email. The end user then claims funds through POST /public/recover-funds with the OTP and a destination address. This requires an email on the account — see PATCH /accounts/{account_id}. If no email is set, the integrator can use POST /cancels to move the credit on the user's behalf.
See Zaps for the full lifecycle and failure handling.
Webhooks¶
Polling works, but webhooks are the recommended path for production. Register an endpoint and Routes delivers lifecycle events for all accounts in your organization to that single URL. Each event includes the account_id it relates to.
Key events to subscribe to:
deposit.verified— deposit reached chain finality and source address passed screening; credit createddeposit.flagged— deposit source failed screening; no credit created (see Transaction Monitoring)credit.created— a new spendable balance is available (from deposits, settlements, or change)trade.settled/zap.settled— execution confirmed on-chaintrade.failed/zap.failed— execution did not completerecovery.claimed— failed-zap credit was recovered via the public recovery flow (if enabled)
See Webhooks for signature verification, event types, and the reliability pattern.
Going to production¶
- Implement webhook signature verification and deduplicate events by
event_id - Route webhook events by
account_idto the correct end user in your system - Use
external_idon accounts to correlate with your own user identifiers - Always reuse
Idempotency-Keywhen retrying writes - Monitor settlement finality — poll until
out_finalizedandin_finalizedare bothtrue - Maintain an event feed reconciliation loop using
GET /v1/events - Build asset discovery/typeahead with
GET /assets?q=and store selectedasset_key; also pin and refresh your supported catalog viaGET /assets - Distinguish
filled(MM committed) fromsettled(on-chain confirmed) in your state tracking - Do not assume fixed execution latency. Use
poll_after_msand webhooks as the canonical timing model.
Non-custodial key export¶
Routes uses a 1-of-2 key model — the end user and Routes each hold a signing key, either of which can sign independently. This means end users can always export their private keys and take full control of their on-chain addresses.
There are two export paths:
Integrator-assisted — the integrator calls POST /accounts/{account_id}/export-keys with a target_public_key. Keys are encrypted to that public key and delivered via the account.key_exported webhook.
User-initiated (email OTP) — if the integrator attaches an email to the account via PATCH /accounts/{account_id}, the end user can initiate export independently via POST /public/initiate-export. Routes sends an OTP to the user's email, the user completes export via POST /public/complete-export, and retrieves encrypted keys from GET /public/export-status.
Once an email is attached, the integrator cannot prevent the user from exporting. This provides a strong non-custodial guarantee: even if the integrator disappears or refuses to cooperate, the end user retains sovereign access to their keys.
Export is irreversible — the account is permanently locked and a new account is required to continue using Routes.
See the API reference for the full endpoint contract, error codes, and rate limiting.