- Your core Mesh integration is working end-to-end (see Prepare for go-live)
- Headless bridging is enabled for your Mesh client. This is gated per-client — contact your Mesh representative to turn it on.
- The origin and destination asset/network pairs you want to bridge have been configured as routes for your account — your Mesh representative sets these up.
Overview
Headless bridging gives you Mesh’s cross-chain bridging engine as a pure API — no Mesh-hosted UI, no iframe, no SDK. You keep your own front end; Mesh does three things for you:- Route discovery — tells you every asset/network pair that Mesh can bridge into or out of the pairs you support, so you can offer them on top of your native options. These pairs can be offered in a deposit flow or withdrawal flow.
- Quote (optional) — for a given pay-in amount on a bridged pair, returns the estimated amount the recipient will receive after bridging fees, so you can show it before anything is sent.
- Address generation — returns a single-use address that is programmed to bridge funds from a pay-in pair into the destination pair (and address) the user selected.
/api/v1/bridging/*); handle native pairs the way you do today.How it works
The whole flow is two required API calls plus an optional quote, and everything after route discovery only happens for bridged pairs:- You call Mesh — route discovery. You send the asset/network pairs you support (as origins for a withdrawal, or destinations for a deposit). Mesh returns the bridged routes it can offer into (deposit) or out of (withdrawal) those pairs. Mesh does not know which pairs you support natively, so filter out routes whose other leg you already handle before showing options.
- You show the user their options — your native pairs plus the bridged pairs from step 1.
- The user selects a destination:
- If native → no second Mesh call. You handle it exactly as you do today.
- If bridged → optionally call Mesh to get a quote for the amount the user entered, and show them the estimated receive amount. Once they confirm, call Mesh to create a bridged address. For user deposits, you’ll show the user that address to which they can send the pay-in funds. For user withdrawals, you’ll send the pay-in to that address. Mesh bridges the funds to the specified pair and address.
/api/v1/bridging, use JSON with camelCase fields, and authenticate with your X-Client-Id and X-Client-Secret headers on every request.
permissionDenied? Until headless bridging is enabled for your client, every headless bridging call described on this page returns HTTP 403 with "status": "permissionDenied" and the message Headless bridging is not enabled for this client. That is the expected response, not a malformed request. Contact your Mesh representative to have it turned on.Step 1 — Discover routes
Route discovery comes in two shapes depending on your flow. Both return the same response structure.- Withdrawal (
POST /api/v1/bridging/routes/withdrawal) — you sendsources(the pairs your platform can pay out / send from). Mesh returns the destinations you can bridge those into. - Deposit (
POST /api/v1/bridging/routes/deposit) — you senddestinations(the pairs you can ultimately receive). Mesh returns the origins a user could pay in from.
Parameter reference
Parameter reference
X-Client-Id requiredX-Client-Secret requiredsources (withdrawal) requirednetwork (CAIP-2 chain id, e.g. eip155:1) and asset (symbol, case-insensitive, e.g. USDC).destinations (deposit) required{ network, asset } shape as sources.optimized optionalDefaults to true. When true, Mesh returns at most one source per distinct destination (withdrawal) or at most one destination per distinct source (deposit), choosing the preferred counterpart using a Mesh-configured network priority. Set to false to return every matching origin → destination pair.
With a single settlement source (e.g. Polygon USDC), optimized: true still returns every available destination.
routes is an origin → destination pair you can offer. Use status on the envelope to confirm success; routes may be empty when no bridged pairs match.
Do not branch on status alone. An unknown network, or a pair with no configured route, is not an error here: you get status: "ok" with an empty routes array. Schema-level failures, such as a missing required field or an empty sources / destinations array, come back in the same envelope with status: "badRequest" and errorType: "invalidField" (or "missingField" when a required field is omitted), with the offending field named in message.
Step 2 — Get a quote (optional)
Whenever you know the pay-in amount before the transfer, in either flow, callPOST /api/v1/bridging/quote with the origin and destination from the selected route, the payout address, and the amount. For a withdrawal that is the amount the user asked to withdraw; for a deposit it is the amount the user told you they will send. Mesh returns the estimated amount the recipient will receive after bridging fees, plus the fee lines behind it. Nothing is created or persisted, so call it as often as the user changes the amount. Requires read access.
estimatedAmount as an estimate and surface warnings to the user. Skip this step only when you have no amount to quote, for example a deposit where the user decides the amount in their own wallet after seeing the address.Parameter reference
Parameter reference
X-Client-Id requiredX-Client-Secret requiredorigin.network / origin.asset requiredorigin. Same shape as in Step 3.destination.network / destination.asset / destination.address requireddestination plus the address you will pass to Step 3.amount required100 for 100 USDC). Must be greater than zero and at most 1,000,000,000.destination.estimatedAmount to the user as the estimated receive amount. fees.items explains where the difference went.
Response reference
Response reference
origin.amountdestination.estimatedAmountincludedInEstimate: true. Indicative only.destination.minimumAmountnull. Indicative for this quote only; the bridge is priced on what actually arrives. Not to be confused with constraints.minimumAmount, the smallest pay-in accepted, in the origin asset.fees.items[]One line per fee: kind, label, amount, asset, amountUsd, includedInEstimate.
includedInEstimate: true — already deducted from destination.estimatedAmount. providerRelayerService (the bridge provider’s service fee) and providerRelayerGas (gas to deliver on the destination chain). providerAggregate is the rollup of those two — do not add it to them. Their amountUsd sum equals fees.totalUsd.
includedInEstimate: false — not deducted; paid by the sender. originGas is the gas for the pay-in transaction, in the origin network’s gas token (e.g. POL), paid by whichever wallet sends the pay-in — your platform on a withdrawal, the end user on a deposit — so make sure it has gas. integrator and subsidized are usually 0.
Fee amounts are in origin-side units (the gas token or the asset you send), never the destination asset; use amountUsd to compare or total.
fees.totalUsdproviderRelayerService + providerRelayerGas). Does not include originGas. May be null.rateestimatedAmount / amount, here 0.987). Not a market price — on a same-asset bridge the shortfall below 1 is entirely fees. It varies with the amount, so re-quote rather than reusing it. Most integrations can simply display estimatedAmount.constraints.minimumAmount / maximumAmountamount outside it is rejected with amountBelowMinimum / amountExceedsMaximum. The same limits are returned by /address (Step 3), converted at the current price, so they can differ slightly between calls; enforce the /address values you show the user.quotedAtwarningserrorType in the envelope):
badRequest— unknown network, or no configured route for the pair.amountBelowMinimum— under the route minimum (constraints.minimumAmount) or less than the provider will bridge. Raise the amount and re-quote.amountExceedsMaximum— over the route maximum (constraints.maximumAmount) or the provider’s available liquidity. Lower the amount and re-quote.permissionDenied— headless bridging is not enabled for your client.invalidField/missingField— schema violation (missing field,amountnot in(0, 1,000,000,000],addressover 200 characters);messagenames the field.
Step 3 — Create a bridged address
When a user selects a bridged pair (and has confirmed the quoted estimate, if you showed one), call this endpoint with the route’sorigin and destination, plus the payout address: the user’s wallet on a withdrawal, or your platform receive address on a deposit. Mesh returns a single-use deposit address to send the pay-in to.
Parameter reference
Parameter reference
X-Client-Id requiredX-Client-Secret requireduserId requiredtransactionId requireduserId. While the transfer is pending, repeating the call with the same origin, destination, and address returns the same bridging address and the same expirationTime; changing any of them returns 400 badRequest with a message naming the field. Once the transfer has reached succeeded or failed, the same transactionId returns 409 — use a new transactionId for every new pay-in.origin.network requiredorigin.origin.asset requiredorigin.destination.network requireddestination.destination.asset requireddestination.destination.address requiredThe payout address on the destination chain. On a withdrawal this is the user’s destination wallet. On a deposit it is your platform’s receive address on that chain.
The format must match that chain (e.g. 0x… for EVM, base58 for Solana, bc1… / Bitcoin bech32 for BTC). Use the same address you quoted with in Step 2.
origin asset on the origin network only) to selectedRoute.bridgingAddress.address. Mesh bridges it to the destination.
This endpoint does not take an amount and does not return a quoted amountOut. Send at least constraints.minimumAmount and, when constraints.maximumAmount is not null, no more than that (pay-ins outside these limits can fail at fill time, most often on BTC). Fees reduce what finally arrives, and the received amount is not part of this response; use Step 2 to show an estimate.
Response reference
Response reference
tracking.transferIdselectedRoute.bridgingAddress.addressorigin asset on the origin network here.constraints.minimumAmount / constraints.maximumAmountDecimal strings in the origin asset. Limits Mesh sets for the route, converted into the origin asset at the current price, so they shift slightly between calls. maximumAmount is null when no maximum can be determined. The bridge provider’s own limits apply on top.
warnings repeats the minimum as Minimum transfer amount: {n} {ASSET}. and, when maximumAmount is not null, the maximum as Maximum transfer amount: {n} {ASSET}.
Mesh cannot reject a pay-in once the address is live, so enforce both limits client-side before showing the address. A pay-in outside them may fail at fill time and need Mesh support to recover.
constraints.expirationTimefailed and a Failed webhook is sent. Never send after this time; funds sent after expiry are not bridged automatically and need Mesh support to recover.constraints.singleUseAddresstrue, the address is for this one transfer only — never reuse it for another pay-in.warningstransactionId reuse rules are in the parameter reference above (400 badRequest on a mismatch while pending, 409 after the transfer is terminal). Schema-level failures, such as a missing required field or a destination.address over 200 characters, return errorType: "invalidField" (or "missingField") with the field named in message.
Track the bridge
Each bridged transfer returns a MeshtransferId. Use it to reconcile the transfer and to match Mesh Transfer Status webhooks. Register your webhook endpoint in the Mesh dashboard under Account > API keys > Webhooks, and see the Transfer status webhooks guide for payload details.
You can also poll Get transfers initiated by Mesh (GET /api/v1/transfers/managed/mesh) with Id set to tracking.transferId, or ClientTransactionId set to your transactionId. The transfer is listed immediately as pending and then moves to succeeded or failed. Status can lag the on-chain fill by a few minutes, and BTC fills can stay pending well past the quote ETA without an error.
Succeeded: funds were delivered todestination.address.TxHashandTransferStepscarry the per-leg hashes;TransactionIdechoes yourtransactionId.Failed: sent atexpirationTimeif no pay-in was detected, or earlier if the bridge provider rejects the transfer (e.g. amount outside the min/max).- No
Pendingwebhook is sent for headless transfers. Poll the list API if you need an intermediate state. - A pay-in that lands just before
expirationTimecan settle afterFailedwas sent; Mesh then sendsSucceededfor the sameTransferId. Deduplicate onEventIdand, perTransferId, apply the event with the greater payloadTimestamp— not arrival order, since a retriedFailedcan be delivered after the laterSucceeded.
Succeeded webhook for a headless bridge looks like this (USDC on Polygon → USDC on Ethereum). TransferSteps[0] is the pay-in leg into the bridging address, TransferSteps[1] is the payout leg to destination.address; the top-level Chain, Token, DestinationAmount and TxHash all describe the payout leg.
GET /api/v1/transfers/managed/mesh, match on id (= TransferId) or clientTransactionId (= your transactionId) and read status, sourceAmount, destinationAmount, and bridgingDetails.sourceSide / bridgingDetails.targetSide for the per-leg hashes, amounts and explorer links. The top-level hash on that endpoint is the pay-in (origin-network) hash, not the payout hash.
Timing. EVM → EVM bridges typically fill within a few minutes of the pay-in confirming, and the Succeeded webhook follows one to three minutes after the fill. BTC legs depend on Bitcoin confirmations and can take considerably longer; a transfer that stays pending past the quote ETA is not an error.
What’s next
Explore related guides:- Headless bridging API reference — full request and response schemas for
POST /api/v1/bridging/routes/deposit,POST /api/v1/bridging/routes/withdrawal,POST /api/v1/bridging/quoteandPOST /api/v1/bridging/address, with a live request playground. - Add Mesh to your withdrawal flow — the withdrawal-flow guide, which includes a section on using headless bridging to expand withdrawal destinations.
- Transfer status webhooks — register for and consume transfer status webhooks to track your bridges.
AI coding reference (llms.txt)
AI coding reference (llms.txt)
/api/v1/bridging, JSON camelCase, auth via X-Client-Id + X-Client-Secret headers on every request. Gated per-client — must be enabled by Mesh. Bridging executes on mainnet (no testnet simulation).Route discovery — withdrawal: POST /api/v1/bridging/routes/withdrawal. Body: sources (required, non-empty array of { network (CAIP-2), asset (symbol) } you can send from) | optimized (optional, default true — withdrawal returns at most one source per distinct destination, deposit at most one destination per distinct source; false = every matching origin→destination pair. With a single settlement source, true still returns every destination). Response: content.routes[] of { origin, destination, displayName }.Route discovery — deposit: POST /api/v1/bridging/routes/deposit. Same as withdrawal but send destinations (pairs you can receive) instead of sources. Response identical shape.Quote (optional): POST /api/v1/bridging/quote (read access). Body: origin { network, asset } | destination { network, asset, address } | amount (decimal, in the origin asset, greater than zero and at most 1,000,000,000). Response: content.origin.amount | content.destination.estimatedAmount (net of included fees, indicative) | content.destination.minimumAmount (worst-case receive amount in the destination asset at Mesh’s slippage tolerance — “minimum received”; nullable; applies to this quote only, the later /address bridge is priced on the actual pay-in; distinct from constraints.minimumAmount, the smallest accepted pay-in in the origin asset) | content.fees.items[] { kind, label, amount, asset, amountUsd, includedInEstimate } (includedInEstimate: true lines are already subtracted from estimatedAmount: providerRelayerService + providerRelayerGas = totalUsd; providerAggregate is their rollup, do not add it; includedInEstimate: false = originGas, the pay-in transaction’s gas in the origin network’s gas token, paid by whichever wallet sends the pay-in — the platform on a withdrawal, the user on a deposit; integrator/subsidized usually 0; fee amounts are in origin-side units, never the destination asset — use amountUsd to compare) | content.fees.totalUsd | content.rate (destination units per 1 origin unit after fees = estimatedAmount / amount; not a market price; amount-dependent; informational) | content.constraints { minimumAmount, maximumAmount } (origin-asset pay-in range; same limits as /address but freshly converted per call — enforce the /address values) | content.quotedAt (unix secs) | content.warnings[]. Stateless — nothing minted or stored; call on every amount change. Errors: badRequest (unknown network / no route), amountBelowMinimum (below constraints.minimumAmount or the provider minimum), amountExceedsMaximum (above constraints.maximumAmount or the provider’s available liquidity), permissionDenied; schema violations return invalidField / missingField.Create bridged address: POST /api/v1/bridging/address (requires write access). Body: userId (required, max 384 chars, no PII) | transactionId (required, max 128 chars) | origin { network, asset } (pay-in leg) | destination { network, asset, address } (payout leg; address is the user’s wallet on a withdrawal, your platform’s receive address on a deposit). transactionId is single-use per userId (scoped to your client and environment): while the transfer is pending the same origin/destination/address returns the same address and expirationTime, a changed field returns 400 badRequest naming the field; after succeeded/failed the same transactionId returns 409 — use a new one per pay-in.Create bridged address — response: content.tracking.transferId (Mesh UUID, for status + webhooks) | content.selectedRoute.bridgingAddress.address (send origin asset on origin network here only) | content.constraints { minimumAmount, maximumAmount (nullable), expirationTime (unix secs, always set = created + 6h, never extended), singleUseAddress } | content.warnings[]. Min/max are limits Mesh sets for the route, converted to the origin asset at the current price (move slightly between calls; provider limits apply on top), rounded to the asset’s decimals and echoed in warnings as Minimum transfer amount: {n} {ASSET}. / Maximum transfer amount: {n} {ASSET}.; Mesh cannot reject a live pay-in, so enforce them client-side. Unfunded at expirationTime → failed.Flow: call route discovery with your native pairs → show native + bridged options → user picks. Native pair = skip the bridging APIs; handle it as you do today. Discovery does not know your native pairs, so filter overlapping routes client-side. Bridged pair = optionally call quote with the user’s amount and show estimatedAmount, then call create-address, send pay-in to bridgingAddress.address, Mesh bridges to destination.Critical: addresses are single-use — new transactionId per pay-in, never reuse. Bridging is mainnet-only; test with small real amounts. Track via transferId + Transfer Status webhooks: headless emits Succeeded and Failed only (no Pending); Failed at expirationTime if unfunded or on provider rejection; a later Succeeded can follow a Failed for a pay-in that settled after expiry, so deduplicate on EventId and apply the event with the greater payload Timestamp per TransferId (not arrival order). In the Succeeded payload TransferSteps[0] = pay-in leg, TransferSteps[1] = payout leg; top-level Chain/Token/DestinationAmount/TxHash describe the payout leg. Or poll GET /api/v1/transfers/managed/mesh with Id = tracking.transferId or ClientTransactionId = your transactionId (listed immediately as pending; read status, sourceAmount, destinationAmount, bridgingDetails.sourceSide/targetSide for per-leg hashes — top-level hash is the pay-in hash; status can lag the on-chain fill by minutes; BTC can stay pending past the quote ETA without erroring).Route discovery (withdrawal) — request body: