DocsDeveloper API

Build swaps and cross-chain transfers into your wallet or app with the Cleave API.

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:

  1. GET /partner/v1/quote with the tokens, amount, recipient and slippage.
  2. Read the amounts, fees and notices. Show them to your user.
  3. If returned, send execution.approvals[] in order. Otherwise approve the token with execution.approval if needed.
  4. The user signs and sends tx as returned.
  5. 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/v1/. Public /v1/quote and /v1/bridge/status 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 _ -).
  • Bearer is 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 Origin header are refused (403 forbidden_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 Origin before forwarding.
  • No CORS headers are sent. OPTIONS answers 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

MethodPathPurpose
GET, HEAD/partner/v1/quoteSwap, wrap/unwrap and cross-chain quotes with a ready tx
GET, HEAD/partner/v1/bridge/quoteTransfers of a family token itself (USDC, USDT, ETH, WBTC) to another chain, filled in seconds; /partner/v1/quote quotes them too, and any other tokenIn goes there
GET, HEAD/partner/v1/metaChains, contracts, lanes, fee caps, your key's limits
GET, HEAD/partner/v1/tokensDefault token list of a chain
GET, HEAD/partner/v1/tokens/searchSearch the full token list
GET, HEAD/partner/v1/tokens/lookupOne token: list entry and on-chain details
GET, HEAD/partner/v1/pricesUSD prices of up to 4 tokens
GET, HEAD/partner/v1/healthService health per chain
POST/partner/v1/reportReport a problem with a quote or transaction
  • HEAD returns the headers of the GET answer, without a body.
  • Every answer has X-Request-Id; quote and error bodies repeat it as requestId. Log it and quote it when you contact us.
  • Every answer, errors included, has a Link header 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/v1/delivery answers 404 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: read chains[].usdcDecimals.
  • bps: 1 bps = 0.01 %.
  • Identifiers are opaque strings: rail ids (rails[].rail, meta lanes[].rails[].rail), tracking.kind, notice codes, report types, venue names and token sources/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

ParamRequiredFormatMeaning
chainIdyesintegerChain where tokenIn is spent
tokenInyesaddress or native sentinelToken sold
tokenOutyesaddress or native sentinelToken received, on toChainId
amountInyesbase units, ^[1-9][0-9]{0,77}$Exact input amount
toChainIdnointegerDestination chain. Default chainId. Another value makes it cross-chain
slippageBpsno*integer 0–9999Sets minOut and every floor
recipientno*addressReceiver of the output
sendernoaddressThe wallet that signs on the source chain (cross-chain). Refunds go to it
deadlineno*unix secondsSwap expiry
permit2no1Sign a Permit2 allowance instead of approving RouterV3 (cross-chain swaps; see Permit2 for cross-chain swaps)
referralBpsnointeger 0–9999Your fee, in bps of the output
referralRecipientwith referralBps > 0addressWhere 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 slippageBps of at least 1.
  • autoDeliver is not a partner parameter: 400 invalid_param.
  • recipient must be the user's wallet. Zero, Cleave contracts, the chain's swap router and wrapped native token and other reserved contracts (meta chains[].forbiddenRecipients is 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 (400 invalid_param).
  • sender must be the user's wallet on the source chain. Zero, the native sentinel and Cleave or route contracts are refused (400 invalid_param). It must not be your referralRecipient: refunds go to the end user only (unavailableRails[].reason refund_recipient; 400 when no route is left).
  • Send sender on every cross-chain quote. It becomes refundRecipient. Without sender (with a recipient) the price is served and tx is withheld: each route keeps its execution, has no tx and carries execution.reason: "sender_required". Quote again with sender.
  • Wrap and unwrap take no recipient or deadline.
  • 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 in unavailableRails (reason native_out_only, or delivery_unchecked when the recipient can't be checked). With no USDC route either: 400 invalid_param, reason native_out_only, or 503 upstream_unavailable, reason delivery_unchecked.
  • The quote is valid for exactly amountIn.

What you get back

Requestkindflow
native → wrapped native, same chainwrap–
wrapped native → native, same chainunwrap–
any other pair, same chainswap–
USDC → USDC, cross-chaincrossbridge
USDC → other token, cross-chaincrossbridge (steps: 2)
native coin → USDC, cross-chaincrossswap_bridge
token → USDC, cross-chaincrossswap_bridge
token → token, cross-chaincrossswap_bridge_swap
token → ETH, USDT or WBTC, token-family lanecrossswap_bridge
ETH → ETH, token-family lanecrossswap_bridge (no swap)
USDT → USDT, WBTC → WBTC, WETH → ETH (WETH into Polygon and BNB Chain), token-family lanecrossbridge (no swap)

Where a lane has the output's token family, the token-family rows apply (see Token-family routes).

Fields in every quote

FieldMeaning
statusok, or no_route (HTTP 200, not an error)
amountInUsd, amountOutUsdUSD estimates for display, or null (see USD estimates)
quotedAt, ageMsWhen chain data was read (unix ms), and its age at answer time
block{number, hash} the quote was computed at; null for wrap/unwrap
ttlSecRe-quote when the quote is older than this; null for wrap/unwrap
cachemiss, collapsed, hit (shared cache, ttlSec long) or null
noticesReady-to-show user texts (see Notices)

Same-chain swap fields

FieldMeaning
amountOutGrossRoute output before fees
fees.referral{bps, amount}: your fee
amountOutNetExpected recipient amount after fees and any supported output transfer fee
minOutfloor(amountOutNet × (10000 − slippageBps) / 10000); enforced on-chain
preferredExecutionclassic: send tx. measured: send txMeasured
executionCall 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, txMeasuredSame, for tokens that take a fee on transfer
routePools and venues used (venue names are display labels)
partialPart 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 tx can still deliver the quote, the answer has executionMeasured and txMeasured null and preferredExecution: "classic": send tx;
  • 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, reason route_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

FieldMeaning
rails[]Routes you can execute, the default first (see Route order). Each has its own tx
rails[].railOpaque route id. Display name: meta lanes[].rails[].displayName
rails[].flowThis 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[].amountOutWhat arrives, in base units of amountOutToken (destination chain)
rails[].amountOutDecimalsDestination token decimals for amountOut and minimum
rails[].amountOutUsdUSD estimate of that route's amountOut, or null
rails[].amountOutKindexact, exact_or_refund, floor or swap_or_usdc. With exact, minimum equals amountOut
rails[].minimumGuaranteed floor, in the same units as amountOut
rails[].feesreferral when you set one, bridge {amount, bps}, native; relay {amount, token, decimals} on every instant route; delivery {amount, recipient} when delivery is included
rails[].refundRecipientWhere 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[].limitsmin, max, maxUnit, plus minUnit/maxUnitInfo with decimals
rails[].execution, rails[].txCall 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.permit2With 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[].trackingHow to follow the transfer: kind, idSource, url (see Status tracking)
rankedByamountOut 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
sourceSource-chain swap details (swap flows), else null
destinationThe 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, secondStepsteps: 2: bridge USDC, then quote a swap on arrival. steps is authoritative

Route order in rails[]: risk, then speed, then amount.

  1. Routes without a swap on arrival, then routes with one, then routes that need a claim on the destination.
  2. Within each group, routes expected within 5 minutes come first.
  3. 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:

  • amountOut and minimum use destination-chain amountOutToken units. Format them with amountOutDecimals.
  • Bridge fees use source-chain bridgeToken units and bridgeToken.decimals. For limits, read limits.minUnit and limits.maxUnitInfo.
  • The request's amountIn uses source-chain tokenIn units. 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. minOutputDst is minimum, in destination units.
  • Standard routes: 0x, or abi.encode(fee) (32 bytes) when a delivery fee is included. dstMinAmountOut is 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 tokenIn on the source chain (execution.function is crossChainSwap);
  • sender is set (it signs the permit);
  • deadline is 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 without permit2;
  • null: use the approve path (native tokenIn, or a swap whose Permit2 call can't be offered for this request).
  • pick=1 answers from the bridge path are unchanged.
FieldMeaning
function, selectorcrossChainSwapWithPermit2 on RouterV3 (selector 0x5ecf0ac4)
addressPermit2
spenderRouterV3, 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
argsEvery key of execution.args, plus permitSingle and signature: null
typedDataEIP-712 PermitSingle to sign; message equals args.permitSingle
txAlways null: encode the call with your signature (below)

typedData:

  • domain: {name: "Permit2", chainId, verifyingContract: <Permit2>}.
  • message.details: {token: tokenIn, amount, expiration, nonce}.
  • amount is execution.args.bounds.maxDebit, the same amount as execution.approval.amount; it is at least amountIn.
  • expiration and sigDeadline equal the request's deadline.
  • nonce is read for this request from Permit2.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.token is tokenIn, and spender is execution.target;
  • expiration and sigDeadline are not after deadline;
  • the nonce: read Permit2.allowance(sender, tokenIn, RouterV3) again and sign with that nonce.

To send:

  1. When tokenIn.allowance(sender, Permit2) is below amount, send approvals[] in order when present, else approval.
  2. Sign typedData with the sender wallet.
  3. Encode function with args in order (srcSwapParams, bounds, srcBlob, railId, dstEidOrDomain, dstRecipient, refundRecipient, dstMinAmountOut, railData, composeMsg, permitSingle), then your signature. Send it to execution.target with value = execution.value. The first ten arguments are the same as in the route's tx.

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 (amountOutToken is 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. amountOut and minimum are destination units (amountOutDecimals); fees, limits and dstMinAmountOut stay in bridgeToken units. Out of BNB Chain, an amount that rounds to 0 is refused amount_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.relay and relayFee in railData, already out of amountOut and minimum. Do not subtract it again.
  • rails[].bridgeToken is the family token on the source chain.
  • limits count in that token: maxUnit: "bridged_token", and minUnit/maxUnitInfo name it.
  • fees.relay {amount, token, decimals}: the route's relay fee in bridgeToken units, already out of amountOut.
  • USDT input may need the two approvals described above (0, then the amount); send execution.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 unavailableRails with reason superseded.
  • When the token-family route is not available, the answer is the USDC routes' answer, and the token-family route is listed in unavailableRails with 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; minimum equals amountOut.
  • It is not quoted with referralBps above 0: the answer is 400 invalid_param, reason referral.

The family token itself (USDT to USDT, WBTC to WBTC, WETH to ETH):

  • tokenIn is the family token on the source chain. No swap: flow: bridge, source: null, steps: 1.
  • bridgeToken is called with token = tokenIn, and execution.permit2 has permitted.token = tokenIn.
  • amountOutKind: exact_or_refund; minimum equals amountOut.
  • WETH into Ethereum, Optimism, Base or Arbitrum: request the native sentinel as tokenOut; it is paid as native ETH to a wallet recipient. 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.
  • gross is amountIn (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/v1/bridge/quote quotes the same transfers. Any other tokenIn is quoted here; the bridge quote answers 400 use_quote for 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/quote 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.statusWhat you get
oksteps: 1, secondStep: null, amountOutKind: swap_or_usdc. On the Express route, if the swap fails on arrival, USDC is delivered instead (rails[].usdcFallback)
unavailableNo 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:

reasonMeaningWhat to do
retry_unavailableDestination swaps to this chain are not offered right nowTake the USDC routes, or try again later
dest_gas_highThe swap needs more gas than can be covered on the destination at current gas pricesTake the USDC routes, or try again later
no_routeNo swap route for this token and amount on the destinationTake the USDC routes
exceeds_size_capThe swap route is too large to run on arrivalTake the USDC routes
amount_too_lowThe amount is too small for a destination swapUse a larger amount, or take the USDC routes
compose_unsupportedThis route cannot carry the swap, or its destination gas cap is exceededTake the USDC routes
rail_unavailable, upstream_unavailable, upstream_error, upstream_timeoutTemporaryQuote 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[].amountOutToken identifies 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_likely warning 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 amountOut and minimum measured after that fee. The Express swap on arrival is not offered for it: that route is in unavailableRails[] with reason fee_on_transfer, and destination is null. The answer carries a warning notice that names the fee. With no such instant route, the answer is 400 unsupported_destination, reason fee_on_transfer, with the bridge-USDC-first plan below. If that check can't be done, there is no quote:
    • 400 unsupported_destination, reason token_unverifiable: not retryable;
    • 503 upstream_unavailable, reason delivery_unchecked: retry in a moment.

    Both carry a bridge-USDC-first plan in error.suggestion (kind: "two_step_bridge_first"): first is the cross-chain transfer to the destination USDC, then the same-chain swap on the destination.

  • 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 reason rebasing, and destination is null. stETH and eETH also read a 1 bps transfer fee (share rounding): for them rebasing wins over fee_on_transfer, in unavailableRails[] and in the 400. With no instant route that swaps on arrival left, also when the delivery check drops every one, the answer is 400 unsupported_destination, reason rebasing. On pick=1 this 400 comes before recipient_blocked. If any instant route that swaps on arrival failed for a temporary reason (a timeout or an unavailable upstream), the answer is instead 503 upstream_unavailable, reason rebasing, retryable: true, with Retry-After and the same plan: quote again in a moment.
  • A public swap quote without recipient has no ready tx.

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 fieldMeaning
kindOpaque identifier of the swap on arrival
quoted{tokenIn, tokenOut} the destination swap was quoted for
routePools and venues of the destination swap, the same shape as source.route
quotedInBridged amount the destination swap was quoted for
quotedGrossOutThat swap's output before Cleave's fee
quotedCreditWhat 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, estOutGuaranteed 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
fallbackRecipientWho receives it: the recipient

USD estimates

amountInUsd and amountOutUsd follow amountIn in every quote.

  • A decimal string in US dollars, or null when 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: amountIn and amountOutNet. Cross-chain: amountIn and rails[0].amountOut; each route also has rails[].amountOutUsd.
  • Estimates use reference market prices. Stablecoins count as $1.
  • null when 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

ParamRequiredMeaning
chainIdyesSource chain (meta bridge.sources[])
toChainIdyesDestination chain, different from chainId
tokenInyesThe 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
tokenOutyesA family token on the destination (meta bridge.destinations[].tokens), or the native sentinel where nativeEth is true. Other tokens: 400 use_quote
amountInyesBase units of tokenIn
senderfor a txThe wallet that signs on the source chain
recipientnoReceiver on the destination. Default: sender
slippageBpsno0–5000, default 50. The guaranteed output must be within this of the expected output
  • Without sender you get the price only: executable: false, tx: null.
  • Unknown or repeated parameters are a 400.
  • claim is not a bridge quote parameter: claim=1 is 400 unsupported_lane, reason use_quote, with suggestion.path set to the regular quote path.
  • Public claim mode is pick=1&claim=1 on /v1/quote (USDC to USDC): one Standard route (pick.reason: "claim") with delivery.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, reason below_minimum, with suggestion.minAmountIn.

Response fields

FieldMeaning
kindauto: filled on the destination, nothing to claim
amountOutAmount delivered on the destination, base units of deliveredToken. Always equals minOut
minOutGuaranteed minimum
deliveredToken{chainId, address, decimals} of the token received
amountInUsd, amountOutUsdUSD 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
executabletrue when tx is ready
simulation{status, reason, block}: the tx run against the chain before answering
expiresAt, expiresInSecSend before this. Re-quote when expiresInSec drops below 10
refundTokenToken returned on the source chain if the fill does not happen
fallbackToken, claim, alternativeAlways null
noticesPresent 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 meta bridge.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 400 invalid_param, reason amount_too_low, with suggestion.minAmountIn.
  • USDT delivered on BNB Chain: 0.005 USDT more goes to the filler so the fill is prompt. It is already out of amountOut and minOut.
  • 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, reason no_fast_route.
  • An offer worth 30 % or more less than the input at reference prices is still served, with the price_impact warning in notices (see Notices). Show it before signing. With pick=1 on /v1/quote, such a path ranks below every path that loses less than 30 %.

Errors specific to bridge quotes:

HTTPcode / reasonRetryWhat to do
400unsupported_chainnoUse a chain from meta bridge.sources[]
400unsupported_lane / route_unavailablenoNo bridge for this pair; try suggestion.tokenOut (destination USDC) when present
400unsupported_lane / no_fast_routenoOnly slow paths. Public USDC to USDC: pick=1&claim=1 on /v1/quote asks for the claim route instead
400unsupported_lane / use_quotenotokenIn or tokenOut is not the family token (also any swap on either chain): quote the pair at suggestion.path (/v1/quote or /partner/v1/quote); also any claim parameter (claim mode is pick=1&claim=1 on /v1/quote)
400invalid_param / amount_too_low, amount_too_highnoChange 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)
400invalid_param / amount_too_lownoAlso when the output would be more than 3 % (300 bps) below the amount deposited: use a larger amount
400invalid_param / recipient_blockednoChoose another recipient
502bad_upstream_data / verification_failednoChoose another token or amount
503not_configured / bridge_fee_recipientnoNot available to this destination
503upstream_unavailableyesTemporary (prices, checks or the fill side); wait Retry-After
504upstream_timeoutyesRetry; 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.

FieldMeaning
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[].destinationComposerComposerV5 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
venuesSwap venues per chain
feesprotocolFeeCapBps, 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
tokensToken and price endpoints, searchMax
bridgeBridge quote: quote path, enabled, params, sources[] (with approvalSpenders), destinations[] (family tokens, fillHeadroom, nativeEth), expiry, fees
bridge.destinations[].fillHeadroomPer 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/quote and /partner/v1/quote carry the same margin inside fees.relay, in bridgeToken units (e.g. 5000 from a 6-decimal chain)
reportingReport endpoint (/partner/v1/report), types, size and rate limits
partnerYour partnerId, limit {perSec, windowSec, sharedPerSec}, ipAllowlist, referral.maxBps
termstermsOfUse 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:

ContractRolesFunctions
CleaveCrossChainRouterV3tx_target, approval_spender, forbidden_recipientthe five entries above
CleaveComposerV5destination_receiver, recovery_target, forbidden_recipientclaim, claimFor, recoverStranded
CleaveMeasuredShellV2M2destination_swap_shell, forbidden_recipientswapMeasuredWithBlobV1
Route and venue adapters, classic swap executors, RailRegistryV3, measured executor of RouterV3forbidden_recipientnone

Public GET /v1/meta 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

EndpointParamsAnswer
/partner/v1/tokenschainId, optional vDefault list: version, updatedAt, count, total, tokens[]
/partner/v1/tokens/searchchainId, q (1–48 chars), optional limit (1–50, default 20)count, tokens[]
/partner/v1/tokens/lookupchainId, addressonChain {symbol, name, decimals}, isToken, inList, listed, logo, flags, listStatus
  • A token entry: address, symbol, name, decimals, logo, sources, flags.
  • flags: any of native, uniswap_unsupported, decimals_conflict, verification_stale.
  • logo is an image URL (64 px for most tokens), or a path on the API host (/assets/token-logo/{chainId}/{address}). Logo paths need no key.
  • 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; null for an unlisted token. The logo path answers 404 for unlisted tokens.
  • Lookup flags for 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 has uniswap_unsupported. Warn before using a flagged token.
  • For /quote token symbols and decimals, use these endpoints.
  • While a list is being built: 503 upstream_unavailable, reason token_list_pending.

Unlisted tokens in search (public API only)

The public GET /v1/tokens/search takes an optional unlisted=1. It is not available on /partner/v1/tokens/search 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 like tokens[] (symbol, name or full address). At most limit rows.
  • Every unlisted row is unverified. Show it as such.
  • Row fields: address, symbol, name, decimals, logo, flags.
  • decimals is not read on chain. Before a quote, call lookup and use onChain.decimals.
  • logo: an image URL, or null.
  • 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.
  • q of one character: unlisted is {version, count: 0, tokens: []}.
  • unlisted: null: the unlisted index read timed out or failed; the answer has cache-control: no-store. Retry later.
  • Public lookup: logo of an unlisted token is its image URL when the token is in the unlisted index; flags also 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.

ParamMeaning
chainIdAn enabled chain
tokens1 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.
  • null for 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.

FieldRequiredMeaning
typeyesOne of meta reporting.types
chainIdyesSource chain
clientVersionyesYour app version: 1–64 chars of letters, digits, . _ + -
toChainId, txHash, destTxHash, quoteRequestId, rail, amount, token, recipientper typeContext; the answer names a missing one
messagenoUp to 280 characters

Answer: {accepted, reportId, verified}. Reports are limited to 10 per 60 s per key. Public POST /v1/report 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.

FeeWhereNotes
Your referral feefees.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.amount is the quoted fee; fees.native.value includes 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: gross less Cleave's fee and referral
  • minOut = 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[].amountIn is gross less Cleave's fee and fees.referral.amount (if any), each floored separately, as on-chain. Swap routes: the same with source.amountOutGross as gross and source.amountOutNet as the result.
  • rails[].amountIn − route fee is what is delivered. The route fee is fees.relay.amount on instant routes, fees.bridge.amount on Express routes, and fees.delivery.amount + fees.bridge.amount on 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.native is never deducted: it is paid on top through tx.value.

Referral caps

QuoteMax referralBps
Same-chain swap100
Cross-chain10
Destination swap (on arrival)0
Wrap, unwrap0
  • Above the cap is a 400 (invalid_param, reason referral), never a silent clamp.
  • With referralBps above 0, the instant USDC route and ETH to ETH are not quoted with a partner referral. Send referralBps=0 to get them. Token-family routes pay it, with a source swap or without one.
  • With referralBps above 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 referralRecipient in 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.

levelMeaning
blockingNo transaction is returned. Show the text; do not build a tx
warningShow before signing; the user may continue
infoSecondary 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:

codelevelWhen
price_impactwarningThe 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_uncheckedinfoA token on one side could not be priced, so the check did not run
  • Show price_impact before 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 the Retry-After header) when you should wait.
  • Some errors add suggestion or notices.

Rebasing tokens (their balances change without transfers) are served cross-chain only where a test send proved them:

TokenChainAs tokenInAs tokenOut (swap on arrival)
stETH, OUSD, eETHEthereumServedServed
superOETHbBaseServedRefused
Every other rebasing token (for example AMPL, OETH, USDM, USD+)AnyRefusedRefused
  • Served as tokenIn: execution.args.bounds.maxDebit and execution.approval.amount are amountIn, 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 above amountIn; stETH and eETH stay at amountIn unless their amountIn test misses (below). Whenever execution.approval.amount is above amountIn, the answer declares a fee: source.inputTransfer.status is taxed or source.routeTax.bps is above 0. A stETH or eETH transfer can arrive 1-2 wei short, shown as a 1 bps source.inputTransfer fee; 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 its execution.approval.amount is above amountIn. Always read the amount from the answer, never assume amountIn.
  • Served as tokenOut: see Destination swaps; with no instant route that swaps on arrival, also when none can be quoted, the answer is 400 unsupported_destination, reason rebasing, never a two-step USDC answer. A partner quote with no such route answers with the two-step USDC route.
  • Refused as tokenIn of a cross-chain quote: 400 unsupported_source, reason rebasing, with the two-step plan below (swap to USDC first, then send it).
  • Refused as tokenOut of a quote that swaps on arrival (public quotes with slippageBps): 400 unsupported_destination, reason rebasing, 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, no notices; pick=1 adds path: "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.taxBps shows a transfer fee, source.routeTax.bps a route fee.
  • The wallet can then be debited more than amountIn, up to execution.approval.amount. Approve that amount, not amountIn, and keep it in the wallet: the part the route does not use comes back in the same transaction.
  • /partner/v1/bridge/quote takes only family tokens: other sources are 400 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.tokenOut is 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.
HTTPcodeRetryWhat to do
400invalid_paramnoFix the request; detail names the parameter
400invalid_param / https_requirednoCall over https://; plain http:// is refused, never redirected
400unsupported_chainnoUse a chain from meta
400unsupported_lanenoNo route for this pair; no_fast_route: try another amount or token
400unsupported_sourcenoToken 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
400unsupported_destinationnoToken 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
400same_tokennoPick a different output token
401unauthorizednoKey missing, malformed, unknown, suspended or revoked (reason)
403forbidden_originnoRemove the Origin header
403forbidden_ipnoCall from an allowlisted address
404not_foundnoCheck the path
405method_not_allowednoUse GET/HEAD (POST for reports)
413payload_too_largenoReport body over 4,096 bytes
414uri_too_longnoURL over 2,048 bytes
415unsupported_media_typenoReport Content-Type must be JSON or text
429rate_limitedyesWait Retry-After
500internalyesRetry once; then report with requestId
502bad_upstream_datausuallyRetry unless reason is verification_failed
503upstream_unavailableyesWait Retry-After (2 s; 60 s when reason is route_paused)
503upstream_unavailableyesAlso when the service is busy: wait Retry-After (2 s)
503not_configurednoNot available on this chain
504upstream_timeoutyesRetry

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:

reasonMeaningWhat to do
quote_movedThe measured output or spend no longer meets the quote's boundsGet a fresh quote
compose_unsupportedThe route cannot carry this destination swap, or the swap exceeds its gas capBridge USDC first, then quote a destination swap
route_pausedNew sends are paused on this routeWait Retry-After; a full route pause uses 60 s
bad_requestThe transaction's native value or wrapped-native token is invalidGet a fresh quote and send its tx as returned
fee_on_transferA token transfer fee exceeds the supported bounds, or this route can't swap on arrival into a token that takes a transfer feeTake an instant route that swaps on arrival, swap to USDC first, or choose another token
rebasingThis route can't swap on arrival into a rebasing tokenTake an instant route that swaps on arrival, or bridge USDC first
internalThe server could not build a valid route transactionRetry once, then report requestId
rail_revertedThe route contract refused the quote without a more specific reasonTry another amount or token
recipient_blockedThe recipient can't receive on this routeChoose another recipient
refund_recipientThe 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_mismatchThe destination receiver of the route does not matchTake 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 503 upstream_unavailable with retryAfterSec, 504, 429. Wait Retry-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 example no_fast_route or a fee-on-transfer token) and 503 not_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

LimitValue
Per partnerDefault 3 requests per second, sliding 1 s window; all your keys share it
Maximum per partner100 per second, on request
Shared partner capacity3 per second, shared by all partners, for quotes that need fresh chain work
Reports10 per 60 s per key
Key lookups not served from cache100 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.perSec and sharedPerSec.

Headers:

HeaderValue
X-RateLimit-LimitYour perSec
X-RateLimit-RemainingRequests left in the current window
Retry-AfterSeconds to wait (on 429 and some 503s)

429 reasons:

reasonCause
partner_limitOver your per-second limit
aggregate_limitShared partner capacity busy
auth_attemptsToo many key lookups from your IP
(none)Report limit reached
  • After partner_limit, all requests are refused until Retry-After passes.
  • 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

ChainchainIdNative coin
Ethereum1ETH
Arbitrum42161ETH
Base8453ETH
BNB Chain56BNB
Polygon137POL
Optimism10ETH

Cross-chain lanes exist between every pair of these chains.

Discover everything at run time from GET /partner/v1/meta:

  • Chains: chains[] with quotesEnabled.
  • Chain pairs: lanes[]; a pair missing there is not offered.
  • USDC per chain: chains[].usdc and chains[].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.

ChainContractAddress
EthereumSwap Shell (classic)0x016750b998AaB0b4b07AA6a2276335053A798758
EthereumMeasured ShellV2 / approval spender0x5AfF91027390f731058AcC15E0A3F927b250c9FE
EthereumCross-chain RouterV3 / approval spender0x30809f82f8Df91e976B02F09dc64E0da8eFB1e92
EthereumDestination ComposerV50x436Ac858fD25706a8380b8AFb38Bc7538B852409
ArbitrumSwap Shell (classic)0x4052700f83324Be5710d187b7bbEa9837DfFa8fc
ArbitrumMeasured ShellV2 / approval spender0x78D851D8BC6A9b9739549E7515Bf02D6394E1794
ArbitrumCross-chain RouterV3 / approval spender0xEa0E8caE2dba07D3BC828f42fd6219fF93ddCe97
ArbitrumDestination ComposerV50xe484191bfF3e4DED3520d1D8e0fcf098CAef4cE4
BaseSwap Shell (classic)0xb08F28a3575805ee1E3729fc19Ab7E869CBAF563
BaseMeasured ShellV2 / approval spender0x003d4C95756608Ed4C11C5d0Ad9ca4c4076cAF20
BaseCross-chain RouterV3 / approval spender0x545fef65c37ECEE32309d879ABafaa959E9F114C
BaseDestination ComposerV50x58b73b40b5ED797833955397dF989AA2DEdD50A4
BNB ChainSwap Shell (classic)0x48dD089939C9a29211e7De9417e91A141D37F697
BNB ChainMeasured ShellV2 / approval spender0xC269a4F4dEbD4036597ee830349d824D5bA9ca7E
BNB ChainCross-chain RouterV3 / approval spender0x02ED7CD8D60768797328B098ce0df2fa99db6f23
BNB ChainDestination ComposerV50xE242F3DeB6B8d9e76e93b0bf5e461B12a722e86E
PolygonSwap Shell (classic)0x5b4a223D8489a3b97d4EBe8fE93C6A7aC3Ec1AD6
PolygonMeasured ShellV2 / approval spender0xA06eE4d57A2E6ea9213Ab4Bce2C055fD09BB43A1
PolygonCross-chain RouterV3 / approval spender0xEa0E8caE2dba07D3BC828f42fd6219fF93ddCe97
PolygonDestination ComposerV50x6Cbc5c7AA249EDD719001786b6e0a07D44027693
OptimismSwap Shell (classic)0xb08F28a3575805ee1E3729fc19Ab7E869CBAF563
OptimismMeasured ShellV2 / approval spender0x003d4C95756608Ed4C11C5d0Ad9ca4c4076cAF20
OptimismCross-chain RouterV3 / approval spender0x34FCCA283211224877F89F814eeA0a3a912fd354
OptimismDestination ComposerV50xAB708AEd94a07B9da06D92b4796988ee6eD0D32c

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/v1/meta (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}>"
  }
}
FieldMeaning
kindOpaque identifier of the tracking method. Never hard-code it
idSourceThe id to use: the source transaction hash for every partner route
urlStatus 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/bridge/status/<source chain>/<source tx hash> 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.stateMeaning
availableRefund can be claimed; no refund transaction has been evidenced
processingRefund activity is evidenced and is still in progress
doneRefund 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/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 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.