Overview
The Cleave API gives your server a quote and a ready transaction. Your user signs and sends it from their own wallet.
What you can do:
- Same-chain swaps on 6 chains: Ethereum, Optimism, BNB Chain, Polygon, Base and Arbitrum.
- Wrap and unwrap the native coin on each chain.
- Cross-chain transfers between any two of these chains: USDC to USDC, token to USDC, and token to token.
- Your own referral fee on each quote, paid on-chain to your address.
- USD estimates on every quote, and token prices on request.
The flow is quote → execute:
GET /partner/v1/quotewith the tokens, amount, recipient and slippage.- Read the amounts, fees and notices. Show them to your user.
- If needed, the user approves the token (
execution.approval). - The user signs and sends
txas returned. - Cross-chain: track the transfer with
rails[].tracking.
Only fast routes are served. If no fast route exists for a transfer, the API answers no_fast_route.
Base URL
https://api.cleave.fiAll endpoints live under /partner/v1/. Every answer is JSON and carries apiVersion: "1".
Auth and keys
Get a key
Click Request API key and tell us about your integration. We issue the key to you directly. Keep it on your servers only.
Send the key
Send it on every request:
Authorization: Bearer cq_live_<your key>- Keys look like
cq_live_followed by 43 characters (A-Z a-z 0-9 _ -). Beareris case-insensitive. There is no other auth method.- Several keys of one partner share one rate limit.
Server-to-server only
- Requests that carry an
Originheader are refused (403forbidden_origin). Browsers send it on cross-origin requests, so a key can't be used from a web page. - If your web app calls your backend, strip
Originbefore forwarding. - No CORS headers are sent.
OPTIONSanswers 405.
Optional IP allowlist
We can lock a key to up to 16 IPv4/IPv6 addresses or CIDR ranges. Other addresses get 403 forbidden_ip. Skip it if your egress IP changes (serverless, shared proxies).
Keep the key safe
- Store it in a secret store. Never ship it in an app or a repo.
- Do not log full request headers.
- Lost or leaked: ask us to revoke it and issue a new one.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET, HEAD | /partner/v1/quote | Swap, wrap/unwrap and cross-chain quotes with a ready tx |
| GET, HEAD | /partner/v1/meta | Chains, contracts, lanes, fee caps, your key's limits |
| GET, HEAD | /partner/v1/tokens | Default token list of a chain |
| GET, HEAD | /partner/v1/tokens/search | Search the full token list |
| GET, HEAD | /partner/v1/tokens/lookup | One token: list entry and on-chain details |
| GET, HEAD | /partner/v1/prices | USD prices of up to 4 tokens |
| GET, HEAD | /partner/v1/health | Service health per chain |
| POST | /partner/v1/report | Report a problem with a quote or transaction |
HEADreturns the headers of theGETanswer, without a body.- Every answer has
X-Request-Id; JSON bodies repeat it asrequestId. Log it and quote it when you contact us. - Unknown or repeated query parameters are a 400.
- URLs are limited to 2,048 bytes.
Conventions
- Amounts are decimal strings in base units (wei, or 1e-6 for most USDC). Parse them with a big-integer type.
- Addresses:
0x+ 40 hex, any case in requests; checksummed in answers. - Native coin (ETH, BNB, POL): the sentinel
0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE. - "USDC" is the chain's USDC from meta
chains[].usdc. It has 18 decimals on BNB Chain and 6 elsewhere: readchains[].usdcDecimals. bps: 1 bps = 0.01 %.- Identifiers are opaque strings: rail ids (
rails[].rail, metalanes[].rails[].rail),tracking.kind, noticecodes, reporttypes, venue names and tokensources/flags. Read them from meta or the quote; never hard-code them. - Ignore fields you do not know; new ones may be added.
GET /partner/v1/quote
Parameters
| Param | Required | Format | Meaning |
|---|---|---|---|
chainId | yes | integer | Chain where tokenIn is spent |
tokenIn | yes | address or native sentinel | Token sold |
tokenOut | yes | address or native sentinel | Token received, on toChainId |
amountIn | yes | base units, ^[1-9][0-9]{0,77}$ | Exact input amount |
toChainId | no | integer | Destination chain. Default chainId. Another value makes it cross-chain |
slippageBps | no* | integer 0–9999 | Sets minOut and every floor |
recipient | no* | address | Receiver of the output |
deadline | no* | unix seconds | Swap expiry |
referralBps | no | integer 0–9999 | Your fee, in bps of the output |
referralRecipient | with referralBps > 0 | address | Where your fee is paid |
* A ready tx needs recipient. When a swap runs on the source chain (same-chain swaps, token-in cross-chain), it also needs slippageBps and deadline.
Rules:
- Token to token cross-chain needs
slippageBpsof at least 1. deadlineis refused when the input is USDC on a cross-chain quote.recipientmust be the user's wallet. Zero, Cleave contracts (metachains[].forbiddenRecipients) and, cross-chain, the destination's wrapped native token and route contracts are refused (400invalid_param).- Wrap and unwrap take no
recipientordeadline. - Cross-chain native input is refused: wrap it first.
- Token to token cross-chain, to receive the native coin: request the native sentinel as
tokenOut, not the wrapped token. - The quote is valid for exactly
amountIn.
What you get back
| Request | kind | flow |
|---|---|---|
| native → wrapped native, same chain | wrap | – |
| wrapped native → native, same chain | unwrap | – |
| any other pair, same chain | swap | – |
| USDC → USDC, cross-chain | cross | bridge |
| USDC → other token, cross-chain | cross | bridge (steps: 2) |
| token → USDC, cross-chain | cross | swap_bridge |
| token → token, cross-chain | cross | swap_bridge_swap |
Fields in every quote
| Field | Meaning |
|---|---|
status | ok, or no_route (HTTP 200, not an error) |
amountInUsd, amountOutUsd | USD estimates for display, or null (see USD estimates) |
quotedAt, ageMs | When chain data was read (unix ms), and its age at answer time |
block | {number, hash} the quote was computed at |
ttlSec | Re-quote when the quote is older than this |
cache | miss, collapsed, hit (shared cache, ttlSec long) or null |
notices | Ready-to-show user texts (see Notices) |
Same-chain swap fields
| Field | Meaning |
|---|---|
amountOutGross | Route output before fees |
fees.protocol | {bps, amount, block}: Cleave's fee, read on-chain |
fees.referral | {bps, amount}: your fee |
amountOutNet | Gross minus both fees: what the recipient gets at this block |
minOut | floor(amountOutNet × (10000 − slippageBps) / 10000); enforced on-chain |
preferredExecution | classic: send tx. measured: send txMeasured |
execution | Call parts: target, function, value, params, approval, permit2 |
tx | {to, data, value}, ready to sign; null without the required params or with a blocking notice |
executionMeasured, txMeasured | Same, for tokens that take a fee on transfer |
route | Pools and venues used (venue names are display labels) |
partial | Part of amountIn the route refuses; it goes back to the sender in the same tx |
Cross-chain fields
| Field | Meaning |
|---|---|
rails[] | Routes you can execute, best first. Each has its own tx |
rails[].rail | Opaque route id. Display name: meta lanes[].rails[].displayName |
rails[].amountOut | What arrives, in base units of amountOutToken (destination chain) |
rails[].amountOutUsd | USD estimate of that route's amountOut, or null |
rails[].amountOutKind | exact, exact_or_refund, floor or swap_or_usdc |
rails[].minimum | Guaranteed floor, in the same units as amountOut |
rails[].fees | protocol, referral, bridge {amount, bps}, native |
rails[].eta | {label, seconds, conditional} |
rails[].limits | min, max, maxUnit, plus minUnit/maxUnitInfo with decimals |
rails[].execution, rails[].tx | Call parts and ready tx |
rails[].tracking | How to follow the transfer: kind, idSource, url (see Status tracking) |
rankedBy | amountOut or minimum: how rails[] is sorted |
unavailableRails[] | Routes not served for this quote, with reason |
source | Source-chain swap details (swap flows), else null |
destination | Destination swap details (token to token), else null; see Destination swaps |
steps, secondStep | steps: 2: bridge USDC, then quote a swap on arrival |
Units on cross-chain quotes:
amountOutandminimumare in destination-chain units ofamountOutToken.amountIn,fees.*.amountandlimitsare in source-chain units.- USDC decimals differ on BNB Chain (18), so convert with
usdcDecimals.
Routes served to partners:
- Only routes expected to complete within 5 minutes are returned.
- Routes that need a separate claim on the destination are never returned.
- A route whose speed depends on the amount is dropped when it would be slow.
- If only slow routes exist, the API answers 400
no_fast_route. - If a fast route failed only for now, the API answers a retryable 503.
Destination swaps (token to token)
When flow is swap_bridge_swap, one route carries a swap on the destination. destination describes that swap.
destination.status | What you get |
|---|---|
ok | The route that carries the swap has amountOutKind: swap_or_usdc. If the swap fails on arrival, USDC is delivered instead (rails[].usdcFallback) |
unavailable | No destination swap right now. steps: 2: rails[] deliver USDC on the destination, and secondStep names the swap to quote after it arrives |
When status is unavailable, destination.reason says why:
reason | Meaning | What to do |
|---|---|---|
retry_unavailable | Destination swaps to this chain are not offered right now | Take the USDC routes, or try again later |
dest_gas_high | The swap needs more gas than can be covered on the destination at current gas prices | Take the USDC routes, or try again later |
no_route | No swap route for this token and amount on the destination | Take the USDC routes |
exceeds_size_cap | The swap route is too large to run on arrival | Take the USDC routes |
amount_too_low | The amount is too small for a destination swap | Use a larger amount, or take the USDC routes |
rail_unavailable, upstream_unavailable, upstream_error, upstream_timeout | Temporary | Quote again in a moment |
- Treat any other
reasonlikeno_route. - The output token must have been checked for a transfer fee on the destination. If that check can't be done, there is no quote:
- 400
unsupported_destination, reasontoken_unverifiable: not retryable; - 503
upstream_unavailable, reasondelivery_unchecked: retry in a moment.
Both carry a bridge-USDC-first plan in
error.suggestion(kind: "two_step_bridge_first"):firstis the cross-chain transfer to the destination USDC,thenthe same-chain swap on the destination. - 400
compose_unsupportedinunavailableRails[]means that route delivers USDC only, so it cannot carry the destination swap.
USD estimates
amountInUsd and amountOutUsd follow amountIn in every quote.
- A decimal string in US dollars, or
nullwhen no estimate is available. - Display only. Never use them to build or check a transaction.
- From $1 up: 2 decimals, rounded. Below $1: up to 4 significant digits. Zero is
"0". - Same-chain:
amountInandamountOutNet. Cross-chain:amountInandrails[0].amountOut; each route also hasrails[].amountOutUsd. - Stablecoins count as $1. A quote never waits for a price.
GET /partner/v1/meta
Your discovery endpoint. Read it at start-up and offer only what it lists.
| Field | Meaning |
|---|---|
chains[] | chainId, name, quotesEnabled, usdc, usdcDecimals, wrappedNative, nativeSentinel, execution, forbiddenRecipients, contracts[], cacheTtlSec |
lanes[] | One per chain pair: chainId, toChainId, router, rails[] |
lanes[].rails[] | rail, displayName, available, eta. Lists every route on the lane, including routes the partner API does not serve |
venues | Swap venues per chain |
fees | protocolFeeCapBps, referral caps |
tokens | Token and price endpoints, searchMax |
reporting | Report endpoint (/partner/v1/report), types, size and rate limits |
partner | Your partnerId, limit {perSec, windowSec, sharedPerSec}, ipAllowlist, referral.maxBps |
reporting | endpoint, types[] (the report types you may send), limit |
- Supports
ETag/If-None-Match(304 when unchanged).
Token endpoints
| Endpoint | Params | Answer |
|---|---|---|
/partner/v1/tokens | chainId, optional v | Default list: version, updatedAt, count, total, tokens[] |
/partner/v1/tokens/search | chainId, q (1–48 chars), optional limit (1–50, default 20) | count, tokens[] |
/partner/v1/tokens/lookup | chainId, address | onChain {symbol, name, decimals}, isToken, inList, listed, logo, flags, listStatus |
- A token entry:
address,symbol,name,decimals,logo,sources,flags. logois a URL, or a path on the API host (/assets/token-logo/{chainId}/{address}). Logo paths need no key.- Lookup
logo: the listed token's logo; for an unlisted token, a relative/assets/token-logo/{chainId}/{address}path when a logo was found; elsenull. - Quotes carry no symbols or decimals: take them from these endpoints.
- While a list is being built: 503
upstream_unavailable, reasontoken_list_pending.
GET /partner/v1/prices
USD prices of up to 4 tokens on one chain.
| Param | Meaning |
|---|---|
chainId | An enabled chain |
tokens | 1 to 4 comma-separated addresses; the native sentinel is allowed; duplicates are merged |
curl -s -H "Authorization: Bearer $CLEAVE_KEY" \
'https://api.cleave.fi/partner/v1/prices?chainId=8453&tokens=0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb,0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'{
"apiVersion": "1",
"chainId": 8453,
"prices": {
"0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb": { "usd": "1.00", "at": 1791810633 },
"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913": { "usd": "1.00", "at": 1791810633 }
}
}- Keys are checksummed addresses.
usd: price of one whole token, same format as the USD estimates.at: unix seconds the price was made.nullfor a token: no price right now.- Prices are cached server-side for about 45 s.
- Display only, like the USD estimates.
GET /partner/v1/health
- 200 with
status: "ok"when every chain is healthy. - 503 with
status: "degraded"otherwise. chains[]has one entry per chain.- It counts against your rate limit: poll it rarely.
POST /partner/v1/report
Tell us when something went wrong. Body: JSON, Content-Type: application/json or text/plain, at most 4,096 bytes.
| Field | Required | Meaning |
|---|---|---|
type | yes | One of meta reporting.types |
chainId | yes | Source chain |
clientVersion | yes | Your app version: 1–64 chars of letters, digits, . _ + - |
toChainId, txHash, destTxHash, quoteRequestId, rail, amount, token, recipient | per type | Context; the answer names a missing one |
message | no | Up to 280 characters |
Answer: {accepted, reportId, verified}. Reports are limited to 10 per 60 s per key.
Examples
Examples use illustrative values. Never copy them into a transaction.
Trimming: each JSON block is a valid subset of a real answer. Values in <angle brackets> are placeholders for identifiers. tx.data is cut to its 4-byte selector followed by ….
Same-chain swap with a referral
1 WETH to DAI on Base, 0.5 % slippage, 0.25 % referral:
curl -s -H "Authorization: Bearer $CLEAVE_KEY" \
'https://api.cleave.fi/partner/v1/quote?chainId=8453&tokenIn=0x4200000000000000000000000000000000000006&tokenOut=0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb&amountIn=1000000000000000000&slippageBps=50&recipient=0x000000000000000000000000000000000000bEEF&deadline=4102444800&referralBps=25&referralRecipient=0x00000000000000000000000000000000000000AA'{
"apiVersion": "1",
"requestId": "a4013f22-d6e2-4eb4-be28-0313ee57ddc5",
"kind": "swap",
"status": "ok",
"chainId": 8453,
"toChainId": 8453,
"tokenIn": "0x4200000000000000000000000000000000000006",
"tokenOut": "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb",
"amountIn": "1000000000000000000",
"amountInUsd": "2500.00",
"amountOutUsd": "2491.25",
"quotedAt": 1791810633561,
"ageMs": 2,
"cache": "miss",
"block": {
"number": "1000",
"hash": "0x00000000000000000000000000000000000000000000000000000000000003e8"
},
"ttlSec": 5,
"amountOutGross": "2500000000000000000000",
"fees": {
"protocol": { "bps": 10, "amount": "2500000000000000000", "block": "1000" },
"referral": { "bps": 25, "amount": "6250000000000000000" }
},
"amountOutNet": "2491250000000000000000",
"minOut": "2478793750000000000000",
"preferredExecution": "classic",
"execution": {
"target": "0x00000000000000000000000000000000000000A5",
"function": "swapWithBlob((address,address,uint256,uint256,uint256,address,address,uint16,bool),bytes)",
"value": "0",
"approval": {
"token": "0x4200000000000000000000000000000000000006",
"spender": "0x00000000000000000000000000000000000000A5",
"amount": "1000000000000000000"
}
},
"tx": {
"to": "0x00000000000000000000000000000000000000A5",
"data": "0xab33b1ec…",
"value": "0"
},
"notices": []
}Other fields omitted; ignore unknown fields.
Illustrative: the protocol fee here (10 bps) is a test value, not the live fee.
Check: referral = 2500 × 25 / 10000 = 6.25 DAI; net = 2500 − 2.5 − 6.25 = 2491.25; minOut = 2491.25 × 0.995.
Cross-chain: USDC on Base to USDC on Arbitrum
curl -s -H "Authorization: Bearer $CLEAVE_KEY" \
'https://api.cleave.fi/partner/v1/quote?chainId=8453&toChainId=42161&tokenIn=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&tokenOut=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amountIn=100000000&slippageBps=50&recipient=0x00000000000000000000000000000000c1ea7e01'{
"apiVersion": "1",
"requestId": "bdeea5d5-e64b-4968-9320-d46ac3f50703",
"kind": "cross",
"flow": "bridge",
"status": "ok",
"chainId": 8453,
"toChainId": 42161,
"tokenIn": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"tokenOut": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"amountIn": "100000000",
"amountInUsd": "100.00",
"amountOutUsd": "99.98",
"quotedAt": 1791810633756,
"ageMs": 3,
"block": {
"number": "52000000",
"hash": "0xb1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1"
},
"rails": [
{
"rail": "<rail id>",
"amountIn": "100000000",
"amountOut": "99982161",
"amountOutUsd": "99.98",
"amountOutToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"amountOutKind": "exact_or_refund",
"minimum": "99982161",
"fees": {
"protocol": { "bps": 0, "amount": "0", "block": "52000000" },
"referral": { "bps": 0, "amount": "0" },
"bridge": {
"amount": "17839",
"bps": 1.78,
"lp": "0",
"relayerGas": "7839",
"relayerCapital": "10000"
},
"native": null
},
"eta": { "label": "~2 s", "seconds": 2, "conditional": false },
"userTxs": 2,
"limits": {
"min": "500022",
"max": "414501693156",
"maxUnit": "bridged_usdc",
"exceedsRailLimit": false
},
"execution": {
"target": "0x00000000000000000000000000000000000000B5",
"function": "bridgeUSDC(uint256,bytes32,uint256,address,uint256,uint256,address,uint16)",
"value": "0",
"approval": {
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"spender": "0x00000000000000000000000000000000000000B5",
"amount": "100000000"
}
},
"tx": {
"to": "0x00000000000000000000000000000000000000B5",
"data": "0xea672b3e…",
"value": "0"
}
}
],
"rankedBy": "amountOut",
"steps": 1,
"secondStep": null,
"ttlSec": 5,
"notices": []
}Other fields omitted; ignore unknown fields. The second route of rails[] is omitted too.
To execute: approve execution.approval if needed, then send tx with its value. Routes with a messaging fee carry it in tx.value (see fees.native).
Read your limits
curl -s -H "Authorization: Bearer $CLEAVE_KEY" https://api.cleave.fi/partner/v1/meta{
"chains": [
{
"chainId": 8453,
"name": "Base",
"quotesEnabled": true,
"usdc": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"usdcDecimals": 6,
"wrappedNative": "0x4200000000000000000000000000000000000006",
"nativeSentinel": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
}
],
"lanes": [
{
"chainId": 8453,
"toChainId": 42161,
"rails": [
{ "rail": "<rail id>", "displayName": "Express", "available": true },
{ "rail": "<rail id>", "displayName": "Instant", "available": true }
]
}
],
"partner": {
"partnerId": "acme",
"limit": { "perSec": 3, "windowSec": 1, "key": "partner", "sharedPerSec": 3 },
"ipAllowlist": false
}
}Other fields omitted; ignore unknown fields. Only one chain and one lane are shown.
Fees in quotes
Every amount the user pays is a field in the quote.
| Fee | Where | Notes |
|---|---|---|
| Cleave protocol fee | fees.protocol {bps, amount, block} | Read on-chain at quote time; capped at 10 bps (meta fees.protocolFeeCapBps) |
| Your referral fee | fees.referral {bps, amount} | Set by you per request |
| Route fee (cross-chain) | rails[].fees.bridge {amount, bps} | Charged by the route; already out of amountOut |
| Messaging fee (cross-chain) | rails[].fees.native {amount, value, bufferPct} | Paid in the native coin through tx.value |
| Destination swap fee | destination.fees.protocol | Token-to-token only |
Same-chain math:
referral = floor(gross × referralBps / 10000)protocol = floor(gross × protocolBps / 10000)net = gross − protocol − referralminOut = floor(net × (10000 − slippageBps) / 10000)
Cross-chain: gross is amountIn for USDC input, or the source swap's USDC output.
Referral caps
| Quote | Max referralBps |
|---|---|
| Same-chain swap | 100 |
| Cross-chain | 10 |
| Destination swap (token to token, on arrival) | 0 |
| Wrap, unwrap | 0 |
- Above the cap is a 400 (
invalid_param, reasonreferral), never a silent clamp. - With
referralBpsabove 0, the instant route is not quoted: it cannot pay a referral. SendreferralBps=0to get it. - The same caps are in meta
partner.referral.maxBps. - Your fee is paid on-chain to
referralRecipientin the user's transaction.
Notices
notices holds ready-to-show texts: {code, level, title, text}. Cross-chain rails carry their own rails[].notices.
level | Meaning |
|---|---|
blocking | No transaction is returned. Show the text; do not build a tx |
warning | Show before signing; the user may continue |
info | Secondary text |
Branch on level, never on the wording. code is an opaque identifier: log it, but do not hard-code its values.
Errors
Errors use one shape, with HTTP status and retryable:
{
"apiVersion": "1",
"requestId": "75444d74-3598-4074-a019-eb28425d8cda",
"error": {
"code": "unsupported_lane",
"reason": "no_fast_route",
"message": "No fast route serves this transfer between these networks. Try another amount or token.",
"detail": "<developer text>",
"retryable": false
}
}code: stable, branch on it.reason: finer cause, when present.message: end-user text.detail: developer text; it may change, never parse it.retryAfterSec(and theRetry-Afterheader) when you should wait.- Some errors add
suggestionornotices.
A fee-on-transfer token can't be sent cross-chain directly. The answer is 400 unsupported_source, reason fee_on_transfer, with a two-step plan:
{
"suggestion": {
"kind": "two_step",
"first": {
"chainId": 8453,
"tokenIn": "0x00000000000000000000000000000000000Fee01",
"tokenOut": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
},
"then": {
"chainId": 8453,
"toChainId": 42161,
"tokenIn": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"tokenOut": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"
}
}
}first: a same-chain swap to USDC on the source chain.then: a cross-chain transfer of that USDC.then.tokenOutis always the destination USDC, never the token you asked for.- To end in another token, quote a same-chain swap on the destination once the USDC arrives.
| HTTP | code | Retry | What to do |
|---|---|---|---|
| 400 | invalid_param | no | Fix the request; detail names the parameter |
| 400 | unsupported_chain | no | Use a chain from meta |
| 400 | unsupported_lane | no | No route for this pair; no_fast_route: try another amount or token |
| 400 | unsupported_source | no | Token can't be sent cross-chain (e.g. fee-on-transfer); see suggestion |
| 400 | unsupported_destination | no | Token can't be received cross-chain directly (e.g. fee_on_transfer, token_unverifiable); see suggestion |
| 400 | same_token | no | Pick a different output token |
| 401 | unauthorized | no | Key missing, malformed, unknown, suspended or revoked (reason) |
| 403 | forbidden_origin | no | Remove the Origin header |
| 403 | forbidden_ip | no | Call from an allowlisted address |
| 404 | not_found | no | Check the path |
| 405 | method_not_allowed | no | Use GET/HEAD (POST for reports) |
| 413 | payload_too_large | no | Report body over 4,096 bytes |
| 414 | uri_too_long | no | URL over 2,048 bytes |
| 415 | unsupported_media_type | no | Report Content-Type must be JSON or text |
| 429 | rate_limited | yes | Wait Retry-After |
| 500 | internal | yes | Retry once; then report with requestId |
| 502 | bad_upstream_data | yes | Retry |
| 503 | upstream_unavailable | yes | Wait Retry-After (2 s; 60 s when reason is route_paused) |
| 503 | route_check_pending | yes | Retry after Retry-After |
| 503 | not_configured | no | Not available on this chain |
| 504 | upstream_timeout | yes | Retry |
On 401, the reason is one of missing_key, malformed_key, unknown_key, invalid_record, suspended, revoked.
Retryable or not
Trust retryable.
- Transient (
retryable: true): for example 503upstream_unavailablewithretryAfterSec, 503route_check_pending, 504, 429. WaitRetry-After, then send the same request again. - Deterministic (
retryable: false): the same request fails the same way. Change the amount, token or chain instead. This covers every 400 (for exampleno_fast_routeor a fee-on-transfer token) and 503not_configured. - Stop auto-retrying after a few retryable errors in a row, and show
message.
A quote with status: "no_route" (HTTP 200) is not an error.
Cross-chain, when every fast route refuses the transfer (for example the amount is too low or too high), the answer is usually 400 no_fast_route. It can also be a status: "no_route" body, where unavailableRails[].reason says why each route was refused (for example amount_too_low or exceeds_rail_limit). Either way: change the amount or token.
Rate limits
| Limit | Value |
|---|---|
| Per partner | Default 3 requests per second, sliding 1 s window; all your keys share it |
| Maximum per partner | 100 per second, on request |
| Shared partner capacity | 3 per second, shared by all partners, for quotes that need fresh chain work |
| Reports | 10 per 60 s per key |
| Key lookups not served from cache | 100 per 10 s per IP |
- What counts: every request that passes the key and IP checks, including 400s and
HEAD. - Shared capacity is taken by cross-chain quotes, same-chain swap quotes not served from cache, and reports that need on-chain checks. Meta, health, wrap/unwrap and token endpoints never take it.
- Your current values: meta
partner.limit.perSecandsharedPerSec.
Headers:
| Header | Value |
|---|---|
X-RateLimit-Limit | Your perSec |
X-RateLimit-Remaining | Requests left in the current window |
Retry-After | Seconds to wait (on 429 and some 503s) |
429 reasons:
reason | Cause |
|---|---|
partner_limit | Over your per-second limit |
aggregate_limit | Shared partner capacity busy |
auth_attempts | Too many key lookups from your IP |
| (none) | Report limit reached |
- After
partner_limit, all requests are refused untilRetry-Afterpasses. - If our limiter is degraded, a fixed fallback of 30 requests per 10 s applies and the
X-RateLimit-*headers are absent. - Quote on user action, not on a timer. Re-quote just before signing if the quote is older than
ttlSec.
Supported chains and tokens
| Chain | chainId | Native coin |
|---|---|---|
| Ethereum | 1 | ETH |
| Optimism | 10 | ETH |
| BNB Chain | 56 | BNB |
| Polygon | 137 | POL |
| Base | 8453 | ETH |
| Arbitrum | 42161 | ETH |
Cross-chain lanes exist between every pair of these chains.
Discover everything at run time from GET /partner/v1/meta:
- Chains:
chains[]withquotesEnabled. - Chain pairs:
lanes[]; a pair missing there is not offered. - USDC per chain:
chains[].usdcandchains[].usdcDecimals. - Contracts to approve or call:
chains[].execution,chains[].contracts[],lanes[].router. - Tokens: the token endpoints (lists, search, lookup).
ttlSec is 5 s on most chains and 12 s on Ethereum.
Status tracking for cross-chain transfers
Every route in rails[] carries a tracking object. Use it; do not hard-code a tracking endpoint.
{
"tracking": {
"kind": "<tracking kind>",
"idSource": "source transaction hash",
"url": "<status link with {txHash}>"
}
}| Field | Meaning |
|---|---|
kind | Opaque identifier of the tracking method. Never hard-code it |
idSource | The id to use: the source transaction hash for every partner route |
url | Status link. Replace {txHash} with the source transaction hash and poll it |
- Expected time is in
rails[].eta. - If an instant route is not filled within 3 hours, the input is refunded to the sender on the source chain.
Problems
If a transfer looks stuck, send POST /partner/v1/report with the delayed-transfer type from meta reporting.types, txHash and toChainId.
FAQ
My user's source transaction failed. Is it safe to try again?
Yes. A source transaction that fails reverts as a whole: no tokens move. Get a fresh quote and send the new tx.
Smart-account wallets (for example EIP-7702 delegated accounts) may set their own gas limit instead of the one your app estimates. If that limit is too low, the transaction fails this way and a retry is safe.