- 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, whether that is your own rails or an existing Mesh flow.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.
- 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. The quote is not bound to the later /address call, which obtains a fresh quote that may differ, so treat it as a snapshot rather than a committed floor for settlement.fees.items[]kind, label, amount and asset (the currency the fee is charged in), amountUsd, and includedInEstimate. Lines with includedInEstimate: false (e.g. originGas) are informational and are paid by the sender, not deducted from the payout.fees.totalUsdnull.rateestimatedAmount / amount).quotedAtwarningserrorType: "badRequest" for an unknown network or no configured route for the pair; errorType: "amountBelowMinimum" when the amount is under the minimum Mesh has configured for the route, or when the provider will not bridge that little (raise the amount and re-quote); errorType: "amountExceedsMaximum" when the amount is above the provider’s available liquidity for the route (the message includes the current maximum in USD; lower the amount and re-quote); permissionDenied when headless bridging is not enabled for your client. Schema violations, such as a missing field, an amount outside the allowed range, or an address over 200 characters, come back as errorType: "invalidField" or "missingField" with the field named in message.
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 + transactionId identify the transfer: while it is still in progress, repeating the call with the same origin, destination, and address returns the same bridging address, and changing any of those returns 400 badRequest. Once the transfer has completed, or the earlier address has expired and been cleaned up, the same transactionId creates a new address.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. Mesh uses an internal probe quote, so you can send whatever size you intend, subject to the bridge provider’s floors (undersized pay-ins can still fail, most often on BTC). Fees reduce what finally arrives, and the received amount is not part of this response.
Response reference
Response reference
tracking.transferIdselectedRoute.bridgingAddress.addressorigin asset on the origin network here.constraints.minimumAmount / constraints.maximumAmountDecimal strings in the origin asset. maximumAmount is currently always null.
minimumAmount is a configured display hint (often "0"). Mesh does not block a smaller pay-in. Relay may still fail undersized amounts at quote/fill time (e.g. BTC). Enforce this client-side before showing the address.
If Mesh has a minimum configured, warnings also includes Minimum transfer amount: {n} {ASSET}.
constraints.expirationTimenull.constraints.singleUseAddresstrue, the address is for this one transfer only — never reuse it for another pay-in.warningsuserId and transactionId sent with a different origin, destination, or address while the transfer is still in progress, returns errorType: "badRequest". 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 as the bridge moves from pending to complete. 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. 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.
What’s next
Explore related guides:- 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 (provider’s indicative minimum for this quote, nullable; not bound to the later /address quote) | content.fees.items[] { kind, label, amount, asset, amountUsd, includedInEstimate } | content.fees.totalUsd | content.rate | content.quotedAt (unix secs) | content.warnings[]. Stateless — nothing minted or stored; call on every amount change. Errors: badRequest (unknown network / no route), amountBelowMinimum (below Mesh’s configured route minimum or the provider minimum), amountExceedsMaximum (above the provider’s available liquidity; message carries the current maximum in USD), 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). userId + transactionId identify the transfer (scoped to your client and environment); origin, destination and address are not part of the key but must match the in-progress record or you get 400 badRequest. Once the transfer has succeeded, or the earlier address has expired and been cleaned up, the same transactionId mints a new address.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 (always null today), expirationTime (unix secs, nullable), singleUseAddress } | content.warnings[]. minimumAmount is a configured display hint (often "0"), not an enforced floor — Mesh does not reject an undersized pay-in and the bridge provider can still fail it at quote/fill time (notably BTC), so enforce it client-side. When a minimum is configured, warnings includes Minimum transfer amount: {n} {ASSET}.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, or poll GET /api/v1/transfers/managed/mesh with Id = tracking.transferId (status can lag the on-chain fill by minutes; BTC can stay pending past the quote ETA without erroring).Route discovery (withdrawal) — request body: