Skip to main content
By the end of this guide, you’ll know how to use Mesh’s headless bridging APIs to discover bridged routes and generate a bridging address — so you can offer users far more asset/network options (for deposit or withdrawal) than you natively support, without building, hosting, or embedding any Mesh UI.
Before you start
  • 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:
  1. 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.
  2. 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.
  3. 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.
This lets you present users with many more options while you keep full control of the experience.
Native vs. bridged pairsA native pair is one your platform already sends or receives directly. A bridged pair is one Mesh makes possible by bridging on your behalf. Only bridged pairs use the bridging APIs (/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:
  1. 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.
  2. You show the user their options — your native pairs plus the bridged pairs from step 1.
  3. 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.
All endpoints live on the Mesh B2B API under /api/v1/bridging, use JSON with camelCase fields, and authenticate with your X-Client-Id and X-Client-Secret headers on every request.
Seeing 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.
Bridging executes on mainnet. A generated address is a real, live address, and any pay-in moves real funds — there is no testnet simulation of the bridge itself. Test with small amounts.

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 send sources (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 send destinations (the pairs you can ultimately receive). Mesh returns the origins a user could pay in from.
X-Client-Id required
Your Mesh Client ID. Requires read access.
X-Client-Secret required
Your Mesh API Key.
sources (withdrawal) required
Non-empty array of the pairs you can send from. Each entry: network (CAIP-2 chain id, e.g. eip155:1) and asset (symbol, case-insensitive, e.g. USDC).
destinations (deposit) required
Non-empty array of the pairs you can receive. Same { network, asset } shape as sources.
optimized optional

Defaults 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.

Each item in 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, call POST /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.
The quote is an estimate. The bridging address from Step 3 accepts any amount and the bridge is priced on what actually arrives, so fees and rates can move between quoting and settlement. Show 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.
X-Client-Id required
Your Mesh Client ID. Requires read access.
X-Client-Secret required
Your Mesh API Key.
origin.network / origin.asset required
The pay-in leg, taken from the selected route’s origin. Same shape as in Step 3.
destination.network / destination.asset / destination.address required
The payout leg and recipient wallet, taken from the selected route’s destination plus the address you will pass to Step 3.
amount required
Decimal amount to be sent, in the origin asset (e.g. 100 for 100 USDC). Must be greater than zero and at most 1,000,000,000.
Show destination.estimatedAmount to the user as the estimated receive amount. fees.items explains where the difference went.
origin.amount
The pay-in amount the quote was priced for, in the origin asset.
destination.estimatedAmount
Estimated amount the recipient receives in the destination asset, net of the fees marked includedInEstimate: true. Indicative only.
destination.minimumAmount
Worst-case receive amount in the destination asset if prices move against the quote within Mesh’s slippage tolerance — the “minimum received” figure a swap UI shows (here 98.7 estimated, 96.7 worst case). May be null. 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.totalUsd
USD value of the fees deducted from the bridged funds (providerRelayerService + providerRelayerGas). Does not include originGas. May be null.
rate
Destination units received per 1 unit sent, after fees (estimatedAmount / 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 / maximumAmount
Decimal strings in the origin asset — the pay-in range Mesh accepts for this route. An amount 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.
quotedAt
Unix seconds when the quote was produced. Re-quote if the user lingers before confirming.
warnings
Human-readable caveats to show alongside the estimate.
Common errors (errorType 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, amount not in (0, 1,000,000,000], address over 200 characters); message names 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’s origin 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.
X-Client-Id required
Your Mesh Client ID. Requires write access.
X-Client-Secret required
Your Mesh API Key.
userId required
Your unique, persistent identifier for the user (max 384 characters). Do not use PII such as an email or phone number.
transactionId required
Your unique transfer/order id (max 128 characters). Single-use per userId. 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 required
CAIP-2 chain id of the pay-in leg (the pair you’ll send from). Taken from the selected route’s origin.
origin.asset required
Asset symbol of the pay-in leg. Taken from the selected route’s origin.
destination.network required
CAIP-2 chain id of the payout leg. Taken from the selected route’s destination.
destination.asset required
Asset symbol of the payout leg. Taken from the selected route’s destination.
destination.address required

The 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.

Send the pay-in (the 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.
tracking.transferId
Mesh transfer id (UUID). Use it to track status and match webhooks (see Track the bridge below).
selectedRoute.bridgingAddress.address
The address to display for the pay-in. Send only the origin asset on the origin network here.
constraints.minimumAmount / constraints.maximumAmount

Decimal 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.expirationTime
Unix seconds, 6 hours after the address was created. Fixed: repeat calls return the same value and do not extend it. If no pay-in has been detected by then, the transfer is marked failed 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.singleUseAddress
When true, the address is for this one transfer only — never reuse it for another pay-in.
warnings
Human-readable strings to surface to the user before they send the pay-in.
Addresses are single-use. Generate a fresh one (a new transactionId) for every pay-in, and never route a second transfer to a previously used address.
Errors are all in the envelope. transactionId 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 Mesh transferId. 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.
Webhooks for headless bridging
  • Succeeded: funds were delivered to destination.address. TxHash and TransferSteps carry the per-leg hashes; TransactionId echoes your transactionId.
  • Failed: sent at expirationTime if no pay-in was detected, or earlier if the bridge provider rejects the transfer (e.g. amount outside the min/max).
  • No Pending webhook is sent for headless transfers. Poll the list API if you need an intermediate state.
  • A pay-in that lands just before expirationTime can settle after Failed was sent; Mesh then sends Succeeded for the same TransferId. Deduplicate on EventId and, per TransferId, apply the event with the greater payload Timestamp — not arrival order, since a retried Failed can be delivered after the later Succeeded.
A 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.
Reconciling with the list API. When you look the transfer up with 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:
AI coding reference — a compact summary of this page’s APIs, parameters, and patterns for use by AI coding assistants (following the llms.txt standard). Human readers can safely ignore this.llms.txt — Headless bridgingHeadless bridging = Mesh’s cross-chain bridging as a pure API (no Mesh UI/SDK/iframe). Three steps, the middle one optional: (1) route discovery returns every bridged route Mesh can offer into (deposit) or out of (withdrawal) the pairs you send — it does not know your native pairs, so filter overlaps client-side; (2) quote returns the estimated receive amount for a given pay-in amount on a pair (stateless, indicative); (3) address generation returns a single-use deposit address programmed to bridge a pay-in pair into a user-selected destination pair + address. All endpoints under /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:
Create bridged address — request body: