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, Arbitrum, Base, BNB Chain, Polygon and Optimism.
- Wrap and unwrap the native coin on each chain.
- Cross-chain transfers between any two of these chains: USDC to USDC, native coin to USDC, token to USDC, and token to token.
- Token-family routes on some lanes: any token to ETH, USDT or WBTC, and ETH, WETH, USDT or WBTC to the same token without a swap.
- Your own referral fee on each quote, paid on-chain to your address.
- USD estimates on every quote, and token prices on request.
Cleave Quote in Grok Bot is built on this API.
AI agents have their own tools over MCP, REST and x402; see Agents.
The flow is quote → execute:
GET /partner/with the tokens, amount, recipient and slippage.v1/ quote - Read the amounts, fees and notices. Show them to your user.
- If returned, send
execution.approvals[]in order. Otherwise approve the token withexecution.approvalif needed. - 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.fi
Partner endpoints live under /partner/. Public /v1/ and /v1/ differences are noted below. JSON response bodies carry apiVersion: "1".
Auth and keys
Get a key
Request an 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/ | Swap, wrap/unwrap and cross-chain quotes with a ready tx |
| GET, HEAD | /partner/ | Transfers of a family token itself (USDC, USDT, ETH, WBTC) to another chain, filled in seconds; /partner/ quotes them too, and any other tokenIn goes there |
| GET, HEAD | /partner/ | Chains, contracts, lanes, fee caps, your key's limits |
| GET, HEAD | /partner/ | Default token list of a chain |
| GET, HEAD | /partner/ | Search the full token list |
| GET, HEAD | /partner/ | One token: list entry and on-chain details |
| GET, HEAD | /partner/ | USD prices of up to 4 tokens |
| GET, HEAD | /partner/ | Service health per chain |
| POST | /partner/ | Report a problem with a quote or transaction |
HEADreturns the headers of theGETanswer, without a body.- Every answer has
X-Request-Id; quote and error bodies repeat it asrequestId. Log it and quote it when you contact us. - Every answer, errors included, has a
Linkheader to the Terms of Use and the Privacy Policy:<https://cleave.fi/.terms>; rel="terms-of-service", <https://cleave.fi/ privacy>; rel="privacy-policy" - Quote endpoints reject unknown or repeated query parameters (400).
- URLs are limited to 2,048 bytes.
- There is no partner delivery endpoint:
/partner/answers 404v1/ delivery not_found.
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. - The wrapped-native address (
chains[].wrappedNative) means the ERC20, not the native coin. Use the sentinel for native input or output. - "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 |
sender | no | address | The wallet that signs on the source chain (cross-chain). Refunds go to it |
deadline | no* | unix seconds | Swap expiry |
permit2 | no | 1 | Sign a Permit2 allowance instead of approving RouterV3 (cross-chain swaps; see Permit2 for cross-chain swaps) |
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, token-family routes with a source swap), it also needs slippageBps and deadline. ETH to ETH needs deadline too. A family token sent as itself needs neither.
Rules:
- Token to token cross-chain needs
slippageBpsof at least 1. autoDeliveris not a partner parameter: 400invalid_param.recipientmust be the user's wallet. Zero, Cleave contracts, the chain's swap router and wrapped native token and other reserved contracts (metachains[].forbiddenRecipientsis the full list; a set, order is not meaningful) and, cross-chain, the destination's listed contracts and bridge contracts on any chain are refused (400invalid_param).sendermust be the user's wallet on the source chain. Zero, the native sentinel and Cleave or route contracts are refused (400invalid_param). It must not be yourreferralRecipient: refunds go to the end user only (unavailableRails[].reasonrefund_recipient; 400 when no route is left).- Send
senderon every cross-chain quote. It becomesrefundRecipient. Withoutsender(with arecipient) the price is served andtxis withheld: each route keeps itsexecution, has notxand carriesexecution.reason: "sender_required". Quote again withsender. - Wrap and unwrap take no
recipientordeadline. - Cross-chain USDC routes accept the native sentinel as
tokenIn: ETH on Ethereum, Arbitrum, Base and Optimism; POL on Polygon; BNB on BNB Chain. - Token to token cross-chain, to receive the native coin: request the native sentinel as
tokenOut, not the wrapped token. - Token-family routes that pay native ETH need a wallet
recipient. For a contract, or a smart-account wallet that refuses ETH, the answer comes from the USDC routes, as on other lanes; the family route is inunavailableRails(reasonnative_out_only, ordelivery_uncheckedwhen the recipient can't be checked). With no USDC route either: 400invalid_param, reasonnative_out_only, or 503upstream_unavailable, reasondelivery_unchecked. - 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) |
| native coin → USDC, cross-chain | cross | swap_bridge |
| token → USDC, cross-chain | cross | swap_bridge |
| token → token, cross-chain | cross | swap_bridge_swap |
| token → ETH, USDT or WBTC, token-family lane | cross | swap_bridge |
| ETH → ETH, token-family lane | cross | swap_bridge (no swap) |
| USDT → USDT, WBTC → WBTC, WETH → ETH (WETH into Polygon and BNB Chain), token-family lane | cross | bridge (no swap) |
Where a lane has the output's token family, the token-family rows apply (see Token-family routes).
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; null for wrap/unwrap |
ttlSec | Re-quote when the quote is older than this; null for wrap/unwrap |
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.referral | {bps, amount}: your fee |
amountOutNet | Expected recipient amount after fees and any supported output transfer fee |
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 |
Measured swaps use ShellV2 as target and approval spender. executionMeasured.permit2 is null: approve the measured shell instead.
Each swap quote measures the input token's transfer fee and tests the route at the quote's block. txMeasured is returned only when that test proves its spending limit (executionMeasured.bounds.maxDebit). When the token or the route could not be checked there:
- if
txcan still deliver the quote, the answer hasexecutionMeasuredandtxMeasurednullandpreferredExecution: "classic": sendtx; - if it can't (the token or the route takes a fee, or the output token's transfer fee could not be checked), the answer is 503
upstream_unavailable, reasonroute_check_unavailable,retryable: true: quote again in a moment.
When the input token measures as taxed (it takes a fee on transfer or rebases), execution and tx are null and preferredExecution: "measured": send txMeasured.
Cross-chain fields
| Field | Meaning |
|---|---|
rails[] | Routes you can execute, the default first (see Route order). Each has its own tx |
rails[].rail | Opaque route id. Display name: meta lanes[].rails[].displayName |
rails[].flow | This route's flow, same values as the top-level flow. A USDC fallback behind a token-family route has its own (e.g. bridge under a top-level swap_bridge, or swap_bridge_swap under a top-level bridge) |
rails[].bridgeToken | {address, symbol, decimals, chainId}: the source-chain token bridged, used for bridge fees and limits. symbol is that chain's own token symbol (e.g. USDT0 on Arbitrum and Polygon, ETH for the WETH token on BNB Chain) |
rails[].amountOut | What arrives, in base units of amountOutToken (destination chain) |
rails[].amountOutDecimals | Destination token decimals for amountOut and minimum |
rails[].amountOutUsd | USD estimate of that route's amountOut, or null |
rails[].amountOutKind | exact, exact_or_refund, floor or swap_or_usdc. With exact, minimum equals amountOut |
rails[].minimum | Guaranteed floor, in the same units as amountOut |
rails[].fees | referral when you set one, bridge {amount, bps}, native; relay {amount, token, decimals} on every instant route; delivery {amount, recipient} when delivery is included |
rails[].refundRecipient | Where refunds go: unused input, unused messaging fee and dust, and an instant-route refund after expiry. null with sender_required |
rails[].eta | {label, seconds, conditional}. Instant routes: the 90th percentile of recent fill times on the lane, or the route's own estimate when longer; about 1 minute when the lane has no recent fills. Other routes: per source chain |
rails[].limits | min, max, maxUnit, plus minUnit/maxUnitInfo with decimals |
rails[].execution, rails[].tx | Call parts and ready tx. execution.args include refundRecipient (right after dstRecipient) and railData. With execution.reason: "sender_required" there is no tx key |
rails[].execution.approvals[] | When an allowance reset is needed: ordered {token, spender, amount, tx} entries |
rails[].execution.permit2 | With permit2=1: the Permit2 call on a source swap when it can be offered, else as without permit2 (see Permit2 for cross-chain swaps) |
rails[].tracking | How to follow the transfer: kind, idSource, url (see Status tracking) |
rankedBy | amountOut or minimum: the amount that breaks ties after risk and speed (step 3 of Route order) |
unavailableRails[] | Routes not served for this quote, with reason. superseded: the route only delivers USDC while a one-step route to the requested token is served |
source | Source-chain swap details (swap flows), else null |
destination | The swap on arrival of the Express route only; with status: "ok" that route is in rails[]. null for partner quotes, and on one-step answers without the Express swap route in rails[] (see Destination swaps) |
steps, secondStep | steps: 2: bridge USDC, then quote a swap on arrival. steps is authoritative |
Route order in rails[]: risk, then speed, then amount.
- Routes without a swap on arrival, then routes with one, then routes that need a claim on the destination.
- Within each group, routes expected within 5 minutes come first.
- Then by
rankedBy, the largest amount first, less the native fee (fees.native) the route charges on top. A route whose native fee can't be priced right now comes after the routes without one.
rails[0] is the default route. The top-level amountOutUsd and notices describe it.
Units on cross-chain quotes:
amountOutandminimumuse destination-chainamountOutTokenunits. Format them withamountOutDecimals.- Bridge fees use source-chain
bridgeTokenunits andbridgeToken.decimals. For limits, readlimits.minUnitandlimits.maxUnitInfo. - The request's
amountInuses source-chaintokenInunits. Messaging fees use the source chain's native coin units. - USDC has 18 decimals on BNB Chain and 6 on the other supported chains. Source and destination decimals can differ; read each route's metadata.
Some tokens, such as USDT, need their allowance reset before a new approval. For these cross-chain inputs, execution.approvals[] contains two entries: approve 0, then approve the required amount. Each includes token, spender, amount and a ready tx (to, data, value). Send those approval transactions in array order before the route's tx. The singular execution.approval remains; when approvals[] is present, use the ordered list instead of sending only the singular approval.
Cross-chain calls go to RouterV3 (meta lanes[].router, version: 3). Family-token input calls bridgeToken (selector 0x9edd28ad): USDC, or the token a token-family route bridges as itself (USDT, WETH, WBTC). A source swap calls crossChainSwap (selector 0x84022178), and native input calls crossChainSwapNative (selector 0xf79eec2e). execution.permit2 names bridgeTokenWithPermit2 (selector 0x5ccdd67d) for family-token input, with permitted.token = tokenIn; it is null on swaps. With permit2=1 swaps can carry the Permit2 call instead (see Permit2 for cross-chain swaps). Always use the chosen route's execution and tx from the quote.
execution.args.railData carries the route options:
- Instant routes:
abi.encode(relayFee, minOutputDst), 64 bytes.minOutputDstisminimum, in destination units. - Standard routes:
0x, orabi.encode(fee)(32 bytes) when a delivery fee is included.dstMinAmountOutis then the burned amount after that fee. - Express routes:
0x.
dstMinAmountOut is in source-chain units on every route. execution.approval is null: no token approval is needed. Send one transaction on the source chain. A manual-claim route also needs a destination claim (public quotes only).
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.
Permit2 for cross-chain swaps
Add permit2=1 to a cross-chain quote (also with pick=1) to sign a Permit2 allowance instead of approving RouterV3. Without it, nothing changes.
permit2=1 never blocks a quote: when the Permit2 call can't be offered, the answer is the same as without it. The only error it adds is 400 invalid_param, reason permit2, for a value other than 1. Same-chain quotes ignore it.
A route carries the Permit2 call when:
- it swaps an ERC20
tokenInon the source chain (execution.functioniscrossChainSwap); senderis set (it signs the permit);deadlineis at least 60 s and at most 24 h ahead, in unix seconds;- the amount is below 2^160 − 1 (Permit2 reads 2^160 − 1 as unlimited).
With permit2=1, every route keeps its execution, tx, fees, amounts and order. Only execution.permit2 can change. Tell the shapes apart by execution.permit2.function:
crossChainSwapWithPermit2(…): the Permit2 call below;bridgeTokenWithPermit2(…): family-token input bridged as is (USDC, or a family token sent as itself), unchanged from the answer withoutpermit2;null: use the approve path (nativetokenIn, or a swap whose Permit2 call can't be offered for this request).pick=1answers from the bridge path are unchanged.
| Field | Meaning |
|---|---|
function, selector | crossChainSwapWithPermit2 on RouterV3 (selector 0x5ecf0ac4) |
address | Permit2 |
spender | RouterV3, the same as execution.target |
approval | {token, spender, amount}: approve tokenIn to Permit2 for at least amount |
approvals[] | When an allowance reset is needed: approve 0, then amount, to Permit2; each entry has a ready tx |
args | Every key of execution.args, plus permitSingle and signature: null |
typedData | EIP-712 PermitSingle to sign; message equals args.permitSingle |
tx | Always null: encode the call with your signature (below) |
typedData:
domain:{name: "Permit2", chainId, verifyingContract: <Permit2>}.message.details:{token: tokenIn, amount, expiration, nonce}.amountisexecution.args.bounds.maxDebit, the same amount asexecution.approval.amount; it is at leastamountIn.expirationandsigDeadlineequal the request'sdeadline.nonceis read for this request fromPermit2.allowance(sender, tokenIn, RouterV3)at the latest block.- Numbers are decimal strings: convert them before signing.
Before you sign, check:
amountIn <= details.amount <= maxDebit;details.tokenistokenIn, andspenderisexecution.target;expirationandsigDeadlineare not afterdeadline;- the nonce: read
Permit2.allowance(sender, tokenIn, RouterV3)again and sign with that nonce.
To send:
- When
tokenIn.allowance(sender, Permit2)is belowamount, sendapprovals[]in order when present, elseapproval. - Sign
typedDatawith thesenderwallet. - Encode
functionwithargsin order (srcSwapParams,bounds,srcBlob,railId,dstEidOrDomain,dstRecipient,refundRecipient,dstMinAmountOut,railData,composeMsg,permitSingle), then your signature. Send it toexecution.targetwithvalue = execution.value. The first ten arguments are the same as in the route'stx.
One permit at a time:
- Permit2 keeps one nonce per wallet, token and spender. A new signature for the same token uses the same nonce. Once one permit is used, or the nonce moves, older signatures stop working.
- A used, stale or expired permit moves no tokens: the send reverts unless the wallet already holds a Permit2 allowance for RouterV3.
- The send uses the whole permit amount and returns the unused part in the same transaction; no Permit2 allowance for RouterV3 is left.
Token-family routes
On some lanes the instant route also bridges WETH, USDT or WBTC. Meta lanes[].rails[] lists these routes (display name Instant). Today: every pair among Ethereum, Optimism, BNB Chain, Polygon, Base and Arbitrum; no WBTC to or from Base or BNB Chain.
Any token to a family token on the destination:
- The source swap ends in the family token; it is bridged and delivered as is.
steps: 1,amountOutKind: floor. - ETH family into Ethereum, Optimism, Base or Arbitrum: request the native sentinel as
tokenOut. ETH is paid as the native coin (amountOutTokenis the sentinel). - ETH family into Polygon or BNB Chain: request that chain's WETH as
tokenOut. It is paid as the ERC20. - USDC and USDT have 18 decimals on BNB Chain and 6 elsewhere. Routes to or from BNB Chain convert between them on the way: ×1e12 into BNB Chain, rounded down ÷1e12 out of it.
amountOutandminimumare destination units (amountOutDecimals); fees, limits anddstMinAmountOutstay inbridgeTokenunits. Out of BNB Chain, an amount that rounds to 0 is refusedamount_too_low. - USDT delivered on BNB Chain: 0.005 USDT more goes to the filler so the fill is prompt. It is part of
fees.relayandrelayFeeinrailData, already out ofamountOutandminimum. Do not subtract it again. rails[].bridgeTokenis the family token on the source chain.limitscount in that token:maxUnit: "bridged_token", andminUnit/maxUnitInfoname it.fees.relay {amount, token, decimals}: the route's relay fee inbridgeTokenunits, already out ofamountOut.- USDT input may need the two approvals described above (
0, then the amount); sendexecution.approvals[]when present. - Your referral is paid as on other cross-chain routes.
- The USDC routes are quoted too, as fallbacks. When the token-family route is served, it comes first; USDC routes that only deliver USDC are listed in
unavailableRailswith reasonsuperseded. - When the token-family route is not available, the answer is the USDC routes' answer, and the token-family route is listed in
unavailableRailswith its reason.
ETH to ETH (native coin on both sides):
- Offered where the source chain's ETH family lane allows native input: Ethereum, Base, Optimism or Arbitrum as source.
- No swap: the ETH is wrapped and bridged in one transaction.
execution.approval: null,source: null,tx.value = amountIn. amountOutKind: exact_or_refund;minimumequalsamountOut.- It is not quoted with
referralBpsabove 0: the answer is 400invalid_param, reasonreferral.
The family token itself (USDT to USDT, WBTC to WBTC, WETH to ETH):
tokenInis the family token on the source chain. No swap:flow: bridge,source: null,steps: 1.bridgeTokenis called withtoken=tokenIn, andexecution.permit2haspermitted.token=tokenIn.amountOutKind: exact_or_refund;minimumequalsamountOut.- WETH into Ethereum, Optimism, Base or Arbitrum: request the native sentinel as
tokenOut; it is paid as native ETH to a walletrecipient. Into Polygon or BNB Chain: request that chain's WETH (the ERC20). - WETH to WETH (the ERC20) into Ethereum, Optimism, Base or Arbitrum is not a token-family route: the USDC routes answer.
grossisamountIn(see Fees in quotes).- USDT input: send
execution.approvals[]when present, as above. - With
slippageBps, the USDC routes are quoted too, as fallbacks behind it. /partner/quotes the same transfers. Any otherv1/ bridge/ quote tokenInis quoted here; the bridge quote answers 400use_quotefor it.
Destination swaps
For USDC to another token, or token to token, partner quotes never carry the Express swap on arrival: destination: null. With slippageBps above 0 and no partner referral, they can carry the instant route's swap on arrival (below). Otherwise they bridge USDC first: steps: 2. secondStep names the swap to quote after the USDC arrives. Token-family routes are the exception (see above).
Public GET /v1/ can carry a destination swap on the Express route. This applies to token to token, and USDC to another token with slippageBps above 0. Read destination to see whether it is available.
destination describes only the Express route's swap on arrival, and the Express rail is built against it. steps is authoritative. On a one-step answer served by a token-family route or an instant route that swaps on arrival (below), destination is null, unless a USDC fallback behind it is the Express route with a swap on arrival (public quotes with slippageBps). Then destination describes that route, which is also in rails[].
destination.status | What you get |
|---|---|
ok | steps: 1, secondStep: null, amountOutKind: swap_or_usdc. On the Express route, if the swap fails on arrival, USDC is delivered instead (rails[].usdcFallback) |
unavailable | No destination swap on the Express route 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 |
compose_unsupported | This route cannot carry the swap, or its destination gas cap is exceeded | Take the USDC routes |
rail_unavailable, upstream_unavailable, upstream_error, upstream_timeout | Temporary | Quote again in a moment |
- Other contract refusal reasons are listed under Errors.
- Native sentinel output is paid as the native coin:
destination.deliveredAs: "native". A wrapped-native address is paid as the ERC20 ("erc20").rails[].amountOutTokenidentifies the actual payout token. - ETH out to a contract, or to a smart-account wallet that refuses ETH, is not served by the ETH token-family route. The USDC routes answer, with the Express swap on arrival when available, and a
fallback_usdc_likelywarning when ETH can't reach that address. - The output token must have been checked for a transfer fee on the destination, at every quote. A token that takes a fee on transfers is served only by an instant route that swaps on arrival, with
amountOutandminimummeasured after that fee. The Express swap on arrival is not offered for it: that route is inunavailableRails[]with reasonfee_on_transfer, anddestinationisnull. The answer carries awarningnotice that names the fee. With no such instant route, the answer is 400unsupported_destination, reasonfee_on_transfer, with the bridge-USDC-first plan below. 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
- A rebasing output that is served cross-chain (see Errors) is likewise served only by an instant route that swaps on arrival. The Express swap on arrival is in
unavailableRails[]with reasonrebasing, anddestinationisnull. stETH and eETH also read a 1 bps transfer fee (share rounding): for themrebasingwins overfee_on_transfer, inunavailableRails[]and in the 400. With no instant route that swaps on arrival left, also when the delivery check drops every one, the answer is 400unsupported_destination, reasonrebasing. Onpick=1this 400 comes beforerecipient_blocked. If any instant route that swaps on arrival failed for a temporary reason (a timeout or an unavailable upstream), the answer is instead 503upstream_unavailable, reasonrebasing,retryable: true, withRetry-Afterand the same plan: quote again in a moment. - A public swap quote without
recipienthas no readytx.
Public quotes, and partner quotes without a partner referral, can also run the swap on arrival on an instant route. Such a route has a rails[].destination object and amountOutKind: swap_or_usdc. Its bridgeToken names the token bridged. If the swap fails on arrival, the bridged token goes to the recipient instead; status then shows detail: "filled_fallback". When such a route bridging USDC or USDT is served, those bridging WETH or WBTC are not offered. If the destination check for these routes can't run right now, they are in unavailableRails[] with reason upstream_unavailable or upstream_timeout, and the USDC routes stay in rails[]: the same route name can be in both. Quote again in a moment. On pick=1, with no one-step route to tokenOut served, the answer is then 503 upstream_unavailable, retryable: true, with that reason and suggestion.tokenOut (the destination USDC) when a USDC route is served. A rebasing output gets the 503 reason rebasing described above.
rails[].destination field | Meaning |
|---|---|
kind | Opaque identifier of the swap on arrival |
quoted | {tokenIn, tokenOut} the destination swap was quoted for |
route | Pools and venues of the destination swap, the same shape as source.route |
quotedIn | Bridged amount the destination swap was quoted for |
quotedGrossOut | That swap's output before Cleave's fee |
quotedCredit | What the recipient gets for quotedIn: quotedGrossOut after Cleave's fee, lower when the token takes a transfer fee (measured on the destination). minOut = floor(quotedCredit × (10000 − slippageBps) / 10000) |
minOut, estOut | Guaranteed and expected amount of the requested token, after any transfer fee it takes (measured on the destination) |
fallbackToken | {chainId, address, symbol, decimals}: the bridged token, delivered if the swap fails |
fallbackRecipient | Who receives it: the recipient |
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. - Estimates use reference market prices. Stablecoins count as $1.
nullwhen a token can't be priced, or its price is not ready within 0.5 s. Bridge value checks may wait longer to confirm reference prices.
GET /partner/ v1/ bridge/ quote
A cross-chain transfer within one token family, filled on the destination in seconds: USDC, USDT, ETH or WBTC. One transaction on the source chain, after any approval. tokenIn must be that family's token on the source chain, or native ETH where the native coin is ETH. Anything else, including pairs that would need a swap on both chains, is 400 unsupported_lane, reason use_quote, with suggestion.path set to the regular quote path. Only same-family transfers are served. Any other tokenOut is 400 unsupported_lane, reason use_quote, with suggestion.path set to the regular quote path.
Parameters
| Param | Required | Meaning |
|---|---|---|
chainId | yes | Source chain (meta bridge.sources[]) |
toChainId | yes | Destination chain, different from chainId |
tokenIn | yes | The family token on the source chain (meta bridge.destinations[].tokens of the source chain), or the native sentinel where native is ETH. Other tokens: 400 use_quote |
tokenOut | yes | A family token on the destination (meta bridge.destinations[].tokens), or the native sentinel where nativeEth is true. Other tokens: 400 use_quote |
amountIn | yes | Base units of tokenIn |
sender | for a tx | The wallet that signs on the source chain |
recipient | no | Receiver on the destination. Default: sender |
slippageBps | no | 0–5000, default 50. The guaranteed output must be within this of the expected output |
- Without
senderyou get the price only:executable: false,tx: null. - Unknown or repeated parameters are a 400.
claimis not a bridge quote parameter:claim=1is 400unsupported_lane, reasonuse_quote, withsuggestion.pathset to the regular quote path.- Public claim mode is
pick=1&claim=1on/v1/(USDC to USDC): one Standard route (quote pick.reason: "claim") withdelivery.mode: "manual",delivery.reason: "claim", then a claim on the destination (see Status tracking). - If the claim gas on the destination is over 2 % of the amount, the answer is 400
invalid_param, reasonbelow_minimum, withsuggestion.minAmountIn.
Response fields
| Field | Meaning |
|---|---|
kind | auto: filled on the destination, nothing to claim |
amountOut | Amount delivered on the destination, base units of deliveredToken. Always equals minOut |
minOut | Guaranteed minimum |
deliveredToken | {chainId, address, decimals} of the token received |
amountInUsd, amountOutUsd | USD estimates (see USD estimates) |
eta | {label, seconds, conditional}: the 90th percentile of recent fill times on the lane, or the quote's own estimate when longer; about 1 minute when the lane has no recent fills. Partners only get fills of 5 minutes or less |
approvals[] | {token, spender, amount, tx} to send first, in order; empty for native input. Tokens that need an allowance reset (USDT) get two: approve 0, then the amount |
tx | {chainId, to, data, value}, ready to sign; null without sender |
executable | true when tx is ready |
simulation | {status, reason, block}: the tx run against the chain before answering |
expiresAt, expiresInSec | Send before this. Re-quote when expiresInSec drops below 10 |
refundToken | Token returned on the source chain if the fill does not happen |
fallbackToken, claim, alternative | Always null |
notices | Present when the value check has something to say (see Notices) |
Fees (meta bridge.fees):
- The fee is included in the quoted amount, already out of
amountOut. Read the current values from metabridge.fees. - USDC to USDC: the only minimum is the bridge's own small minimum deposit, shown in meta
bridge.fees.usdcMinimum. Below it the answer is 400invalid_param, reasonamount_too_low, withsuggestion.minAmountIn. - USDT delivered on BNB Chain: 0.005 USDT more goes to the filler so the fill is prompt. It is already out of
amountOutandminOut. - That amount is published per destination and family in meta
bridge.destinations[].fillHeadroom: base-unit strings,"0"where nothing is held back. Do not subtract it again.
Rules for partners:
- Only fills of 5 minutes or less are served; claim routes never are.
- If only a slower path exists: 400
unsupported_lane, reasonno_fast_route. - An offer worth 30 % or more less than the input at reference prices is still served, with the
price_impactwarning innotices(see Notices). Show it before signing. Withpick=1on/v1/, such a path ranks below every path that loses less than 30 %.quote
Errors specific to bridge quotes:
| HTTP | code / reason | Retry | What to do |
|---|---|---|---|
| 400 | unsupported_chain | no | Use a chain from meta bridge.sources[] |
| 400 | unsupported_lane / route_unavailable | no | No bridge for this pair; try suggestion.tokenOut (destination USDC) when present |
| 400 | unsupported_lane / no_fast_route | no | Only slow paths. Public USDC to USDC: pick=1&claim=1 on /v1/ asks for the claim route instead |
| 400 | unsupported_lane / use_quote | no | tokenIn or tokenOut is not the family token (also any swap on either chain): quote the pair at suggestion.path (/v1/ or /partner/); also any claim parameter (claim mode is pick=1&claim=1 on /v1/) |
| 400 | invalid_param / amount_too_low, amount_too_high | no | Change the amount; see suggestion.minAmountIn when present (base units of tokenIn; on amount_too_low it is the bridge's current minimum, given when that is above amountIn) |
| 400 | invalid_param / amount_too_low | no | Also when the output would be more than 3 % (300 bps) below the amount deposited: use a larger amount |
| 400 | invalid_param / recipient_blocked | no | Choose another recipient |
| 502 | bad_upstream_data / verification_failed | no | Choose another token or amount |
| 503 | not_configured / bridge_fee_recipient | no | Not available to this destination |
| 503 | upstream_unavailable | yes | Temporary (prices, checks or the fill side); wait Retry-After |
| 504 | upstream_timeout | yes | Retry; also returned when an offer expired before it could be served |
Bridge answers carry no tracking object.
Example: 100 USDC from Base to Arbitrum.
curl -s -H "Authorization: Bearer $CLEAVE_KEY" \
'https://api.cleave.fi/partner/v1/bridge/quote?chainId=8453&toChainId=42161&tokenIn=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&tokenOut=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amountIn=100000000&sender=0x00000000000000000000000000000000000A11cE'{
"apiVersion": "1",
"requestId": "34168d20-e0cd-4fbf-b25e-93802a69496a",
"chainId": 8453,
"toChainId": 42161,
"tokenIn": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"tokenOut": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"amountIn": "100000000",
"amountInUsd": "100.00",
"amountOutUsd": "99.48",
"recipient": "0x00000000000000000000000000000000000A11cE",
"sender": "0x00000000000000000000000000000000000A11cE",
"deliveredToken": {
"chainId": 42161,
"address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"decimals": 6
},
"kind": "auto",
"amountOut": "99475733",
"minOut": "99475733",
"eta": { "label": "~4 s", "seconds": 4, "conditional": false },
"approvals": [
{
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"spender": "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64",
"amount": "100000000",
"tx": {
"chainId": 8453,
"to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"data": "0x095ea7b3…",
"value": "0"
}
}
],
"tx": {
"chainId": 8453,
"to": "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64",
"data": "0xad5425c6…",
"value": "0"
},
"expiresAt": 1791646931,
"refundToken": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"fallbackToken": null,
"simulation": { "status": "ok", "reason": null, "block": "256" },
"executable": true,
"expiresInSec": 3371,
"claim": null,
"alternative": null
}Other fields omitted; ignore unknown fields.
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, measuredExecution, forbiddenRecipients, contracts[], cacheTtlSec |
lanes[] | One per chain pair: chainId, toChainId, router, destinationComposer, rails[] |
lanes[].router | {address, version: 3, crossChainSwap, crossChainSwapNative, crossChainSwapWithPermit2, bridgeToken, bridgeTokenWithPermit2}; each entry is {function, selector}. address is RouterV3, the call target and approval spender |
lanes[].destinationComposer | ComposerV5 on the destination chain |
lanes[].rails[] | rail, railId, adapter, token, decimals, dstToken, dstDecimals, 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 |
crossChainReferral | {bps, scope, recipient}: what cross-chain RouterV3 sends carry in referrer / referralFeeBps when you set no referralBps. recipient is keyed by source chainId; scope is cross_chain_router_v3 |
tokens | Token and price endpoints, searchMax |
bridge | Bridge quote: quote path, enabled, params, sources[] (with approvalSpenders), destinations[] (family tokens, fillHeadroom, nativeEth), expiry, fees |
bridge.destinations[].fillHeadroom | Per family on that destination, the same keys as tokens: base units (string) paid to the filler on top of the fee so the fill is prompt, e.g. {"usdc":"0","usdt":"5000000000000000","eth":"0"} on BNB Chain. "0" means none. Bridge quotes delivering that token already have amountOut and minOut lowered by it; instant routes on /v1/ and /partner/ carry the same margin inside fees.relay, in bridgeToken units (e.g. 5000 from a 6-decimal chain) |
reporting | Report endpoint (/partner/), types, size and rate limits |
partner | Your partnerId, limit {perSec, windowSec, sharedPerSec}, ipAllowlist, referral.maxBps |
terms | termsOfUse and privacyPolicy: the links to the Terms of Use and the Privacy Policy |
- Supports
ETag/If-None-Match(304 when unchanged).
lanes[].router.version is 3: pick the RouterV3 ABI and selectors by it. rails[].token and decimals are the source-chain route token; dstToken and dstDecimals the token that arrives.
chains[].contracts[] roles:
| Contract | Roles | Functions |
|---|---|---|
CleaveCrossChainRouterV3 | tx_target, approval_spender, forbidden_recipient | the five entries above |
CleaveComposerV5 | destination_receiver, recovery_target, forbidden_recipient | claim, claimFor, recoverStranded |
CleaveMeasuredShellV2M2 | destination_swap_shell, forbidden_recipient | swapMeasuredWithBlobV1 |
Route and venue adapters, classic swap executors, RailRegistryV3, measured executor of RouterV3 | forbidden_recipient | none |
Public GET /v1/ serves the same router description and the same terms. Its delivery object has feeIn: "railData" and ticketVersion: "v3". Its bridge.pick object has enabled (always true), marginBps and chooseFrom: {"family": "usdc", "minAmountUsd": "100"}, the same everywhere. marginBps is 0. The picked route is a fast route when one exists; with none, the slower fill is served with its longer eta. A route that loses 30 % or more ranks below every route that loses less; then routes without a swap on arrival come first. Within each group, the Instant route and the bridge quote compare on what they guarantee, less any native fee, with no margin; an Express route is picked over either only when it guarantees over 10 bps more. A client may offer the full route list on a USDC to USDC lane whose lanes[].rails[] include the delivery route (delivery.rail), when delivery.enabled is true and the amount is at least minAmountUsd USD. Otherwise it shows the picked route.
Token endpoints
| Endpoint | Params | Answer |
|---|---|---|
/partner/ | chainId, optional v | Default list: version, updatedAt, count, total, tokens[] |
/partner/ | chainId, q (1–48 chars), optional limit (1–50, default 20) | count, tokens[] |
/partner/ | chainId, address | onChain {symbol, name, decimals}, isToken, inList, listed, logo, flags, listStatus |
- A token entry:
address,symbol,name,decimals,logo,sources,flags. flags: any ofnative,uniswap_unsupported,decimals_conflict,verification_stale.logois an image URL (64 px for most tokens), or a path on the API host (/assets/). Logo paths need no key.token-logo/ {chainId}/ {address} - Tokens of an issuer that is winding down are not listed and not in unlisted search. Since 2026-10-18 this covers the Universal uAssets on Base and Polygon. Lookup still reads them on chain, as unlisted tokens.
- Lookup
logo: the listed token's logo;nullfor an unlisted token. The logo path answers 404 for unlisted tokens. - Lookup
flagsfor an unlisted token:["symbol_collision"]when its on-chain symbol or name matches a listed or major token, or has non-ASCII characters; else[]. A block-listed token also hasuniswap_unsupported. Warn before using a flagged token. - For
/quotetoken symbols and decimals, use these endpoints. - While a list is being built: 503
upstream_unavailable, reasontoken_list_pending.
Unlisted tokens in search (public API only)
The public GET /v1/ takes an optional unlisted=1. It is not available on /partner/ yet (400 unknown parameter). The answer then adds an unlisted block next to count and tokens[]. Without unlisted=1 the answer is unchanged.
curl -s 'https://api.cleave.fi/v1/tokens/search?chainId=8453&q=brett&unlisted=1'{
"apiVersion": "1",
"chainId": 8453,
"version": "43825e079cf7d8a5",
"q": "brett",
"count": 0,
"tokens": [],
"unlisted": {
"version": "a7500084520bf45d",
"count": 1,
"tokens": [
{
"address": "0x532f27101965dd16442E59d40670FaF5eBB142E4",
"symbol": "BRETT",
"name": "Brett",
"decimals": 18,
"logo": "https://…/brett.png",
"flags": []
}
]
}
}unlisted.tokens[]: tokens not in our list, matched liketokens[](symbol, name or full address). At mostlimitrows.- Every unlisted row is unverified. Show it as such.
- Row fields:
address,symbol,name,decimals,logo,flags. decimalsis not read on chain. Before a quote, call lookup and useonChain.decimals.logo: an image URL, ornull.flags:[]or["symbol_collision"]: the symbol or name matches a listed or major token, another unlisted token has the same symbol, or the text has non-ASCII characters. Warn, and never pre-select such a row.- Collision rows come last within each match tier.
qof one character:unlistedis{version, count: 0, tokens: []}.unlisted: null: the unlisted index read timed out or failed; the answer hascache-control: no-store. Retry later.- Public lookup:
logoof an unlisted token is its image URL when the token is in the unlisted index;flagsalso counts a same-symbol unlisted token. - Public limit: 60 requests per 10 s per IP, shared by the token list, search and lookup.
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/ or text/, 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. Public POST /v1/ also runs a human check. When that check can't run, the report is not recorded: 503 upstream_unavailable, reason turnstile_unavailable, retryable: true. Try again in a moment with a fresh check. The partner path has no human check.
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": "2493.75",
"quotedAt": 1791810633561,
"ageMs": 2,
"cache": "miss",
"block": {
"number": "1000",
"hash": "0x00000000000000000000000000000000000000000000000000000000000003e8"
},
"ttlSec": 5,
"amountOutGross": "2500000000000000000000",
"fees": {
"referral": { "bps": 25, "amount": "6250000000000000000" }
},
"amountOutNet": "2493750000000000000000",
"minOut": "2481281250000000000000",
"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.
Check: referral = 2500 × 25 / 10000 = 6.25 DAI; net = 2500 − 6.25 = 2493.75; minOut = 2493.75 × 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&sender=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.93",
"quotedAt": 1791810633756,
"ageMs": 3,
"block": {
"number": "52000000",
"hash": "0xb1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1"
},
"rails": [
{
"rail": "<rail id>",
"bridgeToken": {
"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"symbol": "USDC",
"decimals": 6,
"chainId": 8453
},
"amountIn": "99950000",
"amountOut": "99932166",
"amountOutUsd": "99.93",
"amountOutToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"amountOutDecimals": 6,
"amountOutKind": "exact_or_refund",
"minimum": "99932166",
"fees": {
"bridge": {
"amount": "17834",
"bps": 1.78,
"lp": "0",
"relayerGas": "7839",
"relayerCapital": "9995"
},
"relay": {
"amount": "17834",
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6
},
"native": null
},
"refundRecipient": "0x00000000000000000000000000000000c1EA7E01",
"eta": { "label": "~4 s", "seconds": 4, "conditional": false },
"userTxs": 2,
"limits": {
"min": "500022",
"max": "414501693156",
"maxUnit": "bridged_usdc",
"exceedsRailLimit": false
},
"execution": {
"target": "0x00000000000000000000000000000000000000B5",
"function": "bridgeToken(address,uint256,bytes32,uint256,address,address,uint256,address,uint16,bytes,bytes)",
"selector": "0x9edd28ad",
"value": "0",
"args": {
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amount": "100000000",
"railId": "<rail id>",
"dstEidOrDomain": "42161",
"dstRecipient": "0x00000000000000000000000000000000c1EA7E01",
"refundRecipient": "0x00000000000000000000000000000000c1EA7E01",
"dstMinAmountOut": "99932166",
"referrer": "<meta crossChainReferral.recipient for chain 8453>",
"referralFeeBps": "<meta crossChainReferral.bps>",
"railData": "0x…",
"composeMsg": "0x"
},
"approval": {
"token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"spender": "0x00000000000000000000000000000000000000B5",
"amount": "100000000"
}
},
"tx": {
"to": "0x00000000000000000000000000000000000000B5",
"data": "0x9edd28ad…",
"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: send execution.approvals[] in order when present. Otherwise approve execution.approval if needed. Then send tx with its value. Send tx.value exactly as returned; it includes any messaging fee (see Fees in quotes).
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 quoted amount (amountOut, minimum, rails[].amountIn, amountOutNet) is already net of every fee deducted from it.
| Fee | Where | Notes |
|---|---|---|
| Your referral fee | fees.referral {bps, amount} | Set by you per request; on cross-chain routes only when you set one |
| Route fee (cross-chain) | rails[].fees.bridge {amount, bps} | Charged by the route; already out of amountOut |
| Relay fee (instant routes) | rails[].fees.relay {amount, token, decimals} | The same route fee, in bridgeToken units |
| Delivery fee (public, Standard route with delivery) | rails[].fees.delivery {amount, recipient} | Out of the burned amount |
| Messaging fee (cross-chain) | rails[].fees.native {amount, value, bufferPct} | Paid in the native coin through tx.value |
For native input on cross-chain routes, send tx.value exactly as returned:
- Without a messaging fee (
fees.native: null),tx.value = amountIn. - With a messaging fee,
tx.value = amountIn + fees.native.value.fees.native.amountis the quoted fee;fees.native.valueincludes a refundable 10 % buffer (fees.native.bufferPct: 10).
Any unused part of that fee budget is refunded to the sender in the same transaction. Sending less than the returned tx.value reverts.
Same-chain math for tokens without a transfer fee:
referral = floor(gross × referralBps / 10000)net = amountOutNet:grossless Cleave's fee andreferralminOut = floor(net × (10000 − slippageBps) / 10000)
Cross-chain: gross is what the route bridges before fees: amountIn when no swap runs on the source chain (including native input that is only wrapped), else the source swap's output. Use the route's own rails[].source when present, otherwise the top-level source. In source units, on every route:
rails[].amountInisgrossless Cleave's fee andfees.referral.amount(if any), each floored separately, as on-chain. Swap routes: the same withsource.amountOutGrossasgrossandsource.amountOutNetas the result.rails[].amountIn − route feeis what is delivered. The route fee isfees.relay.amounton instant routes,fees.bridge.amounton Express routes, andfees.delivery.amount + fees.bridge.amounton Standard routes.- On lanes whose decimals differ (USDC and USDT to or from BNB Chain), the delivered amount then converts to destination units: ×1e12 into BNB Chain, rounded down ÷1e12 out of it.
fees.nativeis never deducted: it is paid on top throughtx.value.
Referral caps
| Quote | Max referralBps |
|---|---|
| Same-chain swap | 100 |
| Cross-chain | 10 |
| Destination swap (on arrival) | 0 |
| Wrap, unwrap | 0 |
- Above the cap is a 400 (
invalid_param, reasonreferral), never a silent clamp. - With
referralBpsabove 0, the instant USDC route and ETH to ETH are not quoted with a partner referral. SendreferralBps=0to get them. Token-family routes pay it, with a source swap or without one. - With
referralBpsabove 0, a partner quote has no swap on arrival: it answers with the two-step USDC route. - 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; the top-level notices describe the default (first) route.
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.
Value check. Swap and cross-chain quotes compare what the user pays with what they receive, at reference market prices:
code | level | When |
|---|---|---|
price_impact | warning | The output is worth at least 10 % less than the input. Carries bps, the loss in bps. For a token with a fee, the input is what leaves the wallet (the measured debit), not amountIn |
value_unchecked | info | A token on one side could not be priced, so the check did not run |
- Show
price_impactbefore signing. Quotes are never refused for price impact. - A smaller amount often gets a better rate.
{
"code": "price_impact",
"level": "warning",
"title": "High price impact",
"text": "What you receive is worth about 12.5% less than what you pay at market prices. A smaller amount may get a better rate.",
"bps": 1250
}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.
Rebasing tokens (their balances change without transfers) are served cross-chain only where a test send proved them:
| Token | Chain | As tokenIn | As tokenOut (swap on arrival) |
|---|---|---|---|
| stETH, OUSD, eETH | Ethereum | Served | Served |
| superOETHb | Base | Served | Refused |
| Every other rebasing token (for example AMPL, OETH, USDM, USD+) | Any | Refused | Refused |
- Served as
tokenIn:execution.args.bounds.maxDebitandexecution.approval.amountareamountIn, so the whole balance can be sent, when the swap route charges no fee. For OUSD and superOETHb a route fee is covered from headroom, as for any route fee (source.routeTax), so the amount is then aboveamountIn; stETH and eETH stay atamountInunless theiramountIntest misses (below). Wheneverexecution.approval.amountis aboveamountIn, the answer declares a fee:source.inputTransfer.statusistaxedorsource.routeTax.bpsis above 0. A stETH or eETH transfer can arrive 1-2 wei short, shown as a 1 bpssource.inputTransferfee; the send swaps what arrives, and the quote is sized from a test of that swap. If the test returns less than the quote minus the value of 2 wei, or does not finish, that answer uses the transfer-fee rule below and itsexecution.approval.amountis aboveamountIn. Always read the amount from the answer, never assumeamountIn. - Served as
tokenOut: see Destination swaps; with no instant route that swaps on arrival, also when none can be quoted, the answer is 400unsupported_destination, reasonrebasing, never a two-step USDC answer. A partner quote with no such route answers with the two-step USDC route. - Refused as
tokenInof a cross-chain quote: 400unsupported_source, reasonrebasing, with the two-step plan below (swap to USDC first, then send it). - Refused as
tokenOutof a quote that swaps on arrival (public quotes withslippageBps): 400unsupported_destination, reasonrebasing, with the bridge-USDC-first plan (two_step_bridge_first). Partner quotes are not refused: they answer with the two-step USDC route. retryable: false, nonotices;pick=1addspath: "lane".- Same-chain swaps of these tokens are quoted as before.
Fee-on-transfer tokens on cross-chain /quote:
- A token that takes a fee on every transfer, or whose swap route charges one, is quoted when that fee is measured fresh at the quote's block and a test of the swap with the served spending limit delivers the quoted amount.
source.inputTransfer.taxBpsshows a transfer fee,source.routeTax.bpsa route fee. - The wallet can then be debited more than
amountIn, up toexecution.approval.amount. Approve that amount, notamountIn, and keep it in the wallet: the part the route does not use comes back in the same transaction. /partner/takes only family tokens: other sources are 400v1/ bridge/ quote use_quote.
Every token input other than the native coin, its wrapped token and USDC is measured fresh on cross-chain swap quotes, on the same contracts the cross-chain send uses. If that measurement is inconclusive, there is no quote: 400 unsupported_source, reason input_transfer_unknown or route_check_failed, with the two-step plan below. route_check_failed also covers a test swap that reverts at the quote's block, for example for an amount close to or above the token's supply. If a check could not run right now (it timed out, or the chain could not be read), the answer is 503 upstream_unavailable, reason route_check_unavailable, retryable: true: quote again in a moment (the two-step plan is in suggestion).
A token that charges the sender on top of each transfer, or whose fee would take more than twice amountIn from the wallet, 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 | invalid_param / https_required | no | Call over https://; plain http:// is refused, never redirected |
| 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. it charges the sender on top, its fee is too large to cover, or its fee couldn't be measured); see suggestion. rebasing: this rebasing token can't be sent cross-chain directly |
| 400 | unsupported_destination | no | Token can't be received cross-chain directly (e.g. fee_on_transfer, token_unverifiable); see suggestion. rebasing: no swap on arrival delivers this rebasing token |
| 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 | usually | Retry unless reason is verification_failed |
| 503 | upstream_unavailable | yes | Wait Retry-After (2 s; 60 s when reason is route_paused) |
| 503 | upstream_unavailable | yes | Also when the service is busy: wait Retry-After (2 s) |
| 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, or use_partner_path (a key sent to a public /v1/ endpoint).
Contract refusal reasons can appear in unavailableRails[].reason, destination.reason on public quotes, or error.reason:
reason | Meaning | What to do |
|---|---|---|
quote_moved | The measured output or spend no longer meets the quote's bounds | Get a fresh quote |
compose_unsupported | The route cannot carry this destination swap, or the swap exceeds its gas cap | Bridge USDC first, then quote a destination swap |
route_paused | New sends are paused on this route | Wait Retry-After; a full route pause uses 60 s |
bad_request | The transaction's native value or wrapped-native token is invalid | Get a fresh quote and send its tx as returned |
fee_on_transfer | A token transfer fee exceeds the supported bounds, or this route can't swap on arrival into a token that takes a transfer fee | Take an instant route that swaps on arrival, swap to USDC first, or choose another token |
rebasing | This route can't swap on arrival into a rebasing token | Take an instant route that swaps on arrival, or bridge USDC first |
internal | The server could not build a valid route transaction | Retry once, then report requestId |
rail_reverted | The route contract refused the quote without a more specific reason | Try another amount or token |
recipient_blocked | The recipient can't receive on this route | Choose another recipient |
refund_recipient | The refund address can't take refunds (it rejects the native coin, or it is the referral recipient) | Send from another wallet, or change sender |
composer_mismatch | The destination receiver of the route does not match | Take the other routes; it is reported to us |
sender_required is not an error: it is rails[].execution.reason on any quote without sender that has a recipient. The price is still served.
Retryable or not
Trust retryable.
- Transient (
retryable: true): for example 503upstream_unavailablewithretryAfterSec, 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 and bridge 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 |
| Arbitrum | 42161 | ETH |
| Base | 8453 | ETH |
| BNB Chain | 56 | BNB |
| Polygon | 137 | POL |
| Optimism | 10 | ETH |
Cross-chain lanes exist between every pair of these chains.
Discover everything at run time from GET /partner/:
- 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[].measuredExecution,chains[].contracts[],lanes[].router. - Tokens: the token endpoints (lists, search, lookup).
ttlSec is 5 s on most chains and 12 s on Ethereum.
Contract addresses
Cleave contracts on each chain. Key them by chain and address: the same address can be a different contract on another chain. RouterV3 is both the cross-chain call target and approval spender. ShellV2 is the measured same-chain call target and approval spender. The classic same-chain Swap Shell is still available.
Only approve the spender named in your quote (execution.approval, execution.approvals[], execution.permit2 or bridge quote approvals[]). It is always one of the contracts above, Permit2, or a bridge deposit contract listed in /partner/ (bridge.sources[].approvalSpenders).
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. - An instant route that expires unfilled after 3 hours can be refunded on the source chain. Expiry alone does not mean a refund was sent.
Public GET /v1/ also reports destination delivery and claims. It tracks one transfer per source transaction: a source transaction that holds more than one transfer answers 404 not_found, reason not_a_bridge_tx.
An expired, unfilled instant-route transfer (token-family routes included) returns status: "refunded", detail: "refund_available" and refund.state: "available". The refund is available to claim; it has not been sent. refund.txHash is null in this state. refund.token is the bridged token on the source chain. refund.to is the refund address: the instant-route depositor, which is the quote's refundRecipient. Use refund.state to distinguish availability from refund activity:
refund.state | Meaning |
|---|---|
available | Refund can be claimed; no refund transaction has been evidenced |
processing | Refund activity is evidenced and is still in progress |
done | Refund completion is evidenced |
Do not treat status: "refunded" alone as proof that funds were returned.
deliveredToken {chainId, address, amount} is set on a filled instant route that runs calls on arrival (a swap on arrival, or a native coin payout): the token and amount that reached the recipient, read from the fill transaction. For a swap on arrival it is the amount the recipient was credited, after any transfer fee the token takes. After a failed swap it is the bridged token, with detail: "filled_fallback". A delivery held on arrival shows detail: "compose_stranded" until it is paid out in the bridged token (filled_fallback) or becomes a claim (ready_to_claim). The native coin is the native sentinel 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee. Addresses in deliveredToken are lowercase. It is null for plain fills.
If status is ready_to_claim, send the returned claim transaction on claim.chainId, using claim.to, claim.data and claim.value as-is.
Destination composer claims go to ComposerV5 and use claim(token, recipient, sendAmount) (claim(address,address,uint256)). claim and claimFor return the amount taken from the claim, and the Claimed event carries it. A claim for less leaves the rest claimable. On ComposerV5 answers that are claimable, claimFor {chainId, to, data, value} is also set, after claim: claimFor(recipient, token) (selector 0xb4ba9e11) pays the whole claim to the credited address and anyone may send it. On every other answer claimFor is null. The wallet that holds the claim must sign the transaction. The API encodes the current claimable balance in claim.data. deliveredToken.amount remains the amount for this transfer; do not use it to rebuild the claim. A zero claimable balance returns status: "delivered" and claim: null. If the balance cannot be read, the API returns a retryable error.
Problems
If a transfer looks stuck, send POST /partner/ 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 the swap or bridge transfer. 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.
Terms of use
Use of the API, the partner API and the MCP server is subject to the Terms of Use and the Privacy Policy. By sending requests or connecting a client after this notice, you accept them.
If an agent or other software calls Cleave, its operator accepts them and is responsible for what it does. Partner and agent keys are issued only after the key holder accepts them. Stay within the published rate limits.