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. Route discovery returns every bridged route Mesh can offer into (deposit) or out of (withdrawal) the pairs you send; Mesh does not know which pairs you support natively, so results can include routes whose other leg you already support. Filter those against your own list before showing options to the user. Only bridged pairs use the bridging APIs (/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:
  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.
  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
The provider’s indicative minimum for this quote at Mesh’s configured slippage tolerance. May be null. 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[]
Fee lines from the bridge provider: 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.totalUsd
Total USD value of the fees deducted from the payout. May be null.
rate
Destination units per one origin unit, net of fees (estimatedAmount / amount).
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: "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’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). userId + 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 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. 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.
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. 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.expirationTime
Unix seconds after which the address should not be used. May be null.
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. An idempotency mismatch, meaning the same userId 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 Mesh transferId. 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:
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 (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:
Create bridged address — request body: