# Generate Auth token Source: https://docs.meshconnect.com/api-reference/account-management/auth-token/generate-auth-token https://admin-api.meshconnect.com/swagger/AdminAPI/swagger.json post /admin/api/v1/Token Get a short lived token for initializing request calls for Registered client API. # Get Main Client callback urls Source: https://docs.meshconnect.com/api-reference/account-management/main-clients/get-main-client-callback-urls https://admin-api.meshconnect.com/swagger/AdminAPI/swagger.json get /admin/api/v1/Client/callbackUrls Get information about Main Client Allowed Link URLs. # Update Main Client callback urls Source: https://docs.meshconnect.com/api-reference/account-management/main-clients/update-main-client-callback-urls https://admin-api.meshconnect.com/swagger/AdminAPI/swagger.json post /admin/api/v1/Client/callbackUrls Update information about Main Client Allowed Link URLs. Allowed Link URLs of Main Client will only be used for those Registered clients, that don't have any Allowed Link URLs specified. # Add new Registered client Source: https://docs.meshconnect.com/api-reference/account-management/registered-clients/add-new-registered-client https://admin-api.meshconnect.com/swagger/AdminAPI/swagger.json post /admin/api/v1/SubClient Create new Registered client with specified data. Client will be created without Logo URL. In order to specify a Logo URL, send separate Update Registered Logo request along with id of just created client. # Delete Registered client Source: https://docs.meshconnect.com/api-reference/account-management/registered-clients/delete-registered-client https://admin-api.meshconnect.com/swagger/AdminAPI/swagger.json delete /admin/api/v1/SubClient/{id} Delete Registered client by id. # Get all Registered clients Source: https://docs.meshconnect.com/api-reference/account-management/registered-clients/get-all-registered-clients https://admin-api.meshconnect.com/swagger/AdminAPI/swagger.json get /admin/api/v1/SubClient Get information about all Registered clients. # Get Registered client Source: https://docs.meshconnect.com/api-reference/account-management/registered-clients/get-registered-client https://admin-api.meshconnect.com/swagger/AdminAPI/swagger.json get /admin/api/v1/SubClient/{id} Get information about the Registered client of specified identifier. # Remove Registered client Logo Source: https://docs.meshconnect.com/api-reference/account-management/registered-clients/remove-registered-client-logo https://admin-api.meshconnect.com/swagger/AdminAPI/swagger.json delete /admin/api/v1/SubClient/{id}/logo Remove logo of Registered client. # Update Registered client Source: https://docs.meshconnect.com/api-reference/account-management/registered-clients/update-registered-client https://admin-api.meshconnect.com/swagger/AdminAPI/swagger.json put /admin/api/v1/SubClient/{id} Update information about already Registered client by client id. This request does not support updating client Logo URL. In order to update a Logo URL, send separate Update Registered Logo request along with id of the client. # Update Registered client Logo Source: https://docs.meshconnect.com/api-reference/account-management/registered-clients/update-registered-client-logo https://admin-api.meshconnect.com/swagger/AdminAPI/swagger.json post /admin/api/v1/SubClient/{id}/logo Adds or update a logo for Registered client. Allowed file extensions are ".png", ".jpg", ".jpeg". Allowed file MIME types are "image/png", "image/jpeg", "image/jpg". Maximum file size is 5MB. Upload logo as form data with key 'logoFile'. # Get fiat holdings Source: https://docs.meshconnect.com/api-reference/balance/get-account-balance post /api/v1/balance/get Get real-time account fiat balances. # Get aggregated portfolio fiat balances Source: https://docs.meshconnect.com/api-reference/balance/get-aggregated-portfolio-fiat-balances get /api/v1/balance/portfolio Get cached aggregated fiat balances from all connected integrations. # Get integrations health status Source: https://docs.meshconnect.com/api-reference/managed-account-authentication/get-health-status get /api/v1/status Get the list of supported institutions and their health statuses. # Get Link token Source: https://docs.meshconnect.com/api-reference/managed-account-authentication/get-link-token-with-parameters post /api/v1/linktoken Get a short-lived, one-time use token for initializing a Link session using the client-side SDKs # Refresh auth token Source: https://docs.meshconnect.com/api-reference/managed-account-authentication/refresh-auth-token post /api/v1/token/refresh Refresh auth token of the connected institution. Some institutions do not require tokens to be refreshed. The following institutions require custom flows: WeBull: AuthToken should be provided along with the RefreshToken Vanguard: security settings may activate MFA, requiring user action. If MFA is triggered, a second refresh request should be sent. Second request should contain MFA code and access token obtained from initial response # Remove connection Source: https://docs.meshconnect.com/api-reference/managed-account-authentication/remove-connection delete /api/v1/account Remove connection to the financial institution and erase all related data completely. # Get available integrations Source: https://docs.meshconnect.com/api-reference/managed-account-authentication/retrieve-the-list-of-all-available-integrations get /api/v1/integrations Returns a list of integrations with details including the integration ID, name, type, DeFi wallet provider ID, and categories. # Get deposit address Source: https://docs.meshconnect.com/api-reference/managed-transfers/get-deposit-address post /api/v1/transfers/managed/address/get Get or generate a cryptocurrency deposit address that can be used to transfer assets to the financial institution # Get integrations Source: https://docs.meshconnect.com/api-reference/managed-transfers/get-integrations get /api/v1/transfers/managed/integrations **Get supported integrations list.** --- Get the list of all integrations supported by Mesh to perform transfers, including which tokens and networks are supported. # Get deposit addresses Source: https://docs.meshconnect.com/api-reference/managed-transfers/get-list-of-deposit-addresses post /api/v1/transfers/managed/address/list Get or generate a cryptocurrency deposit address that can be used to transfer assets to the financial institution # Get networks Source: https://docs.meshconnect.com/api-reference/managed-transfers/get-networks get /api/v1/transfers/managed/networks **Get supported networks list.** --- Get the list of all networks supported by Mesh to perform transfers, including which tokens and integrations are supported. The response always includes validation data: - `addressPattern`: The regex pattern for validating addresses on this network - `contractAddress`: For each token, the contract address (if applicable) to enable ERC20 contract address validation # Get supported tokens Source: https://docs.meshconnect.com/api-reference/managed-transfers/get-supported-tokens-list get /api/v1/transfers/managed/tokens Get the list of all tokens supported by Mesh to perform transfers, including which networks and integrations are supported. # Get Mesh transfers Source: https://docs.meshconnect.com/api-reference/managed-transfers/get-transfers-initiated-by-mesh get /api/v1/transfers/managed/mesh Get cryptocurrency transfers initiated by Mesh on exchanges or self-custody wallets. # Get quote Source: https://docs.meshconnect.com/api-reference/managed-transfers/quote-transfer post /api/v1/transfers/managed/quote Get a quote for transferring a fiat amount from a brokerage account in a given cryptocurrency over a specified network. Returns min and max fees and amounts to account for different funding sources (existing crypto balance, cash balance or ACH/debit deposit). Currently only supported for Coinbase. # Get aggregated portfolio Source: https://docs.meshconnect.com/api-reference/portfolio/get-aggregated-portfolio get /api/v1/holdings/portfolio Get the aggregated portfolio of the user containing market values. # Get crypto holdings Source: https://docs.meshconnect.com/api-reference/portfolio/get-holdings post /api/v1/holdings/get Obtain assets from the connected investment account. Performs realtime API call to the underlying integration. # Get holdings values Source: https://docs.meshconnect.com/api-reference/portfolio/get-holdings-values post /api/v1/holdings/value Obtain assets from the connected investment account and return total value and performance. Performs realtime API call to the underlying integration. # Get deposit address Source: https://docs.meshconnect.com/api-reference/transfers/get-deposit-address post /api/v1/transfers/address/get Get or generate a cryptocurrency deposit address that can be used to transfer assets to the financial institution # Get profile data Source: https://docs.meshconnect.com/api-reference/verify/verify post /api/v1/exchange/verify Returns basic profile data of the user's exchange account. Available data varies by exchange and linked account. # Get verified addresses Source: https://docs.meshconnect.com/api-reference/verify/wallet get /api/v1/wallets/verify # 15min Quickstart Source: https://docs.meshconnect.com/build/15min-quickstart New to Mesh? Read [How it all fits together](/resources/how-it-fits) first for a quick orientation on how the Link Token, SDK, callbacks, and webhooks connect — then come back here to build. This guide gets you from zero to a working Mesh deposit flow in sandbox — no prior Mesh knowledge required. In 15mins, you'll have made your first API call, launched the Mesh SDK, and seen a test transfer complete. The full guide series goes deeper on each topic. This is just to get you moving fast. **Before you start** * You have a Mesh dashboard account. If you don't have one yet, reach out to your Mesh representative to request an invitation. * You're building a web app (this guide uses the web SDK). ## Get your sandbox credentials Log into the Mesh dashboard and navigate to **Account > API keys > API keys**. Create a new key with **Read & Write** permissions (required for transfers). You'll need two things: * **Client ID** — your unique identifier, shown next to the key * **API key (sandbox)** — starts with `sk_sandbox_...` Also note the **sandbox base URL**: `https://sandbox-integration-api.meshconnect.com` While you're here, go to **Account > API keys > Access** and add the domain where you'll be running your app (eg. `localhost:3000` for local development). The Mesh SDK will refuse to load on any domain not on this list. ## Install the Mesh web SDK ```shell theme={null} # with npm npm install --save @meshconnect/web-link-sdk # with yarn yarn add @meshconnect/web-link-sdk ``` Details on [mesh-web-sdk](https://github.com/FrontFin/mesh-web-sdk/tree/main/packages/link#meshconnectweb-link-sdk). ## Request a Link Token A Link Token is how you start every user session. You request it from your server, and the parameters configure what the user can do in Link. Here's the minimal request for a deposit flow: ```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace (your Mesh Client ID) --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace (your Mesh API Key) --data ' { "userId": "xxx", // Replace (a unique user identifier) "restrictMultipleAccounts": true, // "true" is standard and is used for any tranfer flow "transferOptions": { "transactionId": "xxx", // Replace (optional: a unique transaction identifier to tie back to your data) "transferType": "deposit", // Ensures the language and flow matches your user's mental model for a deposit "isInclusiveFeeEnabled": false, // "false" is standard, meaning any applicable fees are on top of the deposit amount "generatePayLink": false, // "true" is to be used if you're launching Mesh in a separate webpage (see more about "PayLinks" in the section about launching the Mesh SDK) "toAddresses": [ // Replace (example array below, update with one or multiple asset/network/address combos for testing) { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana "symbol": "USDC", "address": "YOUR_SOL_ADDRESS", // Replace (destination address for this asset/network, user-specific if necessary) }, { "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611", // Ethereum "symbol": "USDC", "address": "YOUR_ETH_ADDRESS", // Replace (destination address for this asset/network, user-specific if necessary) }, { "networkId": "aa883b03-120d-477c-a588-37c2afd3ca71", // Base "symbol": "USDC", "address": "YOUR_BASE_ADDRESS", // Replace (destination address for this asset/network, user-specific if necessary) } ], } } ' ``` The response will include a `linkToken` — hold onto it for the next step. **Why does this live server-side?** Your API key is in this request, so it must be made from your server, not the browser. Your frontend receives the `linkToken` and uses it to initialize the SDK. ## Initialize and launch the Mesh SDK On your frontend, initialize a session of Link in the Mesh SDK using the `linkToken` from Step 3: ```javascript theme={null} const connection = createLink({ renderType: 'overlay', theme: 'system', language: 'system', displayFiatCurrency: 'USD', }) connection.openLink(linkToken) ``` When `openLink` is called, Mesh Link opens and guides the user through connecting their account and approving the deposit. ## Test it with a sandbox account In sandbox, use one of these pre-configured test accounts to simulate a real exchange connection: | Exchange | Username | Password | OTP | | -------- | -------- | --------- | -------- | | Coinbase | `Mesh` | `Pass123` | `123456` | | Binance | `Mesh` | `Pass123` | `123456` | See the [Sandbox & Testnets](/resources/sandbox-testnets) guide for the full list of test accounts and wallet testing instructions. ## What's next You've got a working deposit flow. Here's where to go from here: * **Start the full walkthrough**: [Prepare to build](/build/prepare-to-build) is the complete step-by-step guide series. * **Deepen your Link Token knowledge**: [Fetch a Link Token](/build/fetch-link-token) covers all use cases — payments, onramps, withdrawals, and wallet verification. * **Explore all launch options**: [Launch the Mesh SDK](/build/launch-sdk) covers embedded mode, overlay, and Paylinks across all 5 platforms. * **Handle more events**: [Use Mesh's callback functions](/build/callbacks) covers the full callback API. * **Build a great return-user experience**: [Supercharge return-users](/build/return-users) covers Mesh Managed Tokens. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — 15-minute Quickstart** End-to-end sandbox deposit integration in 5 steps using the Web SDK. No prior Mesh knowledge required. **Steps**: (1) Get sandbox API key + Client ID from Mesh dashboard → (2) Install `@meshconnect/web-link-sdk` → (3) `POST /api/v1/linktoken` server-side with `userId`, `transferType: "deposit"`, `toAddresses[]` → (4) `createLink({renderType, theme, language, displayFiatCurrency}).openLink(linkToken)` client-side → (5) Test with username `Mesh`, password `Pass123`, OTP `123456` **Sandbox base URL**: `https://sandbox-integration-api.meshconnect.com` | **Auth headers**: `X-Client-Id`, `X-Client-Secret` **Add allowed domain**: Dashboard > Account > API keys > Access (must include `localhost:3000` for local dev) **Key networkIds**: Solana `0291810a-5947-424d-9a59-e88bb33e999d` | Ethereum `e3c7fdd8-b1fc-4e51-85ae-bb276e075611` | Base `aa883b03-120d-477c-a588-37c2afd3ca71` **Step 3 — Link Token request (server-side)**: ```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace (your Mesh Client ID) --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace (your Mesh API Key) --data ' { "userId": "xxx", // Replace (a unique user identifier) "restrictMultipleAccounts": true, // "true" is standard and is used for any tranfer flow "transferOptions": { "transactionId": "xxx", // Replace (optional: a unique transaction identifier to tie back to your data) "transferType": "deposit", // Ensures the language and flow matches your user's mental model for a deposit "isInclusiveFeeEnabled": false, // "false" is standard, meaning any applicable fees are on top of the deposit amount "generatePayLink": false, // "true" is to be used if you're launching Mesh in a separate webpage (see more about "PayLinks" in the section about launching the Mesh SDK) "toAddresses": [ // Replace (example array below, update with one or multiple asset/network/address combos for testing) { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana "symbol": "USDC", "address": "YOUR_SOL_ADDRESS", // Replace (destination address for this asset/network, user-specific if necessary) }, { "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611", // Ethereum "symbol": "USDC", "address": "YOUR_ETH_ADDRESS", // Replace (destination address for this asset/network, user-specific if necessary) }, { "networkId": "aa883b03-120d-477c-a588-37c2afd3ca71", // Base "symbol": "USDC", "address": "YOUR_BASE_ADDRESS", // Replace (destination address for this asset/network, user-specific if necessary) } ], } } ' ``` **Step 4 — SDK initialization (client-side)**: ```javascript theme={null} const connection = createLink({ renderType: 'overlay', theme: 'system', language: 'system', displayFiatCurrency: 'USD', onIntegrationConnected: payload => console.log('[connected]', payload), onTransferFinished: payload => console.log('[transfer]', payload), onExit: payload => console.log('[exit]', payload), onEvent: ev => console.log('[event]', ev) }) connection.openLink(linkToken) ``` **Next steps**: Fetch a Link Token (full param reference) → Launch the Mesh SDK (all 5 platforms) → Use Mesh's callback functions → Supercharge return-users (MMT) # Use Mesh's callback functions Source: https://docs.meshconnect.com/build/callbacks By the end of this guide, you'll understand how to use each SDK callback function to respond to key events in the user journey. **Before you start** * You have the Mesh SDK initialized and launching (see [Launch the Mesh SDK](/build/launch-sdk)) ## Overview When a user interacts with Link, Mesh fires SDK events that you can respond to in your app. The four callback functions below cover the most important moments in the user journey: connecting an account, completing a transfer, exiting, and responding to granular in-flow events. ## SDK callback functions | SDK callback function | Description | Key uses | Payload details | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`onIntegrationConnected()`** | Allows you to run specific business logic when the user has successfully completed connecting an account. | Capture accessTokens to:
• pass to Mesh SDK for a return-user experience
• pass to Mesh endpoints to get user deposit addresses or balances | • **`accessToken`**: the access and refresh tokens to the connected account with some basic metadata about the account and tokens
• **`brokerBrandInfo`**: links to icons and logos for the connected integration | | **`onTransferFinished()`** | Allows you to run specific business logic when the user has successfully completed a transfer. | Show user a banner or notification acknowledging a pending transaction. Example: *"Your `99.99` `USDC` deposit is `pending` and will be credited to your account when it receives enough network confirmations."* | • **`status`**: pending / succeeded / failed
• **`userId`**: A unique, client-specific user identifier
• **`transactionId`**: A unique, client-specific transfer identifier
• **`txId`**: A unique, exchange-specific transfer identifier
• **`transferId`**: A unique, Mesh-specific transfer identifier
• **`txHash?`**: A unique blockchain identifier
• **`fromAddress`**: Address transfer is sent from
• **`toAddress`**: Address transfer is sent to
• **`symbol`**: Symbol of asset being transferred
• **`amount`**: Amount being transferred
• **`amountInFiat`**: Fiat equivalent of transfer amount
• **`totalAmountInFiat`**: Total amount transferred, including transfer-related fees
• **`networkId`**: Selected network identifier
• **`networkName`**: Selected network name
• **`refundAddress`**: The address that the user can receive back to | | **`onExit()`** | Allows you to run specific business logic when the user has exited Link at some point. | Show the user some experience when they exit Link without successfully completing a transfer. | • **`errorMessage`**: Descriptive error message, if applicable
• **`summary`**: // optional
• **`page`**: the page the user was on when they exited
• **`selectedIntegration`**: Name and `Id` of integration
• **`transfer`**: `previewId` and other details about the transfer preview | | **`onEvent()`** | Allows you to run specific business logic in more granular scenarios, like when the user exits Link from specific parts in the user journey. The events that can be used in this callback function are listed below. | | Different payload structure for different events | ## SDK-specific instructions for setting up callback functions ```javascript theme={null} const connection = createLink({ renderType: 'overlay', // Opens SDK on top of your web app theme: 'system', // Possible values: 'system', 'dark', 'light' language: 'system', // See "Multi-language support" guide displayFiatCurrency: 'USD', // See "Fiat currency support" guide accessTokens: accessTokens, // See "Supercharge return users" guide onIntegrationConnected: payload => { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs console.log('[MESH LINK integration connected]', payload) }, onTransferFinished: payload => { // Called after a transfer flow completes successfully console.log('[MESH LINK transfer finished]', payload) }, onExit: () => { // Called when Link is closed console.log('[MESH LINK exited]') }, onEvent: ev => { // Allows you to handle other specific events console.log('[MESH LINK event]', ev) } }) connection.openLink(linkToken) // Opens a popup that overlays your app ``` ```swift theme={null} func launchMeshLink( linkToken: String, viewController: UIViewController, accessTokens: [IntegrationAccessToken]? = nil ) { var linkHandler: LinkHandler? let settings = LinkSettings( accessTokens: accessTokens, // See "Supercharge return users" guide language: "system", // See "Multi-language support" guide displayFiatCurrency: "USD", // See "Fiat currency support" guide theme: .system // Possible values: 'system', 'dark', 'light' ) // Called after a user connects an exchange, wallet, or other integration. let onIntegrationConnected: (LinkPayload) -> Void = { linkPayload in switch linkPayload { case .accessToken(let accessTokenPayload): print(accessTokenPayload) case .delayedAuth(let delayedAuthPayload): print(delayedAuthPayload) @unknown default: print("unknown LinkPayload") } } // Called after a transfer flow completes successfully or fails. let onTransferFinished: (TransferFinishedPayload) -> Void = { transferFinishedPayload in switch transferFinishedPayload { case .success(let successPayload): print(successPayload) case .error(let errorPayload): print(errorPayload) @unknown default: print("unknown TransferFinishedPayload") } } // Called throughout the Link flow so you can track user progress. let onEvent: ([String: Any]?) -> Void = { payload in print("Event: \(payload ?? [:])") } // Called when the user exits Link let onExit: (Bool?) -> Void = { showAlert in // showAlert is true when 'x' button is tapped // showAlert is false when 'Done' button is tapped on a Transfer Success screen if showAlert ?? false { // in case a custom alert is implemented, linkHandler?.closeLink() must be called to close Link linkHandler?.showExitAlert() // default Exit alert } else { linkHandler?.closeLink() } } let configuration = LinkConfiguration( linkToken: linkToken, settings: settings, disableDomainWhiteList: false, onIntegrationConnected: onIntegrationConnected, onTransferFinished: onTransferFinished, onEvent: onEvent, // onExit is optional, a default alert is shown in case onExit is omitted onExit: onExit ) let result = configuration.createHandler() // createHandler validates the configuration before Link is presented. switch result { case .success(let handler): linkHandler = handler // Present Link from the current UIViewController. handler.present(in: viewController) case .failure(let error): print(error) @unknown default: print("unknown LinkResult") } } ``` ```kotlin theme={null} // Register the Link launcher as an Activity or Fragment property private val linkLauncher = registerForActivityResult(LaunchLink()) { result -> when (result) { is LinkSuccess -> { // Called when Link returns payloads, such as a connected account or completed transfer. Log.i(LOG_TAG, "LinkSuccess: ${result.payloads}") } is LinkExit -> { // Called when Link exits without payloads, including user exits or errors. Log.i(LOG_TAG, "LinkExit: ${result.errorMessage}") } } } private fun launchMeshLink(linkToken: String, accessTokens: List? = null) { // Create LinkConfiguration val configuration = LinkConfiguration( token = linkToken, // Link token returned by the Mesh backend theme = LinkTheme.SYSTEM, // Possible values: 'system', 'dark', 'light' language = "system", // See "Multi-language support" guide displayFiatCurrency = "USD", // See "Multi-language support" guide accessTokens = accessTokens, // See "Supercharge return users" guide ) // Launch linkLauncher.launch(configuration) } ``` ```javascript theme={null} return ( { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs console.log('[MESH LINK integration connected]', payload); }} onTransferFinished={(payload: TransferFinishedPayload) => { // Called after a transfer flow completes successfully or fails. console.log('[MESH LINK transfer finished]', payload); }} onEvent={(event: LinkEventType) => { // Allows you to handle other specific events console.log('[MESH LINK event]', event); }} onExit={(err?: string) => { // Called when Mesh Link exits, completes, is closed by the user, or fails during setup. console.log('[MESH LINK exited]'); }} /> ); ``` ```dart theme={null} Future _showMeshLinkPage( String linkToken, { List accessTokens = const [], }) async { // Show MeshSdk final result = await MeshSdk.show( context, configuration: MeshConfiguration( language: 'system', // See "Multi-language support" guide displayFiatCurrency: 'USD', // See "Fiat currency support" guide theme: ThemeMode.system, integrationAccessTokens: accessTokens, // See "Supercharge return users" guide linkToken: linkToken, onEvent: (event) { // Allows you to handle other specific events print('Mesh event: $event'); }, onError: (errorType) { // Callback for when Mesh Link exits due to an error print('Mesh exit: $errorType'); }, onSuccess: (payload) { // Callback for when the Mesh Link is successfully completed print('Mesh success: ${payload.page}'); }, onIntegrationConnected: (integration) { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs print('Integration connected: $integration'); }, onTransferFinished: (transfer) { // Called after a transfer flow completes successfully or fails. print('Transfer finished: $transfer'); }, ), ); // Handle the result switch (result) { case MeshSuccess(): print('Mesh link finished successfully'); case MeshError(): print('Mesh link error: ${result.type}'); } // Alternatively, use `when` method result.when( success: (success) { final payload = success.payload; print('Mesh link success: ${payload.page}'); }, error: (error) { final errorType = error.type; print('Mesh link error: $errorType'); }, ); } ``` ## What's next Next up: [Supercharge return-users](/build/return-users) — once you're capturing `accessTokens` via `onIntegrationConnected`, you're ready to set up Mesh Managed Tokens for a seamless return-user experience. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Use Mesh's callback functions** The 4 SDK callback functions for responding to key user journey events. Wire all 4 in every SDK initialization. **onIntegrationConnected(payload)** — fires when user connects an exchange or wallet. Key payload: `accessToken.accountTokens[].tokenId` (store this for MMT) | `accessToken.brokerType` | `brokerBrandInfo` (icons/logos) **onTransferFinished(payload)** — fires when transfer flow completes. Use for immediate UI updates only; use webhooks for business logic. Key payload: `status` (pending/succeeded/failed) | `userId` | `transactionId` | `txId` (exchange-specific) | `transferId` (Mesh-specific) | `txHash` | `symbol` | `amount` | `amountInFiat` | `totalAmountInFiat` | `fromAddress` | `toAddress` | `refundAddress` | `networkId` | `networkName` **onExit()** — fires when user closes Link at any point. Key payload: `errorMessage` | `summary.page` | `selectedIntegration` (name + Id) | `transfer.previewId` **onEvent(ev)** — fires for granular in-flow events. Use for analytics and specific UX logic. See Mesh SDK events guide for full event list. **Critical**: `onTransferFinished` fires when the provider acknowledges the request — not when it confirms on-chain. Always use webhooks for final confirmation before crediting users or releasing inventory. **Web SDK — canonical initialization with all 4 callbacks**: ```javascript theme={null} const connection = createLink({ renderType: 'overlay', // Opens SDK on top of your web app theme: 'system', // Possible values: 'system', 'dark', 'light' language: 'system', // See "Multi-language support" guide displayFiatCurrency: 'USD', // See "Fiat currency support" guide accessTokens: accessTokens, // See "Supercharge return users" guide onIntegrationConnected: payload => { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs console.log('[MESH LINK integration connected]', payload) }, onTransferFinished: payload => { // Called after a transfer flow completes successfully console.log('[MESH LINK transfer finished]', payload) }, onExit: () => { // Called when Link is closed console.log('[MESH LINK exited]') }, onEvent: ev => { // Allows you to handle other specific events console.log('[MESH LINK event]', ev) } }) connection.openLink(linkToken) // Opens a popup that overlays your app ``` # Fetch a Link Token Source: https://docs.meshconnect.com/build/fetch-link-token By the end of this guide, you'll know how to request a Link Token for any use case: deposits, payments, onramps, withdrawals, and wallet verification. **Before you start** * You have a Mesh dashboard account with a sandbox API key (see [Prepare to build](/build/prepare-to-build)) * You know which use case(s) you're building for ## Overview A Link token, as the name implies, is an access key for a session of Link, Mesh's SDK. You'll request it with an API call to Mesh, and the parameters you include will configure the user session. This is how you'll start every user session. This guide will walk you through how to request a Link token, including code snippets for specific use cases. ## How to request a Link token ### General information **Endpoint**: **Get Link token with parameters** ([/api/v1/linktoken](https://docs.meshconnect.com/api-reference/managed-account-authentication/get-link-token-with-parameters)) You will start every user session with this request. Its parameters configure the user's experience within Link. * Tokens are short-lived (must be used within 10mins). * Tokens are one-time use. ### Use-case-specific example requests
X-Client-Id required
Your Mesh Client ID.
X-Client-Secret required
Your Mesh API Key.
userId required
A unique, persistent user identifier. Personally identifiable information such as an email address or phone number should not be used. 300 characters length maximum.
restrictMultipleAccounts optional

Defaults to `true`, which is standard used for any transfer flow.

On some non-transfer flows (ie. "read" use cases), a user could connect multiple accounts in a row if this value were `false`. Sometimes valuable for withdrawal use cases to let the user create an external "address book".

integrationId optional

A unique Mesh identifier representing a specific integration.

Use the **Get integrations** endpoint ([/api/v1/transfers/managed/integrations](https://docs.meshconnect.com/api-reference/managed-transfers/get-integrations)) to pull a list of integrations and the corresponding Mesh `integrationId`. These values won't change, so you do not have to hit this endpoint before each Link Token request.

To be used if the user selects the integration in your UX before launching Mesh (most commonly in an `onramp` flow).

transferOptions.transactionId optional required for PayLinks transfers
A unique transaction identifier used to tie back to your data or track this transaction in future calls to Mesh.
transferOptions.transferType optional
Ensures the language and flow matches your user's mental model for the type of transfer they're doing. Defaults to `deposit`.
transferOptions.isInclusiveFeeEnabled optional

Specifies if fees should be included in the amount to transfer.

`false` is standard for `deposit` and `payment`, meaning any applicable fees are on top of the deposit/payment amount. `true` is standard for `onramp`, meaning the amount the user receives is the amount specified minus applicable fees.

transferOptions.generatePayLink optional

When `true`, this request will return a url in addition to the Link token that can be used to launch Mesh Link in a separate web page.

This should only be used if you're launching Mesh in a separate webpage (see more about "PayLinks" in the [Launch the Mesh SDK](/build/launch-sdk) guide).

transferOptions.amountInFiat optional

The fiat-equivalent amount of the symbol to be purchased.

To be used if the user enters an amount in your UX before launching Mesh (most commonly in an `onramp` flow).

transferOptions.toAddresses.networkId optional required for transfers

Mesh's unique identifier for the network to be used for this `toAddress`.

Use the **Get networks** endpoint ([/api/v1/transfers/managed/networks](https://docs.meshconnect.com/api-reference/managed-transfers/get-networks)) to pull a list of all supported networks and the corresponding Mesh `networkId`. These values won't change, so you do not have to hit this endpoint before each Link Token request.

transferOptions.toAddresses.symbol optional required for transfers
The symbol of the asset that can be transferred to this `toAddress`.
transferOptions.toAddresses.address optional required for transfers
The destination to which the specified `symbol` can be sent on the specified `networkId`.
transferOptions.toAddresses.amount optional required for payments

Exact amount of the asset that should be transferred.

This parameter is optional for `deposit` and `onramp`, but required when transferType: `payment`. Not to be used in combination with the `amountInFiat` field.

transferOptions.toAddresses.displayAmountInFiat optional

A fiat-equivalent amount that will be shown to the user in the Mesh UI.

This ensures a consistent experience from your checkout experience to Mesh. It will only be used if it is within 1% of the `amountInFiat` Mesh determines based on its pricing data. This is generally used for non-stablecoin payments, as Mesh maps stablecoins to a 1:1 price with USD for display purposes.

verifyWalletOptions.verificationMethods optional required for verifications

Method by which the user must verify their self-hosted wallet.

Enum to support future verification methods, but only one option for now.

verifyWalletOptions.message optional

The message the user should sign in their wallet.

The exact language isn't important. Mesh has standard language if this isn't provided.

verifyWalletOptions.networkId optional

A unique Mesh identifier for the network on which the user must verify their self-hosted address.

Use the **Get networks** endpoint ([/api/v1/transfers/managed/networks](https://docs.meshconnect.com/api-reference/managed-transfers/get-networks)) to pull a list of all supported networks and the corresponding Mesh `networkId`. These values won't change, so you do not have to hit this endpoint before each Link Token request.

To be used if you need your user to verify an address on a specific network. Should not be used in combination with `networkType`.

verifyWalletOptions.networkType optional

The network type on which the user must verify their self-hosted address.

To be used if you need your user to verify an address on a particular network type (eg. EVM). Should not be used in combination with `networkId`.

verifyWalletOptions.address optional

A list of address from which the user can verify ownership. User is not allowed to verify an address outside this list.

Used if you need your user to verify ownership of a specific address. If not provided, user can verify ownership of any self-hosted address they connect.

```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "transferOptions": { "transactionId": "UNIQUE_TRANSACTION_ID", // Replace "transferType": "deposit", "isInclusiveFeeEnabled": false, "generatePayLink": false, "toAddresses": [ // Replace (example array below) { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana "symbol": "USDC", // Replace "address": "xxx", // Replace }, { "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611", // Ethereum "symbol": "USDC", // Replace "address": "xxx", // Replace }, { "networkId": "aa883b03-120d-477c-a588-37c2afd3ca71", // Base "symbol": "USDC", // Replace "address": "xxx", // Replace } ], } } ' ``` * The `toAddresses` are the possible destinations at which user deposits can be received. This object is an array. The example request shows 3 possible destination addresses (USDC.sol, USDC.eth, USDC.base), but you should **provide all possible destination addresses** to maximize the chances that Mesh can make a transaction happen. **Want more help?** Use our [toAddress generator](https://mesh-address-generator.vercel.app/?section=link). This tool helps you quickly construct your toAddress array. * There is an additional parameter for `addressTag` within the `toAddress` object. Be sure to include that for assets like XRP or XLM if necessary for proper attribution to your user accounts. * There are additional parameters for `minAmount` & `minAmountInFiat` within the `toAddress` object. Be sure to include this if you have deposit minimums. ```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "transferOptions": { "transactionId": "UNIQUE_TRANSACTION_ID", // Replace "transferType": "payment", "isInclusiveFeeEnabled": false, "generatePayLink": false, "toAddresses": [ // Replace (example array below) { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana "symbol": "USDC", // Replace "address": "xxx", // Replace "amount": 99.99 // Replace }, { "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611", // Ethereum "symbol": "USDC", // Replace "address": "xxx", // Replace "amount": 99.99 // Replace }, { "networkId": "aa883b03-120d-477c-a588-37c2afd3ca71", // Base "symbol": "USDC", // Replace "address": "xxx", // Replace "amount": 99.99 // Replace }, { "networkId": "03dee5da-7398-428f-9ec2-ab41bcb271da", // Bitcoin "symbol": "BTC", // Replace "address": "xxx", // Replace "amount": 0.00141, // Replace "displayAmountInFiat": 99.99 // Replace } ], } } ' ``` * The `toAddresses` are the possible destinations at which user payments can be received. This object is an array. The example request shows 4 possible destination addresses (USDC.sol, USDC.eth, USDC.base, BTC.btc), but you should **provide all possible destination addresses** to maximize the chances that Mesh can make a transaction happen. **Want more help?** Use our [toAddress generator](https://mesh-address-generator.vercel.app/?section=link). This tool helps you quickly construct your toAddress array. * The `amount` parameter is the amount of the `symbol` specified in this object that must be transferred (ie. the total payment amount, in crypto). Do not use the `amountInFiat` parameter, as Mesh cannot guarantee exchange rates. * There is an additional parameter for `addressTag` within the `toAddress` object. Be sure to include that for assets like XRP or XLM if necessary for proper attribution on your end. ```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "integrationId": "5620bf49-3240-4f85-8b4f-9dd6261597e2", // Binance Connect (Replace) "transferOptions": { "transactionId": "UNIQUE_TRANSACTION_ID", // Replace "transferType": "onramp", "amountInFiat": 100.00, // Replace "isInclusiveFeeEnabled": true, "generatePayLink": false, "toAddresses": [ // Replace (this could be a full array, or only one destination if the user selects a token/network in your UX before launching Mesh) { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana "symbol": "USDC", // Replace "address": "xxx", // Replace } ], } } ' ``` * There is an additional parameter for `addressTag` within the `toAddress` object. Be sure to include that for assets like XRP or XLM if necessary for proper attribution to your user accounts. **Want more help?** If you're passing an array of multiple possible destinations for your user's purchase, use our [toAddress generator](https://mesh-address-generator.vercel.app/?section=link). This tool helps you quickly construct your toAddress array. * There are additional parameters for `minAmount` & `minAmountInFiat` within the `toAddress` object. Be sure to include this if you have deposit minimums. * Read more about this in [Add Mesh onramp integrations to your "Buy" lineup](/extend/onramp). ```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, } ' ``` * This user flow will end immediately after they connect their account. * At that point, you will receive an accessToken for the user's account, which you can use to hit Mesh's **Get deposit addresses** endpoint ([/api/v1/transfers/managed/address/list](https://docs.meshconnect.com/api-reference/managed-transfers/get-list-of-deposit-addresses)). * Read more about this in the [Add Mesh to your withdrawal flow](/extend/withdrawal) guide. ```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "integrationId": "757e703f-a8fe-4dc4-d0ec-08dc6737ad96", // Phantom, replace, optional "verifyWalletOptions": { "verificationMethods": [ "signedMessage" ], "message": "MESSAGE_TO_BE_SIGNED", // Replace "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana, replace, optional "networkType": "solana", // Replace, optional "addresses": [ // Optional "xxx" // Replace ], } } ' ``` * Wallet verification is fully compatible with the deposit & payment use cases. If you require users to verify that they own a self-custody wallet before initiating a deposit or payment from that wallet, you can add these configurations into the same Link Token request. * Read more about this in [Verify self-hosted wallets](/extend/verify-wallets). ## What's next Next up: [Launch the Mesh SDK](/build/launch-sdk) — use the Link Token you just requested to initialize and launch a user-facing SDK session. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Fetch a Link Token** Every Mesh session starts server-side with a Link Token request. Tokens are short-lived (10 min) and single-use. **Endpoint**: `POST /api/v1/linktoken` **Required params**: `userId` (unique, persistent, non-PII, ≤300 chars) — the only universally required body param. **Conditionally required**: `transferOptions.toAddresses[].networkId`, `.symbol`, `.address` — required for any transfer flow | `transferOptions.toAddresses[].amount` — required for payment flows (not for deposit/onramp) | `verifyWalletOptions.verificationMethods` — required when using wallet verification **Key optional params**: `transferOptions.transferType` (deposit/payment/onramp — defaults to deposit) | `integrationId` (deep-link to specific provider) | `accessTokens` (MMT return-user array) | `restrictMultipleAccounts` (default true) | `generatePayLink` (true returns `paymentLink` URL) | `transferOptions.isInclusiveFeeEnabled` (false for deposit/payment; true for onramp) | `transferOptions.amountInFiat` | `transferOptions.toAddresses[].addressTag` (for XRP, XLM) | `transferOptions.toAddresses[].minAmount` / `minAmountInFiat` **verifyWalletOptions** (wallet ownership): `verificationMethods: ["signedMessage"]` | `message` | `networkId` or `networkType` (not both) | `addresses[]` **Get networkIds**: Use `GET /api/v1/transfers/managed/networks` to fetch the full list of supported networks and their `networkId` values. These values don't change — safe to cache permanently, no need to call this endpoint before every Link Token request. **Key networkIds**: Solana `0291810a-5947-424d-9a59-e88bb33e999d` | Ethereum `e3c7fdd8-b1fc-4e51-85ae-bb276e075611` | Base `aa883b03-120d-477c-a588-37c2afd3ca71` | Bitcoin `03dee5da-7398-428f-9ec2-ab41bcb271da` **Tool**: [toAddress generator](https://mesh-address-generator.vercel.app/?section=link) — quickly construct your toAddresses array. **Deposit use case — canonical request body**: ```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "transferOptions": { "transactionId": "UNIQUE_TRANSACTION_ID", // Replace "transferType": "deposit", "isInclusiveFeeEnabled": false, "generatePayLink": false, "toAddresses": [ // Replace (example array below) { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana "symbol": "USDC", // Replace "address": "xxx", // Replace }, { "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611", // Ethereum "symbol": "USDC", // Replace "address": "xxx", // Replace }, { "networkId": "aa883b03-120d-477c-a588-37c2afd3ca71", // Base "symbol": "USDC", // Replace "address": "xxx", // Replace } ], } } ' ``` **Payment use case** — same as deposit but add `"amount": 99.99` to each `toAddress` entry and set `"transferType": "payment"`. **Onramp use case** — add `"integrationId": "...", "amountInFiat": 100.00, "isInclusiveFeeEnabled": true` and set `"transferType": "onramp"`. **Verify use case** — replace `transferOptions` with `"verifyWalletOptions": { "verificationMethods": ["signedMessage"], "message": "...", "networkId": "..." }`. # Prepare for go-live Source: https://docs.meshconnect.com/build/go-live By the end of this guide, you'll have a production API key, a registered webhook URI, and your integration pointing at Mesh's production environment — ready to launch. **Before you start** * Your integration is fully tested and working in sandbox * You've completed Mesh's business verification process (reach out to your Mesh representative if you haven't started this yet) ## Register a production webhook callback URI Production webhook events are separate from sandbox — registering a callback URI for sandbox doesn't carry over. Go to **Account > API keys > Webhooks > Production** in the Mesh dashboard to register your production callback URI. Setting up webhooks for the first time? See [Transfer status webhooks](/resources/webhooks) for full implementation details, including HMAC signature verification. ## Update to the latest SDK version Mesh regularly puts out SDK updates and communicates changes in client slack channels. It's worth a quick check before go-live to ensure you're on the latest and greatest.
🌐 Web : mesh-web-sdk
iOS : mesh-ios-sdk
Android : mesh-android-sdk
React Native : mesh-react-native-sdk
Flutter : mesh-flutter-sdk
## Generate a production API key You can generate a production key in the Mesh developer dashboard in **Account > API keys**. You can grant each key Read-only permissions, or Read & Write (most common, and required for any use case involving transfers). If you haven't done so already, you'll need to complete the following first: * Secure your account with 2FA * Complete Mesh's business verification process Image ## Point to Mesh's production environment Now that you're set up for production, update your integration to point to Mesh's production base URL: [https://integration-api.meshconnect.com](https://integration-api.meshconnect.com). ## Let's test together You should always thoroughly test the integration before going live, and we'd love to help. Ideally Mesh has a testing account in your app, is added to Testflight, etc. We've seen lots of Mesh implementations and have picked up plenty of tips and tricks along the way. Reach out to your Mesh representative to set up a testing session with the Mesh team — we want to make sure your launch goes smoothly. ## What's next You're live! Once you're up and running, explore the **Extend** guides to unlock additional capabilities, or visit **Further resources** for API references and integration-specific docs. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Prepare for go-live** Three steps to launch in production: generate production API key → register production webhook URI → update base URL. **Production API key**: Dashboard > Account > API keys > API keys > Production. Prerequisites: 2FA enabled, allowed IP ranges added, Mesh business verification complete. **Production webhook URI**: Dashboard > Account > API keys > Webhooks > Production. Sandbox webhooks do not carry over — register separately. Only one production URI at a time (can deactivate and replace). **Production base URL**: `https://integration-api.meshconnect.com` (Sandbox: `https://sandbox-integration-api.meshconnect.com`) **Recommendation**: Schedule a joint testing session with your Mesh representative before launch. # Launch the Mesh SDK Source: https://docs.meshconnect.com/build/launch-sdk By the end of this guide, you'll know how to initialize and launch the Mesh SDK embedded in your app, as an overlay, or via Paylinks. **Before you start** * You have a Mesh sandbox API key and the SDK installed for your platform (see [Prepare to build](/build/prepare-to-build)) * You know how to fetch a Link Token (see [Fetch a Link Token](/build/fetch-link-token)) ## Overview Mesh's client-side SDKs are supported on the following platforms:
🌐 Web
iOS
Android
React Native
Flutter
Those SDKs are home to **Link**, the Mesh-hosted experience that facilitates the user journey (ie. connecting an account, configuring / previewing / approving transfers, verifying wallet ownership, etc.). You'll launch an SDK session using a Link Token. This guide will walk you through how to launch an SDK session, including code snippets for each SDK. ## Initialize the Mesh SDK You have 3 options for how to launch Link. You should choose which one fits your UX and other needs the best. ### Option 1: Embedded (web SDK only) **Recommended for web implementations** Link can be embedded as an iframe into a container within your product surface area. This makes it feel more native to your product by minimizing the "seam" between your app and Mesh. You provide a custom iframe ID in the `openLink` function of the web SDK. While this path will feel more native to your platform, it also comes with a slightly more involved implementation. Please see the [Polish the experience](/build/polish) guide for important instructions on how to properly implement this experience. Image You'll include a custom iframe ID in the `openLink` function when embedding Link into a container in your product (see end of snippet).
renderType
`embedded` indicates this will not be an overlay, and will slightly modify the UX (importantly, the Mesh SDK nav bar goes away except for the back `<` button). When embedded, add a custom-iframe-id in the `openLink` function.
custom-iframe-id (not a parameter, but including here for clarity)
Lets you embed the Mesh SDK directly into a container within your product surface area.
language
You can use the `system` setting to match their device or browser default, or you can specify a language if you have such a setting in user profiles. See the [Multi-language support](/resources/multi-language) guide for more info on supported languages.
theme
You can use the `system` setting to match their device or browser default, or you can specify a theme if you have such a setting in user profiles.
displayFiatCurrency
Link defaults to USD, or you can specify a fiat currency if you have such a setting in user profiles. See the [Fiat currency support](/resources/fiat-currency) guide for more info on supported currencies
accessTokens
After users connect an exchange account, Mesh can maintain authorization so that the user doesn't have to login again. This enables a 1 or 2 click transfer experience for return users. See the [Use Mesh's callback functions](/build/callbacks) guide for more information on consuming Mesh SDK events and handling them with callback functions, and the [Supercharge return-users](/build/return-users) guide to set up a return user experience
linkToken
Link token returned by the Mesh backend.
```javascript theme={null} const connection = createLink({ renderType: 'embedded', // Indicates Link will be embedded in a container within your web app theme: 'system', language: 'system', displayFiatCurrency: 'USD', accessTokens: accessTokens, // See "Supercharge return users" guide onIntegrationConnected: payload => { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs console.log('[MESH LINK integration connected]', payload) }, onTransferFinished: payload => { // Called after a transfer flow completes successfully console.log('[MESH LINK transfer finished]', payload) }, onExit: () => { // Called when Link is closed console.log('[MESH LINK exited]') }, onEvent: ev => { // Allows you to handle other specific events console.log('[MESH LINK event]', ev) } }) connection.openLink(linkToken, 'custom-iframe-id') // Open in your custom iframe ``` * The `createLink` function accepts one argument, a configuration Object typed `LinkOptions` and returns an Object with two functions, `openLink` and `closeLink`. Calling `openLink` will render the Link UI in an iframe. Calling `closeLink` will close the already-rendered Link UI. Please note, that the Link UI will close itself once the user finishes their workflow.
### Option 2: Overlay Link opens as a modal on top of your product, with a semi-opaque overlay behind it. This focuses the user on the task at hand and minimizes background noise and distractions. Image
renderType
`overlay` indicates Mesh will pop up over your product and ensures certain UX elements behave accordingly.
language
You can use the `system` setting to match their device or browser default, or you can specify a language if you have such a setting in user profiles. See the [Multi-language support](/resources/multi-language) guide for more info on supported languages.
theme
You can use the `system` setting to match their device or browser default, or you can specify a theme if you have such a setting in user profiles.
displayFiatCurrency
Link defaults to USD, or you can specify a fiat currency if you have such a setting in user profiles. See the [Fiat currency support](/resources/fiat-currency) guide for more info on supported currencies
accessTokens
After users connect an exchange account, Mesh can maintain authorization so that the user doesn't have to login again. This enables a 1 or 2 click transfer experience for return users. See the [Use Mesh's callback functions](/build/callbacks) guide for more information on consuming Mesh SDK events and handling them with callback functions, and the [Supercharge return-users](/build/return-users) guide to set up a return user experience
linkToken
Link token returned by the Mesh backend.
```javascript theme={null} const connection = createLink({ renderType: 'overlay', // Opens SDK on top of your web app theme: 'system', language: 'system', displayFiatCurrency: 'USD', accessTokens: accessTokens, // See "Supercharge return users" guide onIntegrationConnected: payload => { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs console.log('[MESH LINK integration connected]', payload) }, onTransferFinished: payload => { // Called after a transfer flow completes successfully console.log('[MESH LINK transfer finished]', payload) }, onExit: () => { // Called when Link is closed console.log('[MESH LINK exited]') }, onEvent: ev => { // Allows you to handle other specific events console.log('[MESH LINK event]', ev) } }) connection.openLink(linkToken) ``` * The `createLink` function accepts one argument, a configuration Object typed `LinkOptions` and returns an Object with two functions, `openLink` and `closeLink`. Calling `openLink` will render the Link UI in an iframe. Calling `closeLink` will close the already-rendered Link UI. Please note, that the Link UI will close itself once the user finishes their workflow.
language
You can use the `system` setting to match their device or browser default, or you can specify a language if you have such a setting in user profiles. See the [Multi-language support](/resources/multi-language) guide for more info on supported languages.
theme
You can use the `system` setting to match their device or browser default, or you can specify a theme if you have such a setting in user profiles.
displayFiatCurrency
Link defaults to USD, or you can specify a fiat currency if you have such a setting in user profiles. See the [Fiat currency support](/resources/fiat-currency) guide for more info on supported currencies
accessTokens
After users connect an exchange account, Mesh can maintain authorization so that the user doesn't have to login again. This enables a 1 or 2 click transfer experience for return users. See the [Use Mesh's callback functions](/build/callbacks) guide for more information on consuming Mesh SDK events and handling them with callback functions, and the [Supercharge return-users](/build/return-users) guide to set up a return user experience
linkToken
Link token returned by the Mesh backend.
```swift theme={null} func launchMeshLink( linkToken: String, viewController: UIViewController, accessTokens: [IntegrationAccessToken]? = nil ) { var linkHandler: LinkHandler? let settings = LinkSettings( accessTokens: accessTokens, // See "Supercharge return users" guide language: "system", displayFiatCurrency: "USD", theme: .system ) // Called after a user connects an exchange, wallet, or other integration. let onIntegrationConnected: (LinkPayload) -> Void = { linkPayload in switch linkPayload { case .accessToken(let accessTokenPayload): print(accessTokenPayload) case .delayedAuth(let delayedAuthPayload): print(delayedAuthPayload) @unknown default: print("unknown LinkPayload") } } // Called after a transfer flow completes successfully or fails. let onTransferFinished: (TransferFinishedPayload) -> Void = { transferFinishedPayload in switch transferFinishedPayload { case .success(let successPayload): print(successPayload) case .error(let errorPayload): print(errorPayload) @unknown default: print("unknown TransferFinishedPayload") } } // Called throughout the Link flow so you can track user progress. let onEvent: ([String: Any]?) -> Void = { payload in print("Event: \(payload ?? [:])") } // Called when the user exits Link let onExit: (Bool?) -> Void = { showAlert in // showAlert is true when 'x' button is tapped // showAlert is false when 'Done' button is tapped on a Transfer Success screen if showAlert ?? false { // in case a custom alert is implemented, linkHandler?.closeLink() must be called to close Link linkHandler?.showExitAlert() // default Exit alert } else { linkHandler?.closeLink() } } let configuration = LinkConfiguration( linkToken: linkToken, settings: settings, disableDomainWhiteList: false, onIntegrationConnected: onIntegrationConnected, onTransferFinished: onTransferFinished, onEvent: onEvent, // onExit is optional, a default alert is shown in case onExit is omitted onExit: onExit ) let result = configuration.createHandler() // createHandler validates the configuration before Link is presented. switch result { case .success(let handler): linkHandler = handler // Present Link from the current UIViewController. handler.present(in: viewController) case .failure(let error): print(error) @unknown default: print("unknown LinkResult") } } ```
linkToken
Link token returned by the Mesh backend.
language
You can use the `system` setting to match their device or browser default, or you can specify a language if you have such a setting in user profiles. See the [Multi-language support](/resources/multi-language) guide for more info on supported languages.
theme
You can use the `system` setting to match their device or browser default, or you can specify a theme if you have such a setting in user profiles.
displayFiatCurrency
Link defaults to USD, or you can specify a fiat currency if you have such a setting in user profiles. See the [Fiat currency support](/resources/fiat-currency) guide for more info on supported currencies
accessTokens
After users connect an exchange account, Mesh can maintain authorization so that the user doesn't have to login again. This enables a 1 or 2 click transfer experience for return users. See the [Use Mesh's callback functions](/build/callbacks) guide for more information on consuming Mesh SDK events and handling them with callback functions, and the [Supercharge return-users](/build/return-users) guide to set up a return user experience
```kotlin theme={null} // Register the Link launcher as an Activity or Fragment property private val linkLauncher = registerForActivityResult(LaunchLink()) { result -> when (result) { is LinkSuccess -> { // Called when Link returns payloads, such as a connected account or completed transfer. Log.i(LOG_TAG, "LinkSuccess: ${result.payloads}") } is LinkExit -> { // Called when Link exits without payloads, including user exits or errors. Log.i(LOG_TAG, "LinkExit: ${result.errorMessage}") } } } private fun launchMeshLink(linkToken: String, accessTokens: List? = null) { // Create LinkConfiguration val configuration = LinkConfiguration( token = linkToken, theme = LinkTheme.SYSTEM, language = "system", displayFiatCurrency = "USD", accessTokens = accessTokens, ) // Launch linkLauncher.launch(configuration) } ```
linkToken
Link token returned by the Mesh backend.
language
You can use the `system` setting to match their device or browser default, or you can specify a language if you have such a setting in user profiles. See the [Multi-language support](/resources/multi-language) guide for more info on supported languages.
theme
You can use the `system` setting to match their device or browser default, or you can specify a theme if you have such a setting in user profiles.
displayFiatCurrency
Link defaults to USD, or you can specify a fiat currency if you have such a setting in user profiles. See the [Fiat currency support](/resources/fiat-currency) guide for more info on supported currencies
accessTokens
After users connect an exchange account, Mesh can maintain authorization so that the user doesn't have to login again. This enables a 1 or 2 click transfer experience for return users. See the [Use Mesh's callback functions](/build/callbacks) guide for more information on consuming Mesh SDK events and handling them with callback functions, and the [Supercharge return-users](/build/return-users) guide to set up a return user experience
```javascript theme={null} return ( { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs console.log('[MESH LINK integration connected]', payload); }} onTransferFinished={(payload: TransferFinishedPayload) => { // Called after a transfer flow completes successfully or fails. console.log('[MESH LINK transfer finished]', payload); }} onEvent={(event: LinkEventType) => { // Allows you to handle other specific events console.log('[MESH LINK event]', event); }} onExit={(err?: string) => { // Called when Mesh Link exits, completes, is closed by the user, or fails during setup. console.log('[MESH LINK exited]'); }} /> ); ```
linkToken
Link token returned by the Mesh backend.
language
You can use the `system` setting to match their device or browser default, or you can specify a language if you have such a setting in user profiles. See the [Multi-language support](/resources/multi-language) guide for more info on supported languages.
theme
You can use the `system` setting to match their device or browser default, or you can specify a theme if you have such a setting in user profiles.
displayFiatCurrency
Link defaults to USD, or you can specify a fiat currency if you have such a setting in user profiles. See the [Fiat currency support](/resources/fiat-currency) guide for more info on supported currencies
accessTokens
After users connect an exchange account, Mesh can maintain authorization so that the user doesn't have to login again. This enables a 1 or 2 click transfer experience for return users. See the [Use Mesh's callback functions](/build/callbacks) guide for more information on consuming Mesh SDK events and handling them with callback functions, and the [Supercharge return-users](/build/return-users) guide to set up a return user experience
```dart theme={null} Future _showMeshLinkPage( String linkToken, { List accessTokens = const [], }) async { // Show MeshSdk final result = await MeshSdk.show( context, configuration: MeshConfiguration( language: 'system', // See "Multi-language support" guide displayFiatCurrency: 'USD', // See "Fiat currency support" guide theme: ThemeMode.system, integrationAccessTokens: accessTokens, // See "Supercharge return users" guide linkToken: linkToken, onEvent: (event) { // Allows you to handle other specific events print('Mesh event: $event'); }, onError: (errorType) { // Callback for when Mesh Link exits due to an error print('Mesh exit: $errorType'); }, onSuccess: (payload) { // Callback for when the Mesh Link is successfully completed print('Mesh success: ${payload.page}'); }, onIntegrationConnected: (integration) { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs print('Integration connected: $integration'); }, onTransferFinished: (transfer) { // Called after a transfer flow completes successfully or fails. print('Transfer finished: $transfer'); }, ), ); // Handle the result switch (result) { case MeshSuccess(): print('Mesh link finished successfully'); case MeshError(): print('Mesh link error: ${result.type}'); } // Alternatively, use `when` method result.when( success: (success) { final payload = success.payload; print('Mesh link success: ${payload.page}'); }, error: (error) { final errorType = error.type; print('Mesh link error: $errorType'); }, ); } ```
### Option 3: Paylinks (a Mesh-hosted webpage) When `generatePayLink: true` in the The Link Token request, the response will include a `paymentLink` url. You can launch this Mesh-hosted webpage in a browser for your user, which avoids the need to download and install the Mesh SDK. When using this approach, it is recommended to also provide a url in the `payLinkReturnUrl` parameter which is where the user will be directed back to upon completion of their session. Payment Links are short-lived (10mins) and one-time use. ***Note: Paylinks are generally not recommended since they miss out on some session-specific SDK configurations, but they're a handy fallback for teams with limited engineering resources or other technical constraints.*** Image ## What's next You've initialized the SDK — now you need to handle what comes back from it. Next up: [Use Mesh's callback functions](/build/callbacks) — wire up the SDK's callback functions to respond to key events like transfer completion and user exit. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Launch the Mesh SDK** Initialize and launch the Mesh Link SDK. Three rendering modes for Web; native initialization for iOS, Android, React Native, Flutter. **Web — createLink() params**: `renderType` ('embedded' or 'overlay') | `theme` ('system'/'dark'/'light') | `language` ('system' or BCP 47) | `displayFiatCurrency` (ISO 4217, default 'USD') | `accessTokens` (MMT return-user array) | `onIntegrationConnected` | `onTransferFinished` | `onExit` | `onEvent` **Web launch**: `connection.openLink(linkToken)` (overlay) or `connection.openLink(linkToken, 'custom-iframe-id')` (embedded) | **Close**: `connection.closeLink()` **Rendering options**: * **Embedded** (recommended for web): iframe in your container. Min height 450px, recommended 665px. * **Overlay**: modal popup over your app. * **Paylink**: `generatePayLink: true` in Link Token request → `paymentLink` URL in response. No SDK install needed. Short-lived (10 min), single-use. **iOS** (`LinkConfiguration`): `linkToken` | `settings` (LinkSettings: `accessTokens`, `language`, `displayFiatCurrency`, `theme`) | `onIntegrationConnected` | `onTransferFinished` | `onEvent` | `onExit` → `configuration.createHandler()` → `handler.present(in: viewController)` **Android** (`LinkConfiguration`): `token` | `theme` (LinkTheme.SYSTEM) | `language` | `displayFiatCurrency` | `accessTokens` → `linkLauncher.launch(configuration)`. Results: `LinkSuccess` (has `payloads`) | `LinkExit` (has `errorMessage`) **React Native**: `` **Flutter**: `MeshSdk.show(context, configuration: MeshConfiguration(linkToken, language, displayFiatCurrency, theme: ThemeMode.system, integrationAccessTokens, onEvent, onError, onSuccess, onIntegrationConnected, onTransferFinished))` **Web (overlay) — copy/paste snippet**: ```javascript theme={null} const connection = createLink({ renderType: 'overlay', // Opens SDK on top of your web app theme: 'system', language: 'system', displayFiatCurrency: 'USD', accessTokens: accessTokens, // See "Supercharge return users" guide onIntegrationConnected: payload => { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs console.log('[MESH LINK integration connected]', payload) }, onTransferFinished: payload => { // Called after a transfer flow completes successfully console.log('[MESH LINK transfer finished]', payload) }, onExit: () => { // Called when Link is closed console.log('[MESH LINK exited]') }, onEvent: ev => { // Allows you to handle other specific events console.log('[MESH LINK event]', ev) } }) connection.openLink(linkToken) ``` **iOS Swift — copy/paste snippet**: ```swift theme={null} func launchMeshLink( linkToken: String, viewController: UIViewController, accessTokens: [IntegrationAccessToken]? = nil ) { var linkHandler: LinkHandler? let settings = LinkSettings( accessTokens: accessTokens, // See "Supercharge return users" guide language: "system", displayFiatCurrency: "USD", theme: .system ) // Called after a user connects an exchange, wallet, or other integration. let onIntegrationConnected: (LinkPayload) -> Void = { linkPayload in switch linkPayload { case .accessToken(let accessTokenPayload): print(accessTokenPayload) case .delayedAuth(let delayedAuthPayload): print(delayedAuthPayload) @unknown default: print("unknown LinkPayload") } } // Called after a transfer flow completes successfully or fails. let onTransferFinished: (TransferFinishedPayload) -> Void = { transferFinishedPayload in switch transferFinishedPayload { case .success(let successPayload): print(successPayload) case .error(let errorPayload): print(errorPayload) @unknown default: print("unknown TransferFinishedPayload") } } // Called throughout the Link flow so you can track user progress. let onEvent: ([String: Any]?) -> Void = { payload in print("Event: \(payload ?? [:])") } // Called when the user exits Link let onExit: (Bool?) -> Void = { showAlert in // showAlert is true when 'x' button is tapped // showAlert is false when 'Done' button is tapped on a Transfer Success screen if showAlert ?? false { // in case a custom alert is implemented, linkHandler?.closeLink() must be called to close Link linkHandler?.showExitAlert() // default Exit alert } else { linkHandler?.closeLink() } } let configuration = LinkConfiguration( linkToken: linkToken, settings: settings, disableDomainWhiteList: false, onIntegrationConnected: onIntegrationConnected, onTransferFinished: onTransferFinished, onEvent: onEvent, // onExit is optional, a default alert is shown in case onExit is omitted onExit: onExit ) let result = configuration.createHandler() // createHandler validates the configuration before Link is presented. switch result { case .success(let handler): linkHandler = handler // Present Link from the current UIViewController. handler.present(in: viewController) case .failure(let error): print(error) @unknown default: print("unknown LinkResult") } } ``` **Android Kotlin — copy/paste snippet**: ```kotlin theme={null} // Register the Link launcher as an Activity or Fragment property private val linkLauncher = registerForActivityResult(LaunchLink()) { result -> when (result) { is LinkSuccess -> { // Called when Link returns payloads, such as a connected account or completed transfer. Log.i(LOG_TAG, "LinkSuccess: ${result.payloads}") } is LinkExit -> { // Called when Link exits without payloads, including user exits or errors. Log.i(LOG_TAG, "LinkExit: ${result.errorMessage}") } } } private fun launchMeshLink(linkToken: String, accessTokens: List? = null) { // Create LinkConfiguration val configuration = LinkConfiguration( token = linkToken, theme = LinkTheme.SYSTEM, language = "system", displayFiatCurrency = "USD", accessTokens = accessTokens, ) // Launch linkLauncher.launch(configuration) } ``` **React Native — copy/paste snippet**: ```javascript theme={null} return ( { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs console.log('[MESH LINK integration connected]', payload); }} onTransferFinished={(payload: TransferFinishedPayload) => { // Called after a transfer flow completes successfully or fails. console.log('[MESH LINK transfer finished]', payload); }} onEvent={(event: LinkEventType) => { // Allows you to handle other specific events console.log('[MESH LINK event]', event); }} onExit={(err?: string) => { // Called when Mesh Link exits, completes, is closed by the user, or fails during setup. console.log('[MESH LINK exited]'); }} /> ); ``` **Flutter — copy/paste snippet**: ```dart theme={null} Future _showMeshLinkPage( String linkToken, { List accessTokens = const [], }) async { // Show MeshSdk final result = await MeshSdk.show( context, configuration: MeshConfiguration( language: 'system', // See "Multi-language support" guide displayFiatCurrency: 'USD', // See "Fiat currency support" guide theme: ThemeMode.system, integrationAccessTokens: accessTokens, // See "Supercharge return users" guide linkToken: linkToken, onEvent: (event) { // Allows you to handle other specific events print('Mesh event: $event'); }, onError: (errorType) { // Callback for when Mesh Link exits due to an error print('Mesh exit: $errorType'); }, onSuccess: (payload) { // Callback for when the Mesh Link is successfully completed print('Mesh success: ${payload.page}'); }, onIntegrationConnected: (integration) { // Payload contains integration tokens that can be passed to SDK for a return user experience, or calling certain Mesh APIs print('Integration connected: $integration'); }, onTransferFinished: (transfer) { // Called after a transfer flow completes successfully or fails. print('Transfer finished: $transfer'); }, ), ); // Handle the result switch (result) { case MeshSuccess(): print('Mesh link finished successfully'); case MeshError(): print('Mesh link error: ${result.type}'); } // Alternatively, use `when` method result.when( success: (success) { final payload = success.payload; print('Mesh link success: ${payload.page}'); }, error: (error) { final errorType = error.type; print('Mesh link error: $errorType'); }, ); } ``` # Polish the experience Source: https://docs.meshconnect.com/build/polish By the end of this guide, you'll have a polished, production-ready integration with a well-designed entry point, clean session handling, a great return-user experience, and real-time webhook notifications. **Before you start** * Your core integration is working end-to-end in sandbox: you can fetch a Link Token, launch the SDK, and handle callbacks * You've set up Mesh Managed Tokens for return users (see [Supercharge return-users](/build/return-users)) ## Overview The Mesh SDK handles the full user journey of connecting accounts and initiating transfers — but there are a few things you'll want to set up on your side to make the experience seamless from start to finish. ## For all implementations ### Create the front door to Mesh Link (eg. a button) **Content**: A common approach to a button is "Direct deposit" or "Pay with crypto" or "Connect an account". These are all fine as they set clear expectations. But wherever possible, lead with words that relay the benefit of Mesh (ie. security). This helps users understand not just that this exists, but rather why they should try it (in connected account flows, users can't misconfigure a transfer and lose assets). An example would be subtext that says "*Securely send to the correct address and network*" and/or using a shield or some other security-oriented icon. **Images**: Lead with icons for the top exchanges and wallets (ie. Binance, Coinbase, MetaMask, Phantom, etc.). Many users don't yet recognize the Mesh brand, so we would not recommend leading with Mesh branding. **Return users**: Create a dedicated entry point for return users. If someone deposits with Coinbase, chances are they'll deposit from that same account in the future. So consider placing a "Deposit from Coinbase" button next to the general button whenever a user has successfully connected an account. See the [Supercharge return-users](/build/return-users) guide for more details. **Want more help?** Use our [button generator](https://mesh-button-builder.vercel.app/). This tool helps you quickly generate code for a button you can put in your app. ### Configure Mesh Link to match your design system Link has a modern, polished design system by default. However, you can configure the interface to closely match your own design system. Fonts, colors, and even loading animations can be updated, making Link appear more native to your platform. This minimizes the seam between your product and Mesh, enhancing the overall experience. In the Mesh developer dashboard, go to **Account > Link Configuration > Interface** to make these configurations. Image ### Handle a clean end of session When a user successfully completes a transfer, they will end on a success screen and can click "Done." This will end the current Link session. 1. When the user lands back on your product surface, acknowledge a pending transaction. Most clients flash a banner for this (eg. "Deposit pending"). 1. **Why?** Crypto users are used to waiting some time to see balances update or payments complete after manually initiating a transaction. But remember, they see a Success screen in Link at the end of a successful flow, so it can be nerve-wracking if they don't see that reflected in your app instantly. 2. You can trigger this banner or notification using the `onTransferFinished()` callback function, whose payload contains data about a pending transfer. See the [**Mesh Link SDK Events**](/resources/sdk-events) guide for more information on this. ### Create a delightful return-user experience Can you imagine having to add your credit card to Apple Pay every single time you wanted to tap-to-pay? That would defeat the whole purpose. But you're willing to put in the effort to set up Apple Pay because you know it will be effortless every time after that. Mesh is similar… a user must connect their account the first time, but can enjoy 1 or 2 click transactions on subsequent visits. See the [Supercharge return-users](/build/return-users) guide for instructions on how to capture `accessTokens` and pass them on subsequent user sessions. ### Consume Mesh webhooks for real-time notifications of transfer status Mesh will confirm success to the user (and to you via an SDK event) when the integration confirms successful receipt of the order to Mesh. At that point, the user has done all they need to, so we let them go. However, this does not mean their transaction is complete. Self-custody wallets (eg. MetaMask, Phantom, etc.) typically post to the blockchain almost immediately upon user signing, but centralized exchanges (eg. Binance, Coinbase, etc.) often take some time to do compliance checks, and batch transactions to save on gas fees. Most exchanges post within 30s, but in some instances it could take minutes, hours, or even days. This is why we strongly encourage all clients to consume Mesh webhooks for proactive, real-time notifications regarding transfer status. You can consider a transaction `pending` until you receive a `succeeded` event, at which point you can release inventory or credit the user's account. Refer to the [Transfer status webhooks](/resources/webhooks) guide for more information. ## For "embedded" implementations If you're embedding Mesh's web SDK into a container in your product surface, these tips are for you. ### Ensure Link fits in your container You must ensure that the container in which Link is loaded abides by certain minimum & maximum dimensions or you'll end up with scenarios where buttons are below the fold, or the user must scroll on single screens (up-down or even left-right). Maintaining the intended dimensions of the SDK ensures that the user experience is preserved. * **Mobile screens:** * The container should maximize the space available for the iframe * **Stationary screens:** * Minimum height of iframe container: 450px * Recommended height of iframe container: 665px * **If Link is inside a modal dialog:** * Modals often have fixed or constrained heights by default, which will clip the Mesh UI. The recommended pattern: * Set the **iframe** height responsively: `height: min(665px, calc(100dvh - Xpx))` where X = your modal chrome (header + tabs + padding) plus any outer gutter — typically **100–140px** total. This gives you 665px on large screens and automatically scales down on smaller viewports. * Set `min-height: 450px` on the iframe as a floor. * Let the **modal container** grow to fit the iframe naturally — no fixed height on the modal. Cap it with `max-height: calc(100dvh - [your gutter])` to prevent viewport overflow. * Flexbox on the modal with the iframe area as a growing child gives you this behavior naturally. * **Why responsive height matters**: Mesh has sticky UI elements — action buttons and the "Secured by Mesh" footer — that are positioned relative to the *iframe's own viewport height*, not your outer container. If the iframe is always 665px but your modal only shows 500px of it on a smaller screen, those sticky elements are permanently below the fold and the user must scroll to reach them. A responsive iframe height ensures they're always anchored to the visible bottom edge. ### Pre-load Link so it feels instant & native Call for a Link Token and initialize an SDK session as soon as the user logs into your app. This allows you to pre-load Link in the background, which will make it render instantly when the user clicks "Deposit" or "Buy" within your app, making it feel native. Link loads fairly quickly, but this can make a difference for users with slower connections or devices. * Refresh after an SDK session has been active for 50 mins. The Mesh SDK has a 60min session expiry. If a user doesn't engage with deposit for 59mins, the session may expire while they're in it. A 50min rule leaves 10mins of validity on the user session if they start at the end of that refresh window, which should be a very comfortable buffer. * Refresh if any user **destination address changes or is added** (the new session is to ensure you're including all correct target address) * The user refreshes some setting in your app like **theme, currency, or language** (pass updated `theme`, `displayFiatCurrency`, or `language` to the new SDK session) * The browser comes back **online** after a connectivity drop (`window.addEventListener('online', preload)`) * The user **closes a modal** that was covering the Link container (so it's ready the moment they reopen) **Loading lifecycle:** Show your own loading indicator (spinner, skeleton) only while fetching the Link token. Once you call the `openLink()` function in the Mesh SDK, you should replace your own spinner with the Mesh SDK, which handles its own animated loading state inside the iframe — your spinner only needs to cover the brief window before the iframe paints. 1. Use `connection.closeLink()` to end the active Link session. **Keep the iframe in the DOM** — do not remove it. 2. Request a new Link Token. 3. Re-initialize the SDK with the same `customIframeId` — Link will repaint inside the existing iframe. ### Handle a clean end of session There are expanded instructions for handling session ends when Link is embedded. These two callbacks serve different purposes — don't conflate them. **`onTransferFinished(payload)`** fires when a transfer is initiated. The user is *still on the success screen inside Link* at this point. Use this to show a "Deposit pending" banner or notification in your app UI. **Do not call `closeLink()` here** — the session is still active and the user hasn't left yet. **`onExit()`** fires when the user clicks "Done" and is ready to leave Link. This is the correct place to call `connection.closeLink()`. After closing, either: 1. **Keep the container open**: Re-initialize a new Link session using the same `customIframeId` to bring the user back to the start of the deposit flow. Or… 2. **Close the container**: Call `closeLink()`, then preload a new session in the background (using the same `customIframeId`) so it's instant next time. **Do not remove the iframe from the DOM** between sessions. Keep the element in place and simply re-initialize the SDK — see "Keep the iframe in the DOM" below. ### Keep the iframe in the DOM When embedding Link, leave the iframe element in the DOM for the entire lifetime of the page — even between sessions. Do not add and remove it each time a session starts or ends. Removing and re-inserting the iframe forces the browser to re-parse, re-layout, and re-paint from scratch. It can also cause subtle timing issues if the element disappears while the SDK is still shutting down. Instead: * Create the iframe (or its container) **once** on page load and leave it in place permanently. * To "hide" Link between sessions, use CSS (`display: none` or `visibility: hidden`) — the DOM element stays intact. * When starting a new session, make the container visible again and re-initialize the SDK with the same `customIframeId`. * Mesh renders its own loading animation inside the iframe — you don't need to rebuild the element to get a clean start. ### Handle user navigation within your app surface This is only applicable to clients that are using Mesh for multiple user flows. For example, some use Mesh to do all of the following 3 things, each of which result in Link sessions in different places. 1. Power all or part of their crypto **Deposit** flow 2. Add Mesh's onramp integrations to the lineup of onramps in their **Buy** flow 3. Add "connect an account" to their **Withdrawal** flow for automated address retrieval * Pre-load Link in only one of those places (whichever is most common, likely deposit). * Persist / maintain that Link session when the user navigates away from that tab. * **Why?** If the user is on "Deposit" and sees a Link SDK session, then clicks to "Buy" or "Withdraw", you won't have to load up a new session if they come back. * Be sure to follow the normal 50min refresh guidance. * If the user **commits** to starting a new Link session in another area — for example, by selecting a provider in the "Buy" tab — kill the original/active SDK session before initializing the new one. * **Why?** A user can technically have two active Link sessions at a time, but it can cause undesirable edge cases with wallet connections and exchange OAuth. Kill the persisted session only at the moment the user *actively initiates* a new flow — not merely because they navigated to a different tab. * **Important**: Tab navigation alone is *not* sufficient reason to kill the session. Only call `closeLink()` when the user takes a deliberate action to start a new Link flow (eg. selects a provider, clicks a connect button). This preserves the preloaded session on the other tab for when they come back. * Instructions: 1. Use `connection.closeLink()` to end the active Link session (keep the iframe in the DOM). 2. Request a new Link Token for the new session. 3. Initialize the new Link SDK session. ## For iOS implementations ### Update your iOS app's plist for smooth wallet connectivity The Mesh SDK enables seamless connectivity with self-custody wallets by leveraging **deep linking** to redirect users to their wallet applications for actions such as signing messages or initiating transactions. To ensure proper functionality, your iOS application must include the list of native wallet links in its `Info.plist` configuration. **Native links vs Universal Links, and why you should add wallet native link to your plist**: * Mesh uses Universal Links for many integrations. Unlike custom URL schemes, Universal Links don't require adding entries to Info.plist. However, Universal Links depend on proper server configuration (the apple-app-site-association file) and domain verification by Apple. When Universal Links fail—due to configuration issues, the app not being installed, or the user's first interaction—the system falls back to opening the link in the browser or using custom URL schemes as a fallback mechanism. * Each wallet application has its own unique URL scheme (e.g., `metamask://` for MetaMask, `trust://` for Trust Wallet). These schemes allow the iOS system to route the user to the appropriate application installed on their device. * iOS apps must explicitly declare the URL schemes they interact with in the `Info.plist` file under the `LSApplicationQueriesSchemes` key. Failing to include these schemes can prevent the app from opening wallet applications, breaking the SDK's logic and key workflows. * This configuration aligns with iOS security best practices by ensuring only verified wallets can be accessed. 1. Update `Info.plist`: Add the `LSApplicationQueriesSchemes` key to your app's `Info.plist` file. This key should include all native wallet URL schemes supported by our SDK. **Recommended List:** ```xml theme={null} LSApplicationQueriesSchemes bitcoin bitkeep bitcoincom bnc cbwallet dfw exodus ledgerlive metamask okx phantom rabby rainbow robinhood-wallet tpoutside tronlinkoutside trust uniswap zengo ``` ***Note: This list is based on Mesh's most-used integrations and may change over time. Do not add more than 50 items — this is a hard limit enforced by Apple.*** 2. Verify Native Link Functionality: Test the integration to ensure your app properly detects wallet installations and redirects users to the appropriate application. ## What's next Next up: [Prepare for go-live](/build/go-live) — when you're happy with the experience in sandbox, this guide walks you through getting your production credentials and launching. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Polish the experience** Production-readiness checklist: entry point design, session lifecycle management, embedded sizing, SDK preloading, return-user UX, webhook consumption, iOS plist for wallet deep links. **Entry point**: Use action-oriented copy ("Direct deposit", "Pay with crypto"). Lead with exchange/wallet icons (Binance, Coinbase, MetaMask, Phantom). Add dedicated return-user entry point per connected account. **Session lifecycle**: Preload Link on user login; refresh every 50 min (Link session expires at 60 min). Also preload after: destination address/network changes, theme changes, connectivity restore (`online` event), modal close. Refresh cycle: `connection.closeLink()` → new Link Token → re-initialize with same `customIframeId` (keep iframe in DOM). `onTransferFinished(payload)` fires while user is still on success screen — use it for a "Deposit pending" banner, **do not** call `closeLink()` here. `onExit()` fires when the user clicks Done — call `closeLink()` here, then either keep container open with a fresh session or close. Listen for `pageLoaded` event to know when to hide your own loading spinner. **Embedded sizing**: Min height 450px, recommended 665px on large screens. In modal containers, use responsive iframe height: `min(665px, calc(100dvh - Xpx))` (X = modal chrome + gutter, typically 100–140px) so Mesh's sticky elements (action buttons, "Secured by Mesh" footer) stay anchored to the visible bottom edge regardless of viewport size. Maximize iframe on mobile. **Multi-flow apps**: Persist Link session when user navigates between tabs. Only call `closeLink()` when the user actively *commits* to a new Link flow (eg. selects a provider) — not on mere tab navigation. Keep iframe in DOM permanently. **iOS plist** (`LSApplicationQueriesSchemes`): Add native wallet URL schemes. Max 50 items (Apple hard limit): ```xml theme={null} LSApplicationQueriesSchemes bitcoin bitkeep bitcoincom bnc cbwallet dfw exodus ledgerlive metamask okx phantom rabby rainbow robinhood-wallet tpoutside tronlinkoutside trust uniswap zengo ``` **Link customization**: Dashboard > Account > Link Configuration > Interface for fonts, colors, loading animations. # Prepare to build Source: https://docs.meshconnect.com/build/prepare-to-build By the end of this guide, you'll have a Mesh dashboard account, a sandbox API key, and the Mesh SDK installed — everything you need to start building. **Before you start** This is the first guide in the series. The only thing you'll need is an invitation to the Mesh dashboard — reach out to your Mesh representative if you haven't received one yet. ## Create a Mesh dashboard account Mesh's developer dashboard is your home base for managing your Mesh account (API keys, configuration options, transaction data, etc.). If no one in your organization has been invited to an account, reach out to your Mesh representative to request an invitation. You'll receive that invitation via email and will be prompted to set up a password to secure your account. If someone in your organization already has a Mesh account, ask them to invite you to it (**Account** > **Team**). Team members are managed with role-based access (shown below), and each team member will have their own account and login. Image ## Generate a sandbox key Get started by navigating to **Account** > **API keys** where you can create and manage keys. You'll also find the sandbox base URL here — use it to point your API requests to the right place during development. Image ## Add one or more "Allowed domains" These are the domains on which Mesh's SDK (Link) will be allowed to render. Link will fail to load on all other domains. You can add "Allowed domains" in **Account** > **API keys**. Image ## Download and install the Mesh SDK Link is Mesh's client-side SDK, supported on 5 platforms. It facilitates the user experience for all flows (ie. connecting an account, configuring/previewing/approving transfers, verifying wallet ownership, etc.), minimizing any frontend work needed on your end (all you need is an entry point and a post-flow acknowledgement). Select which platform is best for you, and click below to download and install it. In the next guide, you'll learn how to configure and launch a session of this SDK. Install the dependency: ```shell theme={null} # with npm npm install --save @meshconnect/web-link-sdk # with yarn yarn add @meshconnect/web-link-sdk ``` Details on [mesh-web-sdk](https://github.com/FrontFin/mesh-web-sdk/tree/main/packages/link#meshconnectweb-link-sdk) [Add package dependency](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#Add-a-package-dependency) using source control repository URL to your Xcode project: ```text theme={null} https://github.com/FrontFin/mesh-ios-sdk ``` Details on [mesh-ios-sdk](https://github.com/FrontFin/mesh-ios-sdk?tab=readme-ov-file#mesh-connect-ios-sdk) Add the dependency to your `build.gradle`: ```groovy theme={null} dependencies { implementation 'com.meshconnect:link:$linkVersion' } ``` Details on [mesh-android-sdk](https://github.com/FrontFin/mesh-android-sdk?tab=readme-ov-file#mesh-connect-android-sdk) Install the dependency: ```shell theme={null} # with npm npm install --save @meshconnect/react-native-link-sdk # with yarn yarn add @meshconnect/react-native-link-sdk ``` This package requires `react-native-webview` to be installed in your project: ```shell theme={null} # with npm npm install --save react-native-webview # with yarn yarn add react-native-webview ``` Details on [mesh-react-native-sdk](https://github.com/FrontFin/mesh-react-native-sdk?tab=readme-ov-file#mesh-connect-react-native-sdk) Add the dependency to `pubspec.yaml`: ```yaml theme={null} dependencies: mesh_sdk_flutter: ``` Mesh SDK uses the `flutter_localizations` package for localization: ```dart theme={null} import 'package:mesh_sdk_flutter/mesh_sdk_flutter.dart'; @override Widget build(BuildContext context) { return MaterialApp( localizationsDelegates: [ ...MeshLocalizations.localizationsDelegates, ], ); } ``` Details on [mesh-flutter-sdk](https://github.com/FrontFin/mesh-flutter-sdk?tab=readme-ov-file#requirements) This step is not necessary if you plan to use Paylinks — Paylinks launch the Mesh SDK in a separate Mesh-hosted webpage and don't require the SDK to be installed locally. See the [Launch the Mesh SDK](/build/launch-sdk) guide for more. ## What's next The next two guides cover the foundational pieces of every Mesh integration: 1. [Fetch a Link Token](/build/fetch-link-token): This token is an access key for a session of Link, Mesh's SDK. You'll request it with a call to a Mesh endpoint, and the parameters you include will configure that user session. 2. [Launch the Mesh SDK](/build/launch-sdk): Mesh has 5 client-side SDKs which are home to **Link**, the Mesh-hosted UX that facilitates the user journey (i.e. connecting an account, configuring / previewing / approving transfers, verifying wallet ownership, etc.). You'll launch an SDK session using a Link Token. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Prepare to build** Dashboard account setup, sandbox API key generation, domain allowlisting, and SDK installation for all 5 platforms. **Dashboard paths**: API keys → Account > API keys > API keys | Allowed domains → Account > API keys > Access **Key**: Create key with Read & Write permissions for any transfer use case. **Sandbox base URL**: `https://sandbox-integration-api.meshconnect.com` **Auth headers on all API calls**: `X-Client-Id: YOUR_CLIENT_ID`, `X-Client-Secret: YOUR_API_KEY` **SDK installs**: * Web: `npm install --save @meshconnect/web-link-sdk` * iOS: Swift Package from `https://github.com/FrontFin/mesh-ios-sdk` * Android: `implementation 'com.meshconnect:link:'` in build.gradle * React Native: `npm install --save @meshconnect/react-native-link-sdk` + `react-native-webview` * Flutter: `mesh_sdk_flutter: ` in pubspec.yaml + `MeshLocalizations.localizationsDelegates` **Note**: SDK install not required if using Paylinks (Mesh-hosted webpage). See Launch the Mesh SDK guide. # Supercharge return-users Source: https://docs.meshconnect.com/build/return-users By the end of this guide, you'll know how to use Mesh Managed Tokens (MMT) to create a seamless return-user experience — so users who've already connected an account don't need to re-authenticate. **Before you start** * You've set up `onIntegrationConnected` callback to capture `accessTokens` object (see [Use Mesh's callback functions](/build/callbacks)) ## Overview Mesh's Managed Token system (MMT) simplifies how you manage user authentication tokens. MMT securely stores and manages token lifecycles on your behalf, giving you a persistent `tokenId` for each connected user account. You can use this to create a seamless return-user experience for exchanges and to interact with certain Mesh APIs — no manual token refresh or re-authentication required. ## Benefits * **Streamlined UX**: After connecting their exchange account to your app, users can skip repetitive authentication steps on future visits. * **Persistent Access**: Mesh handles all token refresh logic. For exchanges with long-lived tokens (like Coinbase), access stays valid indefinitely. For those with expiring tokens (like Binance), users will be prompted to re-authenticate when needed — but the `tokenId` you store never changes, so no backend updates are required on your part. * **Integration-Agnostic**: Works across different auth methods (OAuth, credentials-based) with no added client-side complexity. ## How to Implement MMT ### 1. Obtain a `tokenId` When a user authenticates with their exchange or wallet account, you will receive the SDK `onIntegrationConnected` event, which contains an object like this: ```json theme={null} { "accessToken": { "accountTokens": [ { "account": { "meshAccountId": "", "frontAccountId": "", "accountId": "", "accountName": "", "fund": 0, "cash": 0 }, "accessToken": "", "tokenId": "" // Use this } ], "brokerType": "", // Use this "brokerName": "", "brokerBrandInfo": {} } } ``` Capture the `tokenId` and `brokerType`. ### 2. Build the `accessTokens` object See the [Use Mesh's callback functions](/build/callbacks) guide for more information on how to use the `onIntegrationConnected` callback function to do this. This will later be passed into the Mesh SDK for a returning user experience. #### Notes & references: * Store the `tokenId` securely, for example in a database. Make sure each `tokenId` and `brokerType` pair is clearly tied to the `userId` that connected the account. * You'll notice that the `tokenId` & `accessToken` in the `integrationConnected` event are the same for the initial connection of a particular user account. But if the `accessToken` is ever refreshed or replaced, they will diverge. Link facilitates the user experience, and Mesh manages the token refresh logic for you, which means the `tokenId` will remain unchanged for a given `userId` + `brokerType` combination. **This means you should put the `tokenId` value from the SDK event into the `accessToken` field in this object**. * The `brokerName`, `accountId`, and `accountName` fields are obsolete but must still be passed (can be empty) when initializing the SDK. ### 3. Pass the accessTokens object into the Mesh SDK ```javascript theme={null} const accessTokens: IntegrationAccessToken[] = [ { accessToken: '', // Put the tokenId here brokerType: '', // Put the brokerType here brokerName: '', accountId: '', accountName: '' } ] ``` This will then be used when initializing future sessions of the Mesh SDK for this user so they can skip re-authentication and proceed directly into the transfer or portfolio flow. ```javascript theme={null} const connection = createLink({ renderType: 'embedded', theme: 'system', language: 'system', displayFiatCurrency: 'USD' accessTokens: accessTokens, // Refers to the accessTokens object onIntegrationConnected: payload => { // Payload contains accessTokens for newly connected accounts console.debug('[MESH LINK integration connected]', payload) }, }) connection.openLink(linkToken, 'custom-iframe-id') ``` ```swift theme={null} let accessTokens = [ IntegrationAccessToken( accountId: "", accountName: "", accessToken: "", // Put the tokenId here brokerType: "", // Put the brokerType here brokerName: "" ) ] ``` This will then be used when initializing future sessions of the Mesh SDK for this user so they can skip re-authentication and proceed directly into the transfer or portfolio flow. ```swift theme={null} func launchMeshLink( linkToken: String, viewController: UIViewController, accessTokens: [IntegrationAccessToken]? = nil ) { var linkHandler: LinkHandler? let settings = LinkSettings( accessTokens: accessTokens, // Refers to the accessTokens object language: "system", displayFiatCurrency: "USD", theme: .system ) // Called after a user connects an exchange, wallet, or other integration. // Payload contains accessTokens for newly connected accounts let onIntegrationConnected: (LinkPayload) -> Void = { linkPayload in switch linkPayload { case .accessToken(let accessTokenPayload): print(accessTokenPayload) case .delayedAuth(let delayedAuthPayload): print(delayedAuthPayload) @unknown default: print("unknown LinkPayload") } } let configuration = LinkConfiguration( linkToken: linkToken, settings: settings, disableDomainWhiteList: false, onIntegrationConnected: onIntegrationConnected, ) let result = configuration.createHandler() // createHandler validates the configuration before Link is presented. switch result { case .success(let handler): linkHandler = handler // Present Link from the current UIViewController. handler.present(in: viewController) case .failure(let error): print(error) @unknown default: print("unknown LinkResult") } } ``` ```kotlin theme={null} val accessTokens = listOf( IntegrationAccessToken( accountId = "", accountName = "", accessToken = "", // Put the tokenId here brokerType = "", // Put the brokerType here brokerName = "", ), ) ``` This will then be used when initializing future sessions of the Mesh SDK for this user so they can skip re-authentication and proceed directly into the transfer or portfolio flow. ```kotlin theme={null} // Register the Link launcher as an Activity or Fragment property private val linkLauncher = registerForActivityResult(LaunchLink()) { result -> when (result) { is LinkSuccess -> { // Called when Link returns payloads, such as a connected account or completed transfer. // Upon successful integration payload contains accessTokens for newly connected accounts Log.i(LOG_TAG, "LinkSuccess: ${result.payloads}") } is LinkExit -> { // Called when Link exits without payloads, including user exits or errors. Log.i(LOG_TAG, "LinkExit: ${result.errorMessage}") } } } private fun launchMeshLink(linkToken: String, accessTokens: List? = null) { // Create LinkConfiguration val configuration = LinkConfiguration( token = linkToken, theme = LinkTheme.SYSTEM, language = "system", displayFiatCurrency = "USD", accessTokens = accessTokens, // Refers to the accessTokens object ) // Launch linkLauncher.launch(configuration) } ``` ```javascript theme={null} const accessTokens: IntegrationAccessToken[] = [ { accessToken: '', // Put the tokenId here brokerType: '', // Put the brokerType here brokerName: '', accountId: '', accountName: '' } ] ``` This will then be used when initializing future sessions of the Mesh SDK for this user so they can skip re-authentication and proceed directly into the transfer or portfolio flow. ```javascript theme={null} return ( { // Payload contains accessTokens for newly connected accounts console.log('[MESH LINK integration connected]', payload); }} /> ); ``` ```dart theme={null} final accessTokens = [ IntegrationAccessToken( accessToken: '', // Put the tokenId here brokerType: '', // Put the brokerType here brokerName: '', accountId: '', accountName: '', ) ]; ``` This will then be used when initializing future sessions of the Mesh SDK for this user so they can skip re-authentication and proceed directly into the transfer or portfolio flow. ```dart theme={null} Future _showMeshLinkPage( String linkToken, { List accessTokens = const [], }) async { // Show MeshSdk final result = await MeshSdk.show( context, configuration: MeshConfiguration( language: 'system', displayFiatCurrency: 'USD', theme: ThemeMode.system, integrationAccessTokens: accessTokens, // Refers to the accessTokens object linkToken: linkToken, onIntegrationConnected: (integration) { // Called after a user connects an exchange, wallet, or other integration. // Payload contains accessTokens for newly connected accounts print('Integration connected: $integration'); }, ), ); // Handle the result switch (result) { case MeshSuccess(): print('Mesh link finished successfully'); case MeshError(): print('Mesh link error: ${result.type}'); } // Alternatively, use `when` method result.when( success: (success) { final payload = success.payload; print('Mesh link success: ${payload.page}'); }, error: (error) { final errorType = error.type; print('Mesh link error: $errorType'); }, ); } ``` ## Token Lifecycle and Behavior | Scenario | Result | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | **Same user reconnects with same integration** | Returns same `tokenId` | | **Different user connects to same integration** | Returns a new `tokenId` | | **Same user connects with different scopes (e.g. read vs. write)** | Returns distinct `tokenId` per scope | | **Write endpoint called using read-only TokenId** | API returns a scope mismatch error | | **Token revoked by client (use [Remove connection](https://docs.meshconnect.com/api-reference/managed-account-authentication/remove-connection) endpoint)** | Associated access is permanently disabled and Mesh also deletes the token physically, without any way to restore it | ## Supported Integrations This list is growing and will be updated over time. **As new integrations are added, you will not have to update support on your end**. * **Coinbase** * **Binance** * **Uphold** * *Note: connections with self-custody wallets are maintained on subsequent sessions (unless the user actively kills the connection in their wallet app) without the need for handling tokens. This means the same smooth return user journey is achieved for wallet transactions.* ## Security Considerations MMT streamlines token handling and unlocks some powerful functionality — here's how Mesh protects the data under the hood: * **Encrypted Storage**: All tokens are encrypted at rest using modern encryption standards. * **Scoped Access**: Each `tokenId` is tied to the permission scope (read or write) associated with the API key. Unauthorized operations (e.g., calling a write endpoint with a read-scoped token) will be rejected. * **Client-Level Isolation**: Each `tokenId` is also scoped to a specific `clientId`. Even if the same end user connects the same integration account across multiple client apps, the tokens are isolated and not shared across clients. * **User-Level Isolation**: Each `tokenId` is unique to a specific `endUserId` and integration. * **Token Revocation**: Clients can revoke a `tokenId`, permanently disabling access and triggering a secure deletion process. There is no path to restore a revoked token. ## What's next Next up: [Polish the experience](/build/polish) — with the core integration working, this guide walks you through the finishing touches: a well-designed entry point, clean session handling, and webhook setup. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Supercharge return-users (Mesh Managed Tokens)** Use Mesh Managed Tokens (MMT) for one-tap return-user flows. Mesh manages token refresh; you store a stable `tokenId`. **Step 1 — Capture from onIntegrationConnected**: Extract `accessToken.accountTokens[].tokenId` and `accessToken.brokerType`. Store server-side per `userId` + `brokerType`. **Step 2 — Build accessTokens array**: `[{ accessToken: '', brokerType: '', brokerName: '', accountId: '', accountName: '' }]` Critical: put `tokenId` in the `accessToken` field (not the raw accessToken string). `tokenId` is stable even when the underlying token rotates. **Step 3 — Pass into SDK on next session**: Include `accessTokens` array in `createLink()` / `LinkSettings` / `LinkConfiguration` / `MeshConfiguration`. **Token lifecycle**: Same user + integration → same `tokenId` | Different user or scope → new `tokenId` | Revoked → permanently disabled, unrestorable | Coinbase: tokens valid indefinitely | Binance: tokens expire, user re-auths but `tokenId` unchanged. **Supported integrations**: Coinbase, Binance, Uphold (growing). Self-custody wallet connections persist without tokens. **Security**: Tokens encrypted at rest; scoped per `clientId`, `endUserId`, and permission scope (read vs. write). # Add Mesh onramp integrations to your "Buy" lineup Source: https://docs.meshconnect.com/extend/onramp By the end of this guide, you'll know how to surface Mesh's onramp integrations in your "Buy" lineup — including fetching live quotes and launching users directly into a specific provider's flow. **Before you start** * Your core Mesh integration is working end-to-end (see [Prepare for go-live](/build/go-live)) * You have a "Buy" or onramp flow in your app that you want to extend with Mesh integrations ## Overview Mesh supports multiple onramp products. If you have a "lineup" of onramps in your app, these Mesh integrations can be added directly to your UX alongside other onramps in your "Buy" flow. ## Add exchanges to the "lineup" in your "Buy" tab **Why?**: If a user has an exchange account, there's less friction for them to buy through that account (they're already KYC'd and have added payment methods). That's in contrast to a separate provider where they have to create an account, KYC, and add payment methods. You can simply add the Mesh integration (eg. `Binance`) into your UX the same way you have other providers. **Pro tip**: You can also pull the exchange's icon or logo from Mesh's Integrations endpoint ([/api/v1/integrations](https://docs.meshconnect.com/api-reference/managed-account-authentication/retrieve-the-list-of-all-available-integrations)) if you'd like to add that next to the exchange name in your UX. ## Use Mesh's quote API to show "You receive" quotes **Endpoint**: [/api/v1/transfers/managed/quote](https://docs.meshconnect.com/api-reference/managed-transfers/quote-transfer) **Why?**: This is optional and likely not necessary (if a user has an account with a certain exchange, they'll probably choose that option anyway). But if you'd like to show a "You receive" estimate for each option, you can fetch quotes using this endpoint.
X-Client-Id required
Your Mesh Client ID.
X-Client-Secret required
Your Mesh API Key.
amountInFiat required
The base amount of the specified `fiatCurrency` to be transferred.
fiatCurrency required
The 3 character currency code. Only USD supported for now.
symbol required
Symbol of the asset being purchased.
networkId required

Mesh's unique identifier for the network to be used for this `toAddress`.

Use the **Get networks** endpoint ([/api/v1/transfers/managed/networks](https://docs.meshconnect.com/api-reference/managed-transfers/get-networks)) to pull a list of all supported networks and the corresponding Mesh `networkId`. These values won't change, so you do not have to hit this endpoint before each Link Token request.

toAddress required
The destination to which the specified `symbol` can be sent on the specified `networkId`.
brokerType required
The integration that you will wish to receive a quote from. Eg. `binanceInternationalDirect` or `coinbase`.
```bash theme={null} curl --request POST \ --url https://integration-api.meshconnect.com/api/v1/transfers/managed/quote \ --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "amountInFiat": 99.99, // Replace "fiatCurrency": "USD", // Replace (only USD supported for now) "symbol": "ETH", // Replace "toAddress": "abc123", // Replace "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611", // Ethereum, replace "brokerType": "binanceInternationalDirect" // Replace } ' ``` **Important notes**: * There are two quotes returned in the response: `minAmountFiat` & `maxAmountFiat`. Display **`minAmountFiat`** as the more conservative estimate — it assumes the user doesn't already hold that token and will need to fund the purchase from scratch. * Be sure to pay attention to the `isEligible` field in the response to know if you can show a quote (the `minEligibleAmount` & `minEligibleAmountInFiat` indicate the exchange's minimum withdrawal for that token on that network).
## Launch Link directly into the chosen provider's flow **Why?**: If the user has already selected the integration in your UX, there's no need to see the Mesh catalog. You can launch them directly into their chosen provider's flow.
X-Client-Id required
Your Mesh Client ID.
X-Client-Secret required
Your Mesh API Key.
userId required
A unique, persistent user identifier. Personally identifiable information such as an email address or phone number should not be used. 300 characters length maximum.
restrictMultipleAccounts optional

Defaults to `true`, which is standard used for any transfer flow.

On some non-transfer flows (ie. "read" use cases), a user could connect multiple accounts in a row if this value were `false`. Sometimes valuable for withdrawal use cases to let the user create an external "address book".

integrationId optional

A unique Mesh identifier representing a specific integration.

Use the **Get integrations** endpoint ([/api/v1/transfers/managed/integrations](https://docs.meshconnect.com/api-reference/managed-transfers/get-integrations)) to pull a list of integrations and the corresponding Mesh `integrationId`. These values won't change, so you do not have to hit this endpoint before each Link Token request.

To be used if the user selects the integration in your UX before launching Mesh (most commonly in an `onramp` flow).

transferOptions.transactionId optional
A unique transaction identifier used to tie back to your data or track this transaction in future calls to Mesh.
transferOptions.transferType optional
Ensures the language and flow matches your user's mental model for the type of transfer they're doing. Defaults to `deposit`.
transferOptions.isInclusiveFeeEnabled optional

Specifies if fees should be included in the amount to transfer.

`false` is standard for `deposit` and `payment`, meaning any applicable fees are on top of the deposit/payment amount. `true` is standard for `onramp`, meaning the amount the user receives is the amount specified minus applicable fees.

transferOptions.generatePayLink optional

When `true`, this request will return a url in addition to the Link token that can be used to launch Mesh Link in a separate web page.

This should only be used if you're launching Mesh in a separate webpage (see more about "PayLinks" in the [Launch the Mesh SDK](/build/launch-sdk) guide).

transferOptions.amountInFiat optional

The fiat-equivalent amount of the symbol to be purchased.

To be used if the user enters an amount in your UX before launching Mesh (most commonly in an `onramp` flow).

transferOptions.toAddresses.networkId optional required for transfers

Mesh's unique identifier for the network to be used for this `toAddress`.

Use the **Get networks** endpoint ([/api/v1/transfers/managed/networks](https://docs.meshconnect.com/api-reference/managed-transfers/get-networks)) to pull a list of all supported networks and the corresponding Mesh `networkId`. These values won't change, so you do not have to hit this endpoint before each Link Token request.

transferOptions.toAddresses.symbol optional required for transfers
The symbol of the asset that can be transferred to this `toAddress`.
transferOptions.toAddresses.address optional required for transfers
The destination to which the specified `symbol` can be sent on the specified `networkId`.
transferOptions.toAddresses.amount optional required for payments

Exact amount of the asset that should be transferred.

This parameter is optional for `deposit` and `onramp`, but required when transferType: `payment`. Not to be used in combination with the `amountInFiat` field.

transferOptions.toAddresses.displayAmountInFiat optional

A fiat-equivalent amount that will be shown to the user in the Mesh UI.

This ensures a consistent experience from your checkout experience to Mesh. It will only be used if is within 1% of the `amountInFiat` Mesh determines based on it pricing data. This is generally used for non-stablecoin payments, as Mesh maps stablecoins to a 1:1 price with USD for display purposes.

```bash theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "integrationId": "5620bf49-3240-4f85-8b4f-9dd6261597e2", // Binance Connect (Replace) "transferOptions": { "transactionId": "UNIQUE_TRANSACTION_ID", // Replace "transferType": "onramp", "amountInFiat": 100.00, // Replace "isInclusiveFeeEnabled": true, "generatePayLink": false, "toAddresses": [ // Replace (this could be a full array, or only one destination if the user selects a token/network in your UX before launching Mesh) { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana "symbol": "USDC", // Replace "address": "xxx" // Replace } ] } } ' ```
## What's next Explore the other **Extend** guides: * [Add Mesh to your withdrawal flow](/extend/withdrawal) — enable automated address retrieval for user withdrawals. * [Verify self-hosted wallets](/extend/verify-wallets) — add wallet ownership verification for compliance or security. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Add Mesh onramp integrations to your "Buy" lineup** Integrate exchange-based onramps into your Buy flow: surface exchanges in your lineup, optionally fetch quotes, and deep-link users directly into a chosen provider's flow. **Quote endpoint** (optional): `POST /api/v1/transfers/managed/quote` Params: `amountInFiat` | `fiatCurrency` (USD only) | `symbol` | `networkId` | `toAddress` | `brokerType` Response: `minAmountFiat` (show this — conservative estimate) | `maxAmountFiat` | `isEligible` | `minEligibleAmount` | `minEligibleAmountInFiat` **Get networkIds**: Use GET /api/v1/transfers/managed/networks to fetch the full list of supported networks and their networkId values. These values don't change — safe to cache permanently, no need to call this endpoint before every Link Token request. **Deep-link to provider**: Pass `integrationId` in Link Token request. Get IDs from `GET /api/v1/transfers/managed/integrations` (stable, safe to cache). **Link Token params for onramp**: `transferType: "onramp"` | `isInclusiveFeeEnabled: true` (user receives amount minus fees) | `amountInFiat` (if user selected amount in your UX) | `integrationId` (if user selected provider in your UX) **Exchange icons**: Pull `logoUrl` from `GET /api/v1/integrations` response for display in your lineup. **Note**: Quote API is optional — users with exchange accounts tend to self-select their preferred provider anyway. **Quote API — request body**: ```javascript theme={null} curl --request POST \ --url https://integration-api.meshconnect.com/api/v1/transfers/managed/quote \ --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "amountInFiat": 99.99, // Replace "fiatCurrency": "USD", // Replace (only USD supported for now) "symbol": "ETH", // Replace "toAddress": "abc123", // Replace "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611", // Ethereum, replace "brokerType": "binanceInternationalDirect" // Replace } ' ``` **Onramp Link Token — with provider pre-selected**: ```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "integrationId": "5620bf49-3240-4f85-8b4f-9dd6261597e2", // Binance Connect (Replace) "transferOptions": { "transactionId": "UNIQUE_TRANSACTION_ID", // Replace "transferType": "onramp", "amountInFiat": 100.00, // Replace "isInclusiveFeeEnabled": true, "generatePayLink": false, "toAddresses": [ // Replace (this could be a full array, or only one destination if the user selects a token/network in your UX before launching Mesh) { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana "symbol": "USDC", // Replace "address": "xxx" // Replace } ] } } ' ``` # Verify self-hosted wallets Source: https://docs.meshconnect.com/extend/verify-wallets By the end of this guide, you'll have wallet ownership verification working in your integration, with the signed message payload captured and ready for compliance and audit purposes. **Before you start** * Your core Mesh integration is working end-to-end (see [Prepare for go-live](/build/go-live)) * You've determined which addresses or wallet types require verification in your app ## Background and Overview Travel Rule regulations have long existed in traditional financial markets as a way for regulators to enforce laws pertaining to the movement of money (eg. anti-money laundering, terrorist financing, sanctions, etc.). In short, regulated financial institutions are responsible for knowing who they are receiving assets from, and where they're sending assets to. The crypto world has long-awaited clear Travel Rule guidelines for crypto, and on July 4th, 2024 the European Banking Authority (EBA) released its final guidelines. Other global regulatory regimes are likely not far behind. This guide dives into detail around Mesh's support for verifying ownership of a self-hosted wallet. ## How does wallet verification work? The nature of self-hosted wallets is that they don't know who the user is. In other words, a user doesn't KYC with MetaMask like they would with Coinbase or another centralized exchange. So verifying ownership of a self-hosted wallet is not about receiving user information from that wallet. Instead, it is about confirming that the user you know is in control of the wallet in question. Verification pertains to an address (ie. 0x31…cF98), not a wallet app (ie. MetaMask). Keep in mind that a user can interact with the same wallet (ie. address) from multiple wallet apps, and can also interact with multiple wallets from within the same wallet app. The EBA specifies that one acceptable method of verifying ownership of a self-hosted wallet is having the user sign a self-attestation of ownership (ie. a message) in that wallet. The message doesn't have to be anything specific, but the message the user signs must be the exact message requested by you (or Mesh). A message signature is an off-chain event (ie. it's completely gasless), but is also fully verifiable with the combination of the `signedMessageHash`, the `address`, and `message`. In Mesh's wallet verification flow, the user is prompted to sign a message immediately after connecting their wallet. Similar to connection requests and transfer requests, the user won't need to configure anything — all they have to do is review and sign. Wallet verification can be completely independent of a transfer (ie. the user only connects their wallet and signs the message), or can be used in combination with a transfer (ie. the user connects their wallet, signs a message, and then continues to the transfer). ## How to use wallet verification in Mesh Link ### 1. Use the `verifyWalletOptions` object in the Link Token request
X-Client-Id required
Your Mesh Client ID.
X-Client-Secret required
Your Mesh API Key.
userId required
A unique, persistent user identifier. Personally identifiable information such as an email address or phone number should not be used. 300 characters length maximum.
restrictMultipleAccounts optional

Defaults to `true`, which is standard used for any transfer flow.

On some non-transfer flows (ie. "read" use cases), a user could connect multiple accounts in a row if this value were `false`. Sometimes valuable for withdrawal use cases to let the user create an external "address book".

integrationId optional

A unique Mesh identifier representing a specific integration.

Use the **Get integrations** endpoint ([/api/v1/transfers/managed/integrations](https://docs.meshconnect.com/api-reference/managed-transfers/get-integrations)) to pull a list of integrations and the corresponding Mesh `integrationId`. These values won't change, so you do not have to hit this endpoint before each Link Token request.

To be used if the user selects the integration in your UX before launching Mesh (most commonly in an `onramp` flow).

verifyWalletOptions.verificationMethods optional required for verifications

Method by which the user must verify their self-hosted wallet.

Enum to support future verification methods, but only one option for now.

verifyWalletOptions.message optional

The message the user should sign in their wallet.

The exact language isn't important. Mesh has standard language if this isn't provided.

verifyWalletOptions.networkId optional

A unique Mesh identifier for the network on which the user must verify their self-hosted address.

Use the **Get networks** endpoint ([/api/v1/transfers/managed/networks](https://docs.meshconnect.com/api-reference/managed-transfers/get-networks)) to pull a list of all supported networks and the corresponding Mesh `networkId`. These values won't change, so you do not have to hit this endpoint before each Link Token request.

To be used if you need your user to verify an address on a specific network. Should not be used in combination with `networkType`.

verifyWalletOptions.networkType optional

The network type on which the user must verify their self-hosted address.

To be used if you need your user to verify an address on a particular network type (eg. EVM). Should not be used in combination with `networkId`.

verifyWalletOptions.address optional

A list of address from which the user can verify ownership. User is not allowed to verify an address outside this list.

Used if you need your user to verify ownership of a specific address. If not provided, user can verify ownership of any self-hosted address they connect.

```bash theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "integrationId": "757e703f-a8fe-4dc4-d0ec-08dc6737ad96", // Phantom, replace, optional "verifyWalletOptions": { "verificationMethods": [ "signedMessage" ], "message": "MESSAGE_TO_BE_SIGNED", // Replace "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana, replace, optional "networkType": "solana", // Replace, optional "addresses": [ // Optional "xxx" // Replace ] } } ' ``` **Notes:** * Wallet verification is fully compatible with the deposit & payment use cases. If you require users to verify that they own a self-custody wallet before initiating a deposit or payment from that wallet, you can add these configurations into the same Link Token request. * This will only impact the user experience for self-hosted wallets (ie. MetaMask, Phantom, etc.). This will not change anything about the experience for centralized exchanges (ie. Binance, Coinbase, etc.).
### 2. Store the `walletMessageSigned` payload After the user signs the message in their wallet, you will receive the `walletMessageSigned` SDK event with the following payload: * `signedMessageHash` * `message` * `address` * `timeStamp` * `isVerified` (boolean) This data can be stored on your side for audit purposes, as well as to improve the return user experience within your UX. **Note:** This is the only time the `signedMessageHash` will be provided — Mesh does not retain this data. ## Test your implementation Test using MetaMask (Sepolia testnet) or Phantom (Solana Devnet). See [Sandbox & Testnets](/resources/sandbox-testnets) for wallet setup instructions. **What to test:** * **Full flow**: Launch a sandbox Link session with `verifyWalletOptions` configured. Connect a test wallet — the user should be prompted to sign immediately after connecting, with no additional configuration needed. * **Payload capture**: Confirm your `onEvent` handler receives `walletMessageSigned` with all five fields present: `address`, `isVerified`, `message`, `signedMessageHash`, and `timeStamp`. * **Message match**: Verify that `message` in the payload exactly matches what you passed in `verifyWalletOptions.message`. If you didn't provide a custom message, confirm Mesh's default language appears. * **Rejection path**: Reject the signature request in your test wallet (click "Reject" in MetaMask). Confirm you receive the `verifyWalletRejected` event and that your app handles it gracefully. * **Payload storage**: Confirm you're capturing and storing `signedMessageHash` at the time of the event — it won't be retrievable afterward. ## What's next Explore the other **Extend** guides: * [Add Mesh onramp integrations to your "Buy" lineup](/extend/onramp) — add exchange-based onramps to your "Buy" flow. * [Add Mesh to your withdrawal flow](/extend/withdrawal) — enable automated address retrieval for user withdrawals. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Verify self-hosted wallets** Wallet ownership verification via signed message (EBA Travel Rule compliance). User signs a message in their wallet immediately after connecting; you store the signed payload for audit. **Link Token verifyWalletOptions params**: `verificationMethods: ["signedMessage"]` (required) | `message` (optional — Mesh provides default if omitted) | `networkId` (optional, specific network; do not use with `networkType`) | `networkType` (optional, e.g. "solana" or "evm") | `addresses[]` (optional — restrict to specific addresses) **Get networkIds**: Use GET /api/v1/transfers/managed/networks to fetch the full list of supported networks and their networkId values. These values don't change — safe to cache permanently, no need to call this endpoint before every Link Token request. **Compatible with transfer flows**: Add `verifyWalletOptions` to any deposit or payment Link Token request. User verifies wallet then proceeds to transfer. Does not affect CEX flows. **SDK event: walletMessageSigned** Payload: `signedMessageHash` | `message` | `address` | `timeStamp` | `isVerified` (boolean) Critical: Store `signedMessageHash` at event time — Mesh does not retain it. **Rejection event**: `verifyWalletRejected` fires if user declines to sign. **Test**: MetaMask (Sepolia testnet) or Phantom (Solana Devnet). See Sandbox & Testnets guide for setup. **Verify use case — Link Token request body**: ```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "integrationId": "757e703f-a8fe-4dc4-d0ec-08dc6737ad96", // Phantom, replace, optional "verifyWalletOptions": { "verificationMethods": [ "signedMessage" ], "message": "MESSAGE_TO_BE_SIGNED", // Replace "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana, replace, optional "networkType": "solana", // Replace, optional "addresses": [ // Optional "xxx" // Replace ] } } ' ``` **`walletMessageSigned` event payload** (capture immediately — Mesh does not store it): ```json theme={null} { "address": "0xABC...", "isVerified": true, "message": "I confirm I own this wallet address.", "signedMessageHash": "0xDEF...", "timeStamp": "2024-07-04T12:00:00Z" } ``` # Add Mesh to your withdrawal flow Source: https://docs.meshconnect.com/extend/withdrawal By the end of this guide, you'll know how to use Mesh to power automated address retrieval in your withdrawal flow — eliminating the risk of user error from manual copy/paste. **Before you start** * Your core Mesh integration is working end-to-end (see [Prepare for go-live](/build/go-live)) * You have a withdrawal or payout flow in your app ## Overview Most withdrawal or payout flows require users to select an asset/network and then paste an address, just like they do in a deposit flow. And their primary concern is the same: losing funds because they make a mistake (ie. wrong network, wrong address, etc.). Mesh enables automated address retrieval for user withdrawals or payouts from your platform. After connecting an account, Mesh lets you pull addresses with clear asset/network mapping — eliminating human error and the risk of lost funds. Additionally, after a deposit, "refund addresses" are also available via Mesh SDK events and webhooks. Together, these can form the foundation of an "address book" where users save their preferred withdrawal destinations for future use. ## Register for, and consume Mesh Transfer Status webhooks **Why?**: Mesh provides real-time updates to transfer statuses which can be used to power several things for your app. But one piece (providing the `RefundAddress` for user transfers from external accounts) can be the underpinning of creating a seamless roundtrip journey. * See the [Transfer status webhooks](/resources/webhooks) guide for more info. * Callback URIs can be added in the Mesh dashboard in **Account > API keys > Webhooks**. ## Save the `RefundAddress` and associated details when the user makes a deposit, and surface it as a withdrawal destination. **Why?**: Creating a seamless roundtrip journey for the user builds trust. Users often want to withdraw funds back to the same source from where they deposited. This process makes that easy. It feels personalized. * The objects below are in Mesh transfer webhook events. Save this data, and surface it as a possible withdrawal destination in your UX (eg. `Your Binance account (0x31…cF98)`). | Parameter | Description | | ----------------------- | ------------------------------------------------------------------------------------- | | `SourceAccountProvider` | Where the deposit came from (eg. `Binance`) | | `Chain` | The blockchain used for the transfer (eg. `Ethereum`) | | `Token` | The token used for the transfer (eg. `USDC`) | | `RefundAddress` | Where the user can receive funds back to (eg. a deposit address within that account). | * **Important notes**: * Take proper care to ensure you associate it with the correct user. * That `RefundAddress` is specific to the `Chain` & `Token`. Any transfer of that specific token on that chain to that address is safe, but sending any other token or on any other network to that address may result in lost funds. * See [Concepts](/resources/concepts) for more information about `refundAddress` ## Add a `Connect account` option in the withdrawal flow **Why?**: Users' number-one fear when transferring crypto is making a mistake — wrong network, wrong address. With Mesh Link, that becomes impossible because it's all automated. Providing an automated address retrieval option adds a layer of trust and certainty to the withdrawal process. * Call for a Link Token using the request structure below, and launch the Mesh Link SDK for the user.
X-Client-Id required
Your Mesh Client ID.
X-Client-Secret required
Your Mesh API Key.
userId required
A unique, persistent user identifier. Personally identifiable information such as an email address or phone number should not be used. 300 characters length maximum.
restrictMultipleAccounts optional

Defaults to `true`, which is standard used for any transfer flow.

On some non-transfer flows (ie. "read" use cases), a user could connect multiple accounts in a row if this value were `false`. Sometimes valuable for withdrawal use cases to let the user create an external "address book".

integrationId optional

A unique Mesh identifier representing a specific integration.

Use the **Get integrations** endpoint ([/api/v1/transfers/managed/integrations](https://docs.meshconnect.com/api-reference/managed-transfers/get-integrations)) to pull a list of integrations and the corresponding Mesh `integrationId`. These values won't change, so you do not have to hit this endpoint before each Link Token request.

To be used if the user selects the integration in your UX before launching Mesh (most commonly in an `onramp` flow).

```bash theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "integrationId": "47624467-e52e-4938-a41a-7926b6c27acf" // Coinbase, replace, optional } ' ``` * The user will select an account, authenticate / provide access, and then their session will end. * You will receive an SDK event called `integrationConnected`. You should use the `onIntegrationConnected()` callback function to save the `tokenId` & `accountName` objects, associated with that `userId`. * See the [Supercharge return-users](/build/return-users) guide for more information about this step. * Call the Mesh **Get deposit addresses** endpoint ([/api/v1/transfers/managed/address/list](https://docs.meshconnect.com/api-reference/managed-transfers/get-list-of-deposit-addresses)), supplying the `authToken` (same as `tokenId`) & `type` (same as `accountName`). * The response will return all addresses for each network on which the user can receive the specified crypto token. It will also have other helpful data to build you UX like a `networkName` for displaying the network and a `logoUrl` for that network. * Important note: Be sure to capture the `memo` for deposits of relevant assets (eg. XRP) into any custodial platform.
X-Client-Id required
Your Mesh Client ID.
X-Client-Secret required
Your Mesh API Key.
type required

The type of integration from which your pulling the user's deposit addresses.

This is `deFiWallet` for self-hosted wallets, or a name of an exchange account (eg. `uphold`).

authToken required

The `tokenId` returned to you in the `integrationConnected` event when the user connected this account.

This provides access to read data from the account.

symbol required
The symbol of the asset for which you want the user's deposit address(es) at the linked account.
networks.networkId optional

Mesh's unique identifier for a specific network for which you would like to pull the user's deposit address for `symbol` at the specified integration.

Use the **Get networks** endpoint ([/api/v1/transfers/managed/networks](https://docs.meshconnect.com/api-reference/managed-transfers/get-networks)) to pull a list of all supported networks and the corresponding Mesh `networkId`. These values won't change, so you do not have to hit this endpoint before each Link Token request.

This should not be used with `caipId`. Use one or the other. If both are left blank, this endpoint will return addresses for all eligible networks for the specified `symbol`.

networks.caipId optional

A standardized identifier for blockchain networks/assets (e.g. `eip155:1` is Ethereum mainnet).

This should not be used with `networkId`. Use one or the other. If both are left blank, this endpoint will return addresses for all eligible networks for the specified `symbol`.

```bash theme={null} curl --request POST \ --url https://integration-api.meshconnect.com/api/v1/transfers/managed/address/list \ --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "type": "ACCOUNT_TYPE", // Replace "authToken": "USER_TOKEN_ID", // Replace "symbol": "USDC", // Replace "networks": [ // Replace, example array below, optional { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d" // Solana }, { "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611" // Ethereum }, { "networkId": "aa883b03-120d-477c-a588-37c2afd3ca71" // Base } ] } ' ```
## What's next Explore the other **Extend** guides: * [Add Mesh onramp integrations to your "Buy" lineup](/extend/onramp) — add exchange-based onramps to your "Buy" flow. * [Verify self-hosted wallets](/extend/verify-wallets) — add wallet ownership verification for compliance or security. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Add Mesh to your withdrawal flow** Automated address retrieval for withdrawal/payout flows: consume transfer webhooks for `RefundAddress`, retrieve deposit addresses via API, offer "Connect account" in your withdrawal UI. **RefundAddress from transfer webhooks**: Save `RefundAddress` + `Chain` + `Token` + `SourceAccountProvider` per userId. Surface as a withdrawal destination (e.g. "Your Binance account (0x31…cF98)"). Note: `RefundAddress` is specific to that chain+token combination. **Deposit address retrieval**: `POST /api/v1/transfers/managed/address/list` Params: `type` (broker type, e.g. "uphold", or "deFiWallet" for self-custody) | `authToken` (= `tokenId` from onIntegrationConnected) | `symbol` | `networks[].networkId` or `networks[].caipId` (optional — omit for all networks) Response includes: address per network, `networkName`, `logoUrl`, `memo` (important for XRP, XLM) **Get networkIds**: Use GET /api/v1/transfers/managed/networks to fetch the full list of supported networks and their networkId values. These values don't change — safe to cache permanently, no need to call this endpoint before every Link Token request. **Withdrawal Link Token**: No `transferOptions` needed — user just connects account. Receive `integrationConnected` → save `tokenId` + `accountName` → call address list endpoint. **Optional**: Pass `integrationId` to pre-select the exchange. Set `restrictMultipleAccounts: false` to let users build an "address book" across multiple accounts. **Withdrawal Link Token — minimal request** (no `transferOptions` — user just connects account): ```javascript theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true } ' ``` **Deposit address list — canonical request body** (call after `onIntegrationConnected`): ```javascript theme={null} curl --request POST \ --url https://integration-api.meshconnect.com/api/v1/transfers/managed/address/list \ --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "type": "ACCOUNT_TYPE", // Replace "authToken": "USER_TOKEN_ID", // Replace "symbol": "USDC", // Replace "networks": [ // Replace, example array below, optional { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d" // Solana }, { "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611" // Ethereum }, { "networkId": "aa883b03-120d-477c-a588-37c2afd3ca71" // Base } ] } ' ``` # Intelligent catalog filtering Source: https://docs.meshconnect.com/resources/catalog-filtering By the end of this guide you'll understand the filtering rules that Mesh applies automatically during Link initialization, and therefore why certain integrations may not appear for your users. ## Overview Mesh automatically applies intelligent filtering during Link initialization to ensure users only see compatible, compliant, and viable integrations (wallets and exchanges). This improves success rates, avoids regulatory blockers, and enhances the user experience by not letting them go down dead end roads. Below is a complete breakdown of the filters we apply, categorized by use case and user type. If you need help validating provider coverage for specific asset/network combinations or regions, please reach out to your Mesh account manager. ## Asset & Network Compatibility Mesh only displays integrations that support at least one of the asset/network combinations provided as an eligible destination in your Link Token request. **Applies to:** * All clients **How it works:** * If a user selects USDC on Solana, only integrations that support USDC.sol will be shown. * If none of the provided asset/network combinations are supported by an integration, it is excluded. ## VASP ID Requirement This is a Travel Rule filter. Some exchanges require the sending platform to provide a valid VASP ID when a transfer is originating from certain jurisdictions. If your business is a VASP, and Mesh does not have your VASP ID on file, and the user is in a restricted country, impacted exchanges won't appear in the integrations catalog. **Applies to:** * Custodial platforms (e.g. neobanks, exchanges, fintechs) **How it works:** * If your business is a VASP, and… * Mesh does not have a VASP ID for your client, and… * the user IP address is in an affected country (AE, AT, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GR, HR, HU, IE, IS, IT, JE, KR, LI, LT, LU, LV, MT, NL, NO, NZ, PL, PT, RO, SE, SG, SI, SK), then… * Impacted exchanges are excluded from the integrations catalog ## Wallet Ownership Requirement This is a Travel Rule filter. When a user is sending to a self-hosted wallet, some exchanges enforce wallet ownership verification in certain jurisdictions. Because self-custody wallets cannot provide ownership proof, Mesh impacted exchanges out of the integrations catalog in these scenarios. **Applies to:** * Self-custody wallets **How it works:** * If your business has a self-custody wallet model, and… * The user's IP is in an affected country (AE, AT, BE, BG, CH, CY, CZ, DE, DK, EE, ES, FI, FR, GB, GR, HR, HU, IE, IS, IT, JE, KR, LI, LT, LU, LV, MT, NL, NO, NZ, PL, PT, RO, SE, SG, SI, SK), and… * The transfer amount exceeds 1000 EUR, then… * Users in the following Southeast and East Asian countries will have impacted exchanges filtered out regardless of amount: SG, HK, PH, KR * Impacted exchanges are excluded from the integrations catalog ## Exchange Geography Restrictions Mesh enforces IP-based restrictions at the provider-level to reflect each exchange's supported regions. These rules are enforced directly based on provider policies and automatically reflected in the Link session. **Applies to:** * All clients **Examples:** * Binance: Not shown to users with IPs in US, Canada, or Netherlands * Robinhood: Only shown to users with US IPs ## Use Case Restrictions This is a Travel Rule filter. Some providers restrict transfers based on the type of platform their user is transferring to. Mesh is bound by and abides by all exchange-level compliance requirements. When we believe that transfers to a certain client maybe blocked by a certain exchange in some geographies, we will remove that integration from the client's catalog for users in those geographies. **Applies to:** * Varies ## What's next If you need help validating integration coverage for specific asset/network combinations or user regions, reach out to your Mesh representative. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Intelligent catalog filtering** Mesh automatically filters which integrations appear in Link during session initialization. No client action required — filtering is automatic. **5 filter types**: 1. **Asset & Network Compatibility** (all clients): Only integrations supporting at least one `toAddresses` asset/network pair are shown. 2. **VASP ID Requirement** (Travel Rule — custodial platforms): If your business is a VASP, Mesh lacks your VASP ID, and user IP is in an affected country (EU + NZ, SG, KR) → impacted exchanges hidden. 3. **Wallet Ownership Requirement** (Travel Rule — self-custody wallets): If destination is self-custody wallet, user IP in affected country, and transfer > 1000 EUR (or any amount in SG, HK, PH, KR) → impacted exchanges hidden. 4. **Exchange Geography Restrictions** (all clients): Provider-level IP restrictions. Examples: Binance hidden for US/Canada/Netherlands IPs; Robinhood shown only for US IPs. 5. **Use Case Restrictions** (varies): Some providers restrict transfers based on platform type and geography. **Action**: Contact Mesh account manager to validate coverage for specific asset/network combinations or user regions, or to provide VASP ID. # Common errors Source: https://docs.meshconnect.com/resources/common-errors This page documents the most common errors developers run into when integrating with Mesh — drawn from real support cases and developer feedback. Each entry covers the symptom, root cause, and fix. ## SDK domain not authorized **Symptom:** The Mesh SDK loads but immediately shows an authorization error, or the connection fails silently. May surface as a CORS error in the browser console. **Root cause:** Mesh validates that the SDK is loaded from an approved domain. Any domain not on your allowlist — including `localhost` — is rejected. This most commonly affects developers testing locally or on a staging URL they forgot to add. **Fix:** In the Mesh dashboard, go to **Account > API keys > Access** and add every domain where your app runs: * `localhost:3000` (or your local port) * Any staging or preview domains * Your production domain(s) Changes take effect within a few minutes. ## SDK iframe blocked by Content Security Policy **Symptom:** The Mesh Link modal never appears — the area where Link should render shows as a blank grey box. The browser console shows a CSP error such as: `Refused to frame 'https://web.meshconnect.com/' because an ancestor violates the following Content Security Policy directive: "frame-src 'self'"` Image **Root cause:** Your app's Content Security Policy doesn't permit Mesh's hosted iframe to load. **Fix:** Add `*.meshconnect.com` to your CSP `frame-src` directive: ```text theme={null} Content-Security-Policy: frame-src 'self' *.meshconnect.com; ``` If you set CSP via an HTML meta tag: ```html theme={null} ``` ## Ad-blocker preventing OAuth connection **Symptom:** After selecting an OAuth-based integration (eg. Coinbase, Gemini), the user gets stuck on a loading spinner and never completes authentication. The Link UI may appear initially but the OAuth flow never resolves. **Root cause:** Ad-blocking software can interfere with Link's ability to use browser storage and complete OAuth redirect flows. This includes browser extensions (uBlock Origin, AdBlock Plus, etc.) and browsers with built-in ad-blocking — most commonly Brave. **Fix:** The user should disable their ad-blocker for your domain, or switch to a standard browser without built-in ad-blocking. Ad-blocking software is not officially supported with Mesh Link. If this surfaces frequently in support, consider adding a prompt in your UI that detects ad-blocker presence and suggests disabling it before launching Link. ## Link Token expired or already used **Symptom:** `openLink()` throws an error or the user immediately sees an error screen when Link opens. The error typically references an invalid or expired token. **Root cause:** Link Tokens are **short-lived (10 minutes) and single-use**. A token fetched at page load may expire before the user clicks the connect button. A token from a previous session can never be reused. **Fix:** * Fetch a fresh Link Token immediately before calling `openLink()` — on the button click, not on page load. * After each session (successful or failed), discard the token. Never cache or reuse tokens. ## Network or symbol not supported / empty catalog **Symptom:** Link opens but the integration catalog is empty, or Link returns an error on the `toAddresses` fields. **Root cause (most common):** An ERC-20 contract address (eg. `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`) was passed as the `symbol` field instead of the token ticker (eg. `USDC`). Mesh expects the **token symbol**, not the contract address. **Other causes:** * A `networkId`/`symbol` combination that doesn't exist in Mesh's catalog. * A misspelled token symbol. **Fix:** Use the ticker symbol for `symbol`. Key network IDs for reference: | Network | networkId | | -------- | -------------------------------------- | | Ethereum | `e3c7fdd8-b1fc-4e51-85ae-bb276e075611` | | Solana | `0291810a-5947-424d-9a59-e88bb33e999d` | | Base | `aa883b03-120d-477c-a588-37c2afd3ca71` | To maximize coverage, include multiple `networkId`/`symbol` pairs in `transferOptions.toAddresses` — eg. USDC on Ethereum, Solana, and Base. ## No eligible assets after account connection **Symptom:** The user connects their exchange account but sees a "No eligible assets" screen. The `transferNoEligibleAssets` SDK event fires. **Root cause:** The user's connected account doesn't hold any tokens matching the `networkId`/`symbol` pairs in your `toAddresses` array. This is expected behavior when there's a genuine mismatch — not a bug — but it needs to be handled gracefully. **Common causes:** * The user holds ETH, but you only accept USDC. * The user holds USDC on Ethereum, but `toAddresses` only includes USDC on Solana. * The user genuinely has a zero balance. * In sandbox: accidentally using the `Mesh2` test account, which has an empty portfolio. **Fix:** * Broaden your `toAddresses` array to include more asset/network pairs. * Listen for `transferNoEligibleAssets` and use its `arrayOfTokensHeld` payload to show a helpful message in your UX (eg. "You don't have USDC in that account. Try a different account or deposit manually."). * In sandbox: use the `Mesh` test account (full portfolio) for general testing. Use `Mesh2` specifically to test your empty-state UI. ## Webhook signature validation fails **Symptom:** Your webhook handler rejects every incoming event with a signature mismatch, even though your webhook secret looks correct. **Root cause:** The HMAC-SHA256 signature in Mesh's `X-Mesh-Signature-256` header is computed over the **raw request body bytes** — exactly as received off the wire. The most common mistake is JSON re-serializing the body before computing the HMAC. Parsing the body and then re-serializing it (eg. calling `JSON.stringify(JSON.parse(body))`) can silently change key ordering, whitespace, or number formatting — producing a different byte sequence and therefore a wrong HMAC. **Fix:** Capture the **raw body bytes** before any parsing, compute the HMAC over those raw bytes, then parse the JSON afterward. ```javascript theme={null} // Node.js / Express — correct approach const crypto = require('crypto'); app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-mesh-signature-256']; const hmac = crypto .createHmac('sha256', process.env.MESH_WEBHOOK_SECRET) .update(req.body) // req.body is a raw Buffer — do NOT re-serialize .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(hmac))) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body); // parse AFTER verification // handle event... res.sendStatus(200); }); ``` **The key mistake:** using `express.json()` middleware on the webhook route. This parses the body into a JS object before you can capture the raw bytes, making correct HMAC verification impossible. Use `express.raw({ type: 'application/json' })` instead, scoped to the webhook route only. ## Transfer stays `pending` or finalizes as `failed` **Symptom:** A transfer is initiated but the `succeeded` webhook never arrives, or arrives as `failed`. **Common causes and fixes:** * **Exchange-side delay**: CEX transfers can take minutes to hours to post on-chain. The `pending` webhook fires when the transfer is initiated at the exchange; `succeeded` fires once it confirms on-chain. For Coinbase and Binance, this can occasionally take up to 24 hours. This is normal — build your flow to wait for `succeeded` before crediting the user. * **Amount below exchange minimum**: Most exchanges enforce a minimum withdrawal. If the amount is too small, the exchange rejects it. Use `minAmount` / `minAmountInFiat` in your `toAddresses` to surface minimums before the user initiates. * **Incorrect destination address format**: Some exchange/network combinations enforce strict address validation. Verify that destination addresses are valid for the specified network. * **User 2FA timeout**: If the user took too long to approve the MFA step, the exchange session may have expired and the transfer was never sent. * **Sandbox limitation**: In sandbox, the `pending` webhook is not guaranteed — transfers may skip directly to `succeeded` or `failed`. Don't build logic that depends on receiving `pending` during testing. For transfers stuck in `pending` in production, contact your Mesh representative with the `TransferId` from the webhook payload. ## OAuth integrations show blank screen or raw code in mobile WebView **Symptom:** When launching an OAuth-based integration (eg. Uphold, Paribu) from inside a native app's in-app browser, the user sees a blank white screen or a page of raw code instead of the provider's login screen. The OAuth flow never completes. **Root cause:** Mesh Link's OAuth flows open the provider's authorization URL in a new window via `window.open()`. Native in-app browsers — iOS `WKWebView`, Android `WebView`, and React Native / Flutter equivalents — block or silently drop `window.open()` calls by default unless the host app explicitly implements a popup handler. The two symptoms reflect slightly different failure modes: * **Blank screen:** The WebView opens an empty window but never navigates to the OAuth URL — the popup is created but the navigation is discarded. * **Raw code screen:** The OAuth callback with an authorization code lands in the wrong context and renders as a plain page rather than being intercepted by the SDK. **Fix:** The host app needs to handle `window.open()` at the native layer: * **iOS (`WKWebView`):** Implement the `WKUIDelegate` method `webView(_:createWebViewWith:for:windowFeatures:)` to catch popup navigations and load them in a new `WKWebView` or `SFSafariViewController`. * **Android (`WebView`):** Override `WebChromeClient.onCreateWindow()` to handle new-window requests. * **React Native:** Use `react-native-inappbrowser-reborn` or open OAuth URLs via the device browser (`Linking.openURL`) rather than inside a WebView. Alternatively, if the host app cannot be modified, ask your Mesh representative about redirect-based OAuth configuration, where the callback is handled via a deep link / custom URL scheme instead of a popup postMessage. This is a common limitation of WebView environments. If your product is a mobile app, test OAuth integrations inside the actual in-app browser early — behavior can differ significantly from a standard desktop or mobile browser. ## Duplicate webhook deliveries **Symptom:** Your webhook handler receives the same event multiple times, causing duplicate processing (eg. crediting a user twice). **Root cause:** Mesh delivers webhooks with at-least-once semantics. If your server returns a non-2xx response or takes too long to respond, Mesh retries the delivery. **Fix:** Use the `EventId` field as an idempotency key. Store every processed `EventId` and skip any event you've already handled. Return `200 OK` quickly (before heavy processing), then handle the event asynchronously. **`EventId` vs `Id`**: The `Id` field changes on each delivery attempt. The `EventId` stays constant for the same logical event across all retries. Always deduplicate on `EventId`. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Common errors** Troubleshooting guide for the most common Mesh integration errors. **SDK domain not authorized** (CORS error / auth error on load): Dashboard > Account > API keys > Access → add all domains including `localhost:3000`. **SDK iframe blocked by CSP** (blank grey box): Add `*.meshconnect.com` to `frame-src` CSP directive. **Ad-blocker interfering with OAuth** (spinner, OAuth never resolves): User must disable ad-blocker or use standard browser. Brave and uBlock Origin are common culprits. **Link Token expired or already used** (error on openLink()): Fetch new token on button click (not page load). Tokens are 10 min, single-use. Never cache or reuse. **Empty catalog / symbol not supported**: Use ticker symbol (e.g. `USDC`), not ERC-20 contract address. Include multiple networkId/symbol pairs. **Webhook signature validation fails**: Header is `X-Mesh-Signature-256`. Compute HMAC-SHA256 over **raw request body bytes** before any parsing. Use `express.raw()` not `express.json()` in Node.js. **Duplicate webhook deliveries**: Use `EventId` as idempotency key. Return `200` immediately, process async. **OAuth blank screen / raw code in mobile WebView**: Implement `window.open()` handler at native layer: iOS → `WKUIDelegate`, Android → `WebChromeClient.onCreateWindow()`. Or `Linking.openURL` in React Native. **Transfer stuck pending / finalizes as failed**: CEX delay is normal (up to 24h); check `minAmount` in toAddresses; verify destination address format; sandbox `pending` webhook not guaranteed. # Concepts Source: https://docs.meshconnect.com/resources/concepts Mesh has a small set of core concepts that come up throughout the guides. This page defines each one so you can build with a clear mental model. ## Link **Link** is Mesh's hosted UX that facilitates the user journey of connecting accounts, initiating transfers, and verifying wallets. When a user "uses Mesh", they're in Link. Link is available on five client-side SDKs: web, iOS, Android, React Native, Flutter. You initialize it with a Link Token and wire up callback functions to respond to what the user does. Link handles the complex parts: OAuth flows, wallet connections, transfer previews, compliance steps, and success/error states. All you need to provide is an entry point (eg. a button) and a response to the result. ## Link Token A **Link Token** is how you start every user session. You request one from Mesh's API on your server, passing parameters that configure the experience — what transfer type the user will see, where the funds should go, any amount constraints, display currency, and so on. The token is short-lived (10 minutes) and single-use. Your server requests it, passes it to the client, and the client uses it to initialize a Link session. Everything you want to configure about a user session goes into the Link Token request. See [Fetch a Link Token](/build/fetch-link-token) for use-case-specific examples. ## Integrations **Integrations** are the exchanges (Coinbase, Binance, etc.) and wallets (MetaMask, Phantom, etc.) that Mesh connects to. When users connect their account inside Link, they're authenticating with one of these integrations. Mesh handles the authentication flows, OAuth connections, and API calls to these integrations on your behalf. You don't need to build any direct integrations yourself. ## Mesh Managed Tokens (MMT) When a user connects an exchange account in Link, Mesh can persist that authorization for future sessions. **Mesh Managed Tokens (MMT)** is the system that stores and refreshes those tokens on your behalf. The key concept: you store a `tokenId` (a stable identifier Mesh gives you) and pass it back to the SDK on subsequent sessions. If the token is still valid, the user skips re-authentication entirely and can complete a transfer in one or two taps. Mesh handles all token refresh logic. Even if an underlying access token expires, the `tokenId` you store never changes — no backend updates required on your part. See [Supercharge return-users](/build/return-users) for implementation details. ## SmartFunding **SmartFunding** is Mesh's intelligent transfer-routing capability. When a user has balances of multiple assets, SmartFunding automatically identifies the best source to fulfill a transfer — factoring in available balance, network compatibility, fees, and speed. Clients with SmartFunding capabilities can offer users a more streamlined experience, where Mesh handles the "which account do I send from?" decision intelligently in the background. ## refundAddress Mesh returns the `refundAddress` field in both the managed transfers endpoint ([/api/v1/transfers/managed/mesh](https://docs.meshconnect.com/api-reference/managed-transfers/get-transfers-initiated-by-mesh)) as well as the transfer status webhook payload (see [Transfer status webhooks](/resources/webhooks) guide for more info). This field can be used for reverse money movement in payment flows, or withdrawals in deposit flows. This address can be used to send the specified asset on the specified network back to the user. For wallet transfers, this is the same as where the original incoming transfer originates from. But for exchange transfers, it is a user-specific deposit address within their exchange account (exchanges send onchain from omnibus hot wallets, so the originating address for the original incoming transfer cannot be used for refunds). ## Sub-clients **Sub-clients** are child accounts under your main Mesh account. They're used in B2B or PSP (payment service provider) contexts where you're managing integrations on behalf of multiple downstream merchants or business customers. Each sub-client gets its own API credentials, branding configuration, compliance settings, and data isolation. You can create and manage sub-clients programmatically via the Mesh API. If you're building a single product for your own users, you don't need sub-clients. See [Managing sub-clients](/resources/sub-clients) for more details. ## Sandbox Mesh's **sandbox environment** is a full replica of production for testing purposes, using a separate base URL: `https://sandbox-integration-api.meshconnect.com` In sandbox, you use pre-configured test accounts to simulate real exchange connections and transfers. No real assets move. Sandbox API keys are separate from production keys and are managed in the same dashboard section. See [Sandbox & Testnets](/resources/sandbox-testnets) for test credentials and wallet testing instructions. ## Webhooks **Webhooks** are server-to-server event notifications Mesh sends to a URL you register. The most important event is `transfer.succeeded` — which confirms that a transfer has actually been posted to the blockchain or exchange, as opposed to just being initiated. Because exchanges can take anywhere from seconds to days to post a transaction, webhooks are how you reliably know when to credit a user's account or release inventory. You should always consume webhooks rather than relying solely on the `onTransferFinished` SDK callback for transfer confirmation. Register your webhook callback URL in the Mesh dashboard under **Account > API keys > Webhooks**. ## Transfer types Mesh supports several transfer types, each configuring a different user journey inside Link: * **Deposit** — the user sends crypto from their exchange or wallet to your platform's address. The most common use case. * **Payment** — the user sends a specific crypto amount to your payment wallet. Similar to deposit, but with a fixed `amount` per destination address. * **Onramp** — the user purchases crypto with fiat (via Binance Connect or other onramp providers) and sends it to your address. * **Withdrawal** — the user connects their account so you can retrieve their deposit addresses for use with Mesh's managed transfer API. * **Verify** — the user signs a message with their self-custody wallet to prove ownership of a specific address. The transfer type is set in the Link Token request via the `transferType` parameter. Transfer types can be combined — for example, you can require wallet verification as part of a deposit or payment flow. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Concepts** Glossary of core Mesh terms used across all guides. **Link**: Mesh's hosted UX (iframe / native view) for connecting accounts, initiating transfers, verifying wallets. Available on 5 SDKs: Web, iOS, Android, React Native, Flutter. **Link Token**: Short-lived (10 min), single-use token requested server-side via `POST /api/v1/linktoken`. Configures the entire user session. Every session starts with one. **Integrations**: Exchanges (Coinbase, Binance, etc.) and wallets (MetaMask, Phantom, etc.) that Mesh connects to. Mesh handles all auth flows. **Mesh Managed Tokens (MMT)**: Mesh stores and refreshes OAuth tokens on your behalf. You store a stable `tokenId`. User skips re-auth on return visits. `tokenId` never changes even if underlying access token rotates. **SmartFunding**: Auto-selects best source asset for a transfer based on available balance, network compatibility, fees, and speed. **refundAddress**: Address funds can be returned to. For exchange transfers, this is a user deposit address within the exchange (not the originating on-chain address, which is an omnibus hot wallet). Returned in webhooks and `GET /api/v1/transfers/managed/mesh`. **Sub-clients**: Child Mesh accounts for B2B/PSP use cases. Each gets independent branding, API credentials, and compliance settings. **Webhooks**: Server-to-server event notifications from Mesh. Use `EventId` as idempotency key (stable across retries; `Id` changes per delivery). At-least-once delivery. Register at Dashboard > Account > API keys > Webhooks. **Transfer types**: deposit | payment | onramp — set via `transferType` in Link Token request | withdrawal — no `transferType` needed (user just connects account; no `transferOptions` required) | verify — uses `verifyWalletOptions`, not `transferType`. Types can be combined (e.g. verify + deposit). # Fiat currency support Source: https://docs.meshconnect.com/resources/fiat-currency By the end of this guide, you'll know how to configure Link to display fiat amounts in your users' preferred currency using the `displayFiatCurrency` parameter. **Before you start** * You have the Mesh SDK initialized and launching (see [Launch the Mesh SDK](/build/launch-sdk)) ## Overview Users see fiat-equivalent values in many places throughout Mesh's transfer flows (configuring a transfer, previewing it, etc.). It defaults to USD, but if your app shows fiat values in another currency, it can be jarring for that currency to switch mid-session. You can use the `displayFiatCurrency` parameter in the Mesh SDK to configure this. ## Implementation ### 1. Initialize Link with `displayFiatCurrency` When you initialize Link in your application, use the `displayFiatCurrency` parameter to specify the desired fiat currency behavior. ```javascript theme={null} const connection = createLink({ ... displayFiatCurrency: 'USD', ... }) ``` ```swift theme={null} let settings = LinkSettings(displayFiatCurrency: "USD", ...) ``` ```kotlin theme={null} val configuration = LinkConfiguration( ... displayFiatCurrency = "USD", ... ) ``` ```javascript theme={null} return <... settings={{ displayFiatCurrency: 'USD', ... }} ``` ```dart theme={null} final result = await MeshSdk.show( context, configuration: MeshConfiguration( displayFiatCurrency: 'USD', ... ), ``` ### 2. Test your implementation Thoroughly test your implementation to ensure a seamless experience for your users: * Verify that Link displays correctly with the fiat currencies you intend to support. ## UI behavior All fiat amounts in Mesh Link will display in the specified currency.
## Supported currencies If you need a currency that isn't listed below, reach out to your Mesh representative and we'll get it added. | Status | Symbol | Currency | | -------------------- | ------ | ------------------ | | live | USD | US Dollar | | live | EUR | Euro | | live | GBP | British Pound | | backlog | CNY | Chinese Yuan | | backlog | JPY | Japanese Yen | | backlog | CHF | Swiss Franc | | backlog | CAD | Canadian Dollar | | backlog | AUD | Australian Dollar | | backlog | KRW | South Korean Won | | backlog | HKD | Hong Kong Dollar | | backlog | INR | Indian Rupee | | backlog | BRL | Brazilian Real | | backlog | MXN | Mexican Peso | | backlog | SGD | Singapore Dollar | | backlog | TRY | Turkish Lira | | backlog | SAR | Saudi Riyal | | backlog | AED | UAE Dirham | | backlog | ZAR | South African Rand | | backlog | THB | Thai Baht | ## What's next Both `displayFiatCurrency` and `language` are configured in the same SDK initialization call. See [Multi-language support](/resources/multi-language) for the companion guide on configuring display language, or [Launch the Mesh SDK](/build/launch-sdk) for the full SDK initialization reference across all five platforms. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Fiat currency support** Configure the `displayFiatCurrency` parameter to display fiat amounts in the user's preferred currency throughout Link. **Set in SDK initialization** (all 5 platforms, same parameter name): `displayFiatCurrency: 'USD'` **Values**: ISO 4217 currency code. Defaults to `'USD'` if not specified. **Currently live**: USD (US Dollar) | EUR (Euro) | GBP (British Pound) **Backlog (not yet live)**: CNY, JPY, CHF, CAD, AUD, KRW, HKD, INR, BRL, MXN, SGD, TRY, SAR, AED, ZAR, THB **Note**: Affects all fiat-equivalent display values throughout Link (transfer config, preview, etc.). Does not affect which currencies users can transact in — only the display conversion. `displayFiatCurrency` and `language` are set in the same SDK initialization call. # How it all fits together Source: https://docs.meshconnect.com/resources/how-it-fits By the end of this page, you'll have a clear mental model of how the Mesh components — Link Token, SDK, callbacks, and webhooks — connect in a complete integration. Ready to build? Jump straight to the [15-minute quickstart](/build/15min-quickstart) or start from [Prepare to build](/build/prepare-to-build). Come back here if you want a map of how the pieces fit. ## The big picture Every Mesh integration involves three parties: your **server**, your **client app**, and **Mesh**. Here's the full flow from button click to confirmed transfer. Image ## Step by step ### Step 1: Request a Link Token (server-side) Every Mesh session starts on your server. When a user triggers a Mesh interaction — clicking a "Deposit" or "Pay" button — your server makes a `POST /api/v1/linktoken` request to Mesh. This request configures the entire user session: transfer type (deposit, payment, onramp), destination address, amount constraints, supported networks and assets, display currency, and more. The Link Token encodes all of this configuration without exposing it to the client. Link Tokens are short-lived (10 minutes) and single-use. Request a new one for each user session. → [Fetch a Link Token](/build/fetch-link-token) for use-case-specific examples and the full parameter reference. ### Step 2: Initialize and launch the SDK (client-side) Your client receives the Link Token (passed through your own API), initializes the Mesh SDK with it, and launches Link. From this point, Mesh handles everything the user sees: the integration catalog, authentication flows, transfer preview, compliance steps, and success/error states. Link is available for five platforms — Web, iOS, Android, React Native, Flutter — each with a platform-specific initialization pattern. → [Launch the Mesh SDK](/build/launch-sdk) for all five platforms, including overlay, embedded, and Paylinks options. ### Step 3: Handle SDK callbacks (client-side) When the user takes a significant action inside Link, the SDK fires callback functions in your app. The four callbacks: * **`onIntegrationConnected`** — fires when the user successfully connects their exchange or wallet. The payload includes an `accessToken` (with a `tokenId`) that you should store server-side to enable one-tap return-user flows. * **`onTransferFinished`** — fires when a transfer has been submitted. Contains `status`, `transferId`, `txHash`, and transfer details. Use this to update your UI and trigger any immediate client-side logic. * **`onExit`** — fires whenever Link closes, regardless of outcome. Check the `page` field to detect whether the user finished the flow. * **`onEvent`** — fires for granular user interactions throughout Link. Useful for analytics and specific UX logic.
⚠️
`onTransferFinished` fires when a transfer request is acknowledged by the provider — not when it's confirmed onchain. For final on-chain or exchange confirmation, always use webhooks (Step 4). A transfer that fires `onTransferFinished` with `status: pending` may still fail.
→ [Use Mesh's callback functions](/build/callbacks) for platform-specific setup and full payload details. → [Mesh SDK events](/resources/sdk-events) for the complete event reference. ### Step 4: Verify the outcome with webhooks (server-side) `onTransferFinished` tells you a user's transfer request was acknowledged by the provider. Webhooks tell you what actually happened onchain. After a transfer is submitted, Mesh sends POST requests to your registered webhook URL as the transfer status changes: first `pending`, then either `succeeded` or `failed`. Use the `succeeded` event — not the SDK callback — to credit a user's account, release inventory, or trigger downstream business logic. **Webhooks are server-to-server.** They arrive at your registered URL independently of whether the user's browser or app is still open. Always rely on the `succeeded` webhook for final transfer confirmation. Mesh signs each webhook with HMAC-SHA256 — always verify the signature before processing the payload. Webhooks may be retried and delivered more than once; use `EventId` as your idempotency key. → [Transfer status webhooks](/resources/webhooks) for full setup including HMAC verification and the complete payload reference. ## Return users: skipping re-authentication Once a user has connected their account, they can skip re-authentication on future visits using **Mesh Managed Tokens (MMT)**. When `onIntegrationConnected` fires, save the `tokenId` from the `accessToken` payload on your server. On the user's next visit, pass the `accessToken` object to the SDK. The user can then complete a transfer in one or two taps with no re-login. Mesh handles all token refresh logic. The `tokenId` you store never changes, even if the underlying access token expires. → [Supercharge return-users](/build/return-users) for implementation details across all five SDK platforms. ## What's next * [15-minute quickstart](/build/15min-quickstart) — build a working end-to-end integration in 5 steps * [Prepare to build](/build/prepare-to-build) — get your Mesh dashboard account, API keys, and SDK installed * [Concepts](/resources/concepts) — definitions for every core term used across the 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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — How it all fits together** Architecture overview of a complete Mesh integration. 3-party model: your server ↔ Mesh API, your client ↔ Mesh SDK (Link), Mesh → your webhook endpoint. **4-step integration flow**: 1. **Server → Mesh**: `POST /api/v1/linktoken` → returns `linkToken`. Configures the full user session (transfer type, destination, amounts, display options, etc.). 2. **Client → SDK**: Pass `linkToken` to `createLink()` / `LinkConfiguration` / `MeshConfiguration`. Call `openLink()`. Mesh handles all UX from this point. 3. **SDK → Client callbacks**: `onIntegrationConnected` (save `tokenId` for MMT) | `onTransferFinished` (update UI) | `onExit` (check `page` field) | `onEvent` (analytics) 4. **Mesh → Server webhooks**: `pending` → `succeeded` or `failed`. Use `succeeded` to credit users / release inventory. Always verify HMAC-SHA256 signature. **Critical distinction**: `onTransferFinished` fires when the provider acknowledges the request, not on-chain confirmation. Always use the `transfer.succeeded` webhook for final confirmation. **Return users (MMT)**: Store `tokenId` from `onIntegrationConnected`. Pass as `accessTokens` on next session → user skips re-auth entirely. **Platforms**: Web, iOS, Android, React Native, Flutter — all use the same Link Token; initialization pattern differs per SDK. # Manual deposits Source: https://docs.meshconnect.com/resources/manual-deposits By the end of this guide, you'll understand how Mesh's manual deposit flow works — when it's used, how attribution is applied, and when it may not be appropriate for your use case. ## Overview Mesh's flagship product is the Direct Connection experience. That flow allows users to securely connect their exchange accounts or wallets to your app, enabling seamless, programmatic transfers. When Mesh is connected with the source of funds: * Mesh can read account details (balances, addresses, transaction history) * Transfers are initiated programmatically * Mesh can help prevent user misconfiguration * Connected accounts can be remembered for a return-user experience However, direct connection doesn't always fit with or work for every user. So to ensure broader coverage and higher conversion, Mesh also supports Manual Deposits through a traditional address copy/paste and QR code scan experience that supports advanced functionality like SmartFunding, and still enables you to receive webhook status updates. ## What are manual deposits? Manual Deposits support the traditional crypto transfer flow: 1. The user is shown a QR code and destination address. 2. The token and network are clearly specified. 3. Any required instructions (e.g., memo/tag, minimum deposit amount) are displayed. 4. The user initiates the transfer directly from their wallet or exchange. 5. Mesh monitors the blockchain for the transfer. 6. Once detected, Mesh sends a webhook event confirming the deposit. Just like deposits via Direct Connections, the only possible destinations are those provided by you at the start of the user session via the `linkToken` request. When Mesh detects a valid onchain transfer matching the provided address, token, and network, we send a webhook with `status: succeeded`, indicating a successful deposit. Manual deposits will not emit a `status: pending` webhook event first. Manual deposits also support SmartFunding capabilities like bridging. ## When is manual used? Manual Deposits appear in two ways: ### 1. A primary alternative to Connected Accounts Mesh supports a landing page experience where users can choose their preferred deposit method: * Connect account (Direct) * Deposit manually (QR code) This gives users flexibility and can improve overall conversion by meeting different comfort levels. ### 2. A fallback flow (enabled by default) If a user cannot complete a Connected Account deposit, Mesh automatically presents the Manual Deposit option. For example: * Their exchange or wallet is not supported * They cannot find their integration in the catalog * They choose to exit the direct flow * They run into terminal errors during direct flow * They get their password or 2FA wrong multiple times in the direct flow This backup flow is enabled by default for all deposit sessions. ## How attribution works For Connected Account deposits, attribution is straightforward: Mesh receives confirmation from the connected account when the transfer is initiated. But manual deposits require a different approach. When a user views a Mesh-generated QR code, we monitor for onchain activity and apply the following attribution logic: A transfer is attributed when: * The correct token and network are sent to the exact destination address (including tag/memo, if applicable) * The transfer occurs within 15 minutes (\<30 mins for BTC) of the user viewing the Mesh QR code * There is no existing successful or pending connected account deposit for that session If these conditions are met, Mesh attributes the transfer as a successful Mesh QR code deposit and sends the corresponding webhook event. This attribution window ensures reliable detection while maintaining a clear and consistent standard across clients. ## When Manual Deposits May Not Be Appropriate Manual Deposits are not recommended in the following scenarios: ### 1. Non-Unique Deposit Addresses If you are not providing Mesh with unique wallet addresses per user session, Manual Deposits may not be suitable. An exception would be networks like XLM or XRP that use memo/tag identifiers (which Mesh supports and displays to users). ### 2. Source-of-Funds Verification Requirements If your compliance framework requires verification of the source of funds, Manual Deposits may not meet those requirements, as funds are sent externally without account-level connectivity. ### 3. Exact Payment Amount Requirements If your use case requires receiving a precise amount (e.g., invoice or payment flows), Manual Deposits may introduce variability that needs additional handling. ### 4. Preference Not to Support Manual Transfers Manual Deposits are currently enabled by default. If your business model requires restricting transfers to direct-only flows, reach out to your Mesh representative. ## What's next For a deeper understanding of how Mesh filters which integrations your users can see, see [Intelligent catalog filtering](/resources/catalog-filtering). *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Manual deposits** Mesh's QR code / copy-paste fallback deposit flow. Enabled by default. Supports SmartFunding and bridging. **How it works**: User shown QR code + destination address + token/network + memo/tag if applicable. User initiates transfer from their own wallet. Mesh monitors blockchain. On detection → `status: succeeded` webhook. **Manual deposits emit**: Only `status: succeeded` webhook (no `pending` event first). **Attribution logic**: Transfer attributed when correct token + network + address received within **15 minutes** of user viewing the QR code, and no concurrent connected-account deposit for that session. **Appears in two scenarios**: 1. Primary option on a landing page alongside "Connect account" (configurable) 2. Automatic fallback when direct connection fails or is unavailable **Not appropriate for**: * Non-unique deposit addresses (unless using memo/tag for XRP, XLM, etc.) * Source-of-funds compliance requirements * Exact payment amount requirements * Clients that must restrict to direct-only flows (contact Mesh to disable) # Multi-language support Source: https://docs.meshconnect.com/resources/multi-language By the end of this guide, you'll know how to configure Link to display in your users' preferred language — automatically matching their device locale or specifying a particular language. **Before you start** * You have the Mesh SDK initialized and launching (see [Launch the Mesh SDK](/build/launch-sdk)) ## Overview This guide explains how to configure Link (the Mesh SDK) to support multiple languages for a localized user experience. The `language` parameter allows you to automatically match the user's device/browser language, or specify a particular language and locale. ## Possible Values * ``: A specific language code enumerated as the 2 digit language identifier (eg. "`fr`" for French) and the 2 digit region identifier (eg. "`CA`" for Canada), combined as "`fr-CA`". Alternatively, the SDK will accept an input of only the language (eg. "`fr`"). * If the indicated language (eg. "`fr`") is followed by a region code (eg. "`CA`") that is not recognized or supported for that language, Link will fall back to the default translation for that language, if available (eg. "`fr-FR`"). If no translation for the language is available, Link will default to "`en-US`" (English, US). * If you do not provide a value for the parameter, or if you provide a value for a language that is not supported, Link will default to "`en-US`" (English, US). * `system`: Link will detect the default language on the user's browser and/or device and display Link in that language. If it is an unsupported value, it will fallback to another locale for that language, or it will fallback to the global default of `en-US`. ## Implementation ### 1. Initialize Link with `language` When you initialize Link in your application, use the `language` parameter to specify the desired language behavior. ```javascript theme={null} const connection = createLink({ ... language: 'system', ... }) ``` ```swift theme={null} let settings = LinkSettings(language: "system", ...) ``` ```kotlin theme={null} val configuration = LinkConfiguration( ... language = "system", ... ) ``` ```javascript theme={null} return <... settings={{ language: 'system', ... }} ``` ```dart theme={null} final result = await MeshSdk.show( context, configuration: MeshConfiguration( language: 'system', ... ), ``` ### 2. Test your implementation Thoroughly test your implementation to ensure a seamless experience for your users: * Verify that Link displays correctly in the languages you intend to support. * Please let your Mesh representative know if you spot any incorrectly translated words or phrases, or any layout issues (for example with right-to-left or character-based languages). ## Currently supported languages If you need a language that isn't listed below, reach out to your Mesh representative and we'll get it added. | Status | Language | Region | Locale code | | -------------------- | ------------------------------ | ------------------------------ | ------------ | | live | system | | **`system`** | | live | English | United States (global default) | **`en-US`** | | live | Chinese/Mandarin (Simplified) | China (zh default) | **`zh-CN`** | | live | Finnish | Finland | **`fi-FI`** | | live | French | France (fr default) | **`fr-FR`** | | live | German | Germany | **`de-DE`** | | live | Hindi | India | **`hi-IN`** | | live | Indonesian | Indonesia | **`id-ID`** | | live | Japanese | Japan | **`ja-JP`** | | live | Malay | Malaysia | **`ms-MY`** | | live | Polish | Poland | **`pl-PL`** | | live | Portuguese | Portugal (pt default) | **`pt-PT`** | | live | Russian | Russia | **`ru-RU`** | | live | Spanish | United States (es default) | **`es-US`** | | live | Thai | Thailand | **`th-TH`** | | live | Turkish | Turkey | **`tr-TR`** | | live | Ukrainian | The Ukraine | **`uk-UA`** | | live | Uzbek | Uzbekistan | **`uz-UZ`** | | live | Vietnamese | Vietnam | **`vi-VN`** | | backlog | Arabic | Egypt | **`ar-EG`** | | backlog | Chinese/Mandarin (Traditional) | United States | **`zh-US`** | | backlog | Chinese | Hong Kong | **`zh-HK`** | | backlog | Chinese | Taiwan | **`zh-TW`** | | backlog | Czech | Czech Republic | **`cs-CZ`** | | backlog | Danish | Denmark | **`da-DK`** | | backlog | Dutch, Flemish | Belgium | **`nl-NL`** | | backlog | English | Australia | **`en-AU`** | | backlog | English | India | **`en-IN`** | | backlog | English | United Kingdom | **`en-GB`** | | backlog | French | Canada | **`fr-CA`** | | backlog | Greek, Modern (1453–) | Greece | **`el-GR`** | | backlog | Hebrew | Israel | **`he-IL`** | | backlog | Hungarian | Hungary | **`hu-HU`** | | backlog | Italian | Italy | **`it-IT`** | | backlog | Korean | South Korea | **`ko-KR`** | | backlog | Norwegian | Norway | **`no-NO`** | | backlog | Portuguese | Brazil | **`pt-BR`** | | backlog | Slovak | Slovakia | **`sk-SK`** | | backlog | Spanish, Castilian | Spain | **`es-ES`** | | backlog | Swedish | Sweden | **`sv-SE`** | ## What's next Similar to language, you can configure the currency that fiat amounts display in. See [Fiat currency support](/resources/fiat-currency) for details. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Multi-language support** Configure the `language` parameter to display Link in the user's preferred language. **Set in SDK initialization** (all 5 platforms, same parameter name): `language: 'system'` **Values**: `'system'` (auto-detects device/browser locale) | BCP 47 code (e.g. `'en-US'`, `'fr'`, `'fr-CA'`, `'zh-CN'`) **Fallback chain**: unknown region → language default (e.g. fr-FR for any French variant) → `en-US`. Unsupported language → `en-US`. **Currently live**: en-US, zh-CN, fi-FI, fr-FR, de-DE, hi-IN, id-ID, ja-JP, ms-MY, pl-PL, pt-PT, ru-RU, es-US, th-TH, tr-TR, uk-UA, uz-UZ, vi-VN **Backlog (not yet live)**: ar-EG, zh-HK, zh-TW, cs-CZ, da-DK, nl-NL, en-AU, en-GB, en-IN, fr-CA, el-GR, he-IL, hu-HU, it-IT, ko-KR, no-NO, pt-BR, sk-SK, es-ES, sv-SE **Note**: `language` and `displayFiatCurrency` are set in the same SDK initialization call. See Fiat currency support guide. # Sandbox & Testnets Source: https://docs.meshconnect.com/resources/sandbox-testnets By the end of this guide, you'll be set up to test your full Mesh integration against sandbox exchange accounts and wallet testnets — without touching real funds. **Before you start** * You have a Mesh dashboard account with a sandbox API key (see [Prepare to build](/build/prepare-to-build)) ## Overview The Mesh sandbox is a fully functional replica of production — same API, same flows, same SDK — but using simulated exchange data and testnets so you can build and test your integration without touching real funds. All sandbox API calls should go to: `https://sandbox-integration-api.meshconnect.com` ## Testing exchange (CEX) flows The sandbox includes simulated exchange accounts that behave like their production counterparts, complete with real-time market pricing and stateful portfolios. A set of pre-built test accounts covers a range of scenarios out of the box. ### Test credentials All test accounts share the same password and MFA code: * **Password:** `Pass123` * **MFA / OTP code:** `123456` But the username differs depending on the scenarios you'd like to test. | Username | Portfolio | USD Balance | Best for | | ---------- | ---------------------------------------------------- | ----------- | -------------------------------- | | `Mesh` | Full (BTC, ETH, BNB, USDC, USDT, SOL, XRP, and more) | \$10M | General testing | | `Mesh2` | Empty | \$0 | Empty state / zero balance flows | | `Mesh3` | No crypto, cash only | \$10M | Buy / onramp flows | | `Mesh4` | Full | \$100M | Large transaction testing | | `MeshBTC` | BTC only | \$0 | BTC-specific flows | | `MeshUSDC` | USDC only | \$0 | USDC-specific flows | | `MeshUSDT` | USDT only | \$0 | USDT-specific flows | | `MeshETH` | ETH only | \$0 | ETH-specific flows | | `MeshSOL` | SOL only | \$0 | SOL-specific flows | ## Testing wallet flows For self-custody wallet testing, the sandbox connects to real testnets. Transactions are real on-chain events — just on test networks, not mainnet. | Wallet | Testnet | Test token | Instructions | | -------- | --------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | MetaMask | Sepolia (Ethereum testnet)
Base Sepolia | `SEPOLIAETH`
`SEPOLIAETH` | | | Phantom | Sepolia (Ethereum testnet)
Base Sepolia
Solana Devnet | `SEPOLIAETH`
`SEPOLIAETH`
`DEVNETSOL` | Your wallet must be in testnet mode:
1. Go to Settings > Developer Settings
2. Turn on Testnet mode | ### Getting testnet tokens You'll need testnet tokens in your wallet before you can initiate a transfer. Two ways to get them: 1. **From a public faucet** — e.g. the [Ethereum Sepolia Faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) for Sepolia ETH. 2. **From Mesh** — reach out to your Mesh representative to request tokens directly. ## Limitations A few things to keep in mind as you test: * **Exchanges:** Coinbase and a legacy Binance integration are currently the only exchanges available in sandbox. Other production exchanges are not currently replicated. * **Mesh Managed Tokens (MMT):** Not supported in sandbox. * **Wallets:** Only MetaMask (Sepolia) and Phantom (Sepolia & Solana Devnet) are currently supported for testnet flows. * **Pending webhooks:** The `pending` webhook event is not guaranteed in sandbox — transfers may go directly to `succeeded` or `failed`. Don't build logic that depends on receiving `pending` during testing. * **Testnet expansion:** Additional testnet support is on the roadmap. Reach out to your Mesh representative if you have specific testnet needs. ## What's next Once you're happy with how everything behaves in sandbox, see [Prepare for go-live](/build/go-live) to set up your production credentials and flip the switch. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Sandbox & Testnets** Full production replica for testing. Uses simulated exchange accounts and real blockchain testnets. No real assets move. **Sandbox base URL**: `https://sandbox-integration-api.meshconnect.com` **CEX test accounts** (all use password `Pass123`, MFA/OTP `123456`): * `Mesh`: Full portfolio (BTC, ETH, BNB, USDC, USDT, SOL, XRP+), \$10M USD → general testing * `Mesh2`: Empty portfolio, \$0 → empty state / zero balance testing * `Mesh3`: No crypto, \$10M USD → buy/onramp testing * `Mesh4`: Full portfolio, \$100M USD → large transaction testing * `MeshBTC` / `MeshUSDC` / `MeshUSDT` / `MeshETH` / `MeshSOL`: Single-asset portfolios **Wallet testnets**: * MetaMask: Sepolia ETH (`SEPOLIAETH`), Base Sepolia (`SEPOLIAETH`) * Phantom: Sepolia ETH, Base Sepolia, Solana Devnet (`DEVNETSOL`). Enable testnet: Settings > Developer Settings > Testnet mode. * Get testnet tokens: Ethereum Sepolia Faucet, or ask your Mesh representative. **Sandbox limitations**: Only Coinbase + legacy Binance available. MMT not supported. `pending` webhook not guaranteed. Only MetaMask + Phantom for wallet flows. # Mesh SDK events Source: https://docs.meshconnect.com/resources/sdk-events This reference guide lists every SDK event Mesh can fire during a Link session, along with payload details — so you can build precise business logic around user interactions. **Before you start** * You have the Mesh SDK initialized and have set up callback functions (see [Use Mesh's callback functions](/build/callbacks)) ## Overview Mesh SDKs offer event tracking that not only provides you insights into user interactions within Link, but also gives you the ability to implement business logic based on those interactions. ## SDK Callback Functions Capturing Mesh's SDK events varies slightly across platforms (Web, iOS, Android, React Native, Flutter). Please refer to the [Use Mesh's callback functions](/build/callbacks) guide for more platform-specific guidance for setting up callback functions. ## Core events These are the events most integrations act on. Wire these up first — then consult the complete reference below if you need finer-grained tracking. | SDK Event | When it fires | Payload highlights | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`pageLoaded`** | The first page of Link is fully loaded and visible to the user. | No payload. Use to confirm Link launched successfully. | | **`integrationConnected`** | User successfully connected their exchange or wallet. Triggers the `onIntegrationConnected` callback. | • **`integrationName`**: The connected integration
• **`accessToken`**: Token payload with credentials — store `tokenId` server-side for MMT return-user flows | | **`transferPreviewed`** | User viewed the preview screen showing transfer details, fees, and routing. | • **`symbol`**, **`amount`**, **`amountInFiat`**, **`toAddress`**
• **`fees`**: `institutionTransferFee`, `estimatedNetworkGasFee`, `customClientFee`
• **`previewId`**: Unique ID for this preview | | **`transferInitiated`** | User clicked Proceed from the preview — the transfer request was sent to the exchange or wallet for approval. | • **`symbol`**, **`amount`**, **`toAddress`**, **`fees`** (same structure as `transferPreviewed`) | | **`transferCompleted`** | User reached the Success page at the end of the transfer flow. Triggers the `onTransferFinished` callback. **Use `onTransferFinished` for business logic** — this event is better suited for analytics. | Full `transferFinished` payload: **`status`** (pending/succeeded/failed), **`transferId`**, **`txHash`**, **`symbol`**, **`amount`**, **`fromAddress`**, **`toAddress`**, **`refundAddress`**, **`networkName`** | | **`transferExecutionError`** | An error occurred while executing the transfer. | • **`errorMessage`**: Descriptive error message. | | **`transferNoEligibleAssets`** | The user has no assets eligible for this transfer (wrong token, insufficient balance, or unsupported network). | • **`arrayOfTokensHeld`**: Tokens the user holds, each with `symbol`, `amount`, `amountInFiat`, and `ineligibilityReason` | | **`close`** | User dismissed the Mesh Link modal. Triggers the `onExit` callback. | • **`page`**: The page the user was on when they exited. **`'transferExecutedPage'`** means the transfer completed before exit. | ## Complete event reference | SDK Event Type | Description of Occurrence | Payload Details | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`pageLoaded`** | Triggered when the first page is fully loaded. The first page the user sees may differ based on use case. | No additional payload. | | **`methodSelected`** | Triggered on `HomePage` when the user selects a particular method for their flow. | • **`method`**: The selected method type ('embedded' / 'manual' / 'buy') | | **`close`** | Triggered when the user exits the Mesh Link modal. | • **`page`**: the page the user was on when they exited.
• Note: In the context of a transfer flow, **`page: 'transferExecutedPage'`** would indicate the full flow was successful because the user exited from the Success page. If the page is anything else, the flow wasn't successfully completed. | | **`integrationSelected`** | Triggered when a user selects an integration from the catalog list. | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`userSearched?`**: true/false if the user searched selected this integration from search results or not. | | **`legalTermsViewed`** | Triggered if a user views the terms of use page in Link. | No additional payload. | | **`credentialsEntered`** | Triggered when a user submits exchange login credentials. | No additional payload. | | **`integrationMfaRequired`** | Triggered when the user is prompted to enter MFA in an exchange authentication flow. | No additional payload. | | **`integrationMfaEntered`** | Triggered when the user enters their MFA code in an exchange authentication flow. | No additional payload. | | **`integrationOAuthStarted`** | Triggered when an exchange's OAuth window is launched in authentication flow. | No additional payload. | | **`integrationAccountSelectionRequired`** | Triggered if user is prompted to select a specific account within linked exchange. | No additional payload. | | **`integrationConnected`** | Triggered when a user successfully connects to an integration. | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`accessToken`** payload: The access token to the user account and relevant metadata about the integration | | **`integrationConnectionError`** | Triggered when there is an error in connecting to an integration. | • **`errorMessage`**: Descriptive error message. | | **`transferStarted`** | Triggered when the user begins the transfer flow. This means they have successfully connected an account and have moved on to either configuring or previewing their transfer. It does not mean they have initiated a transfer of assets. | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration. | | **`transferAssetSelected`** | Triggered when the user selects an asset to transfer. This does not relate to assets used for funding operations. This is about the asset being transferred. | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`symbol`**: Currency symbol | | **`transferNetworkSelected`** | Triggered when the user selects a network to transfer on. | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`symbol`**: Currency symbol
• **`networkId`**: Selected network identifier
• **`networkName`**: Selected network name | | **`transferAmountEntered`** | Triggered when the user enters an amount to transfer. | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`symbol`**: Currency symbol
• **`networkId`**: Selected network identifier
• **`networkName`**: Selected network name
• **`amount`**: Amount the user enters for transfer | | **`transferNoEligibleAssets`** | Triggered when there are no assets in the user's account eligible for the transfer, or Mesh cannot use the assets in the account to fund the transfer. | • **`arrayOfTokensHeld`**: A list of tokens held in the user's account
• **`symbol`**: Currency symbol
• **`amount`**: Amount of holding
• **`amountInFiat`**: Amount of holding in fiat
• **`ineligibilityReason`**: Why the token is ineligible.
• **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`noAssetsType`**: | | **`transferConfigureError`** | This may happen if the session linkToken expires during the configuration flow of a transfer. Very rare. | • **`errorMessage`**: Descriptive error message.
• **`requestId`**: | | **`transferPreviewed`** | Triggered when a user previews the details of a pending transfer. | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`symbol`**: Currency symbol
• **`networkId`**: Selected network identifier
• **`networkName`**: Selected network name
• **`amount`**: Crypto amount of the transfer
• **`amountInFiat`** (optional): Amount in fiat currency
• **`fiatCurrency`**: fiat currency symbol for the **`amountInFiat`** value.
• **`toAddress`**: Destination address
• **`fees`**:
• **`institutionTransferFee`**
• **`estimatedNetworkGasFee`**
• **`customClientFee`**
• **`fiatPurchaseStrategy`**: an enumeration of a fiat funding option used to fund this transaction plus any applicable **`tradingFee`**.
• **`cryptocurrencyFundingOptionType`**: an enumeration of any funding options used to fund this transaction plus any applicable **`cryptocurrencyConversionFee`** or **`depositFee`**.
• **`previewId`**: Unique ID for the preview | | **`transferPreviewError`** | Triggered when there is an error in building a transfer preview. | • **`errorMessage`**: Descriptive error message. | | **`linkTransferQRGenerated`** | Triggered when the user lands on the Manual QR screen. | • **`token`** (optional): Token symbol for the transfer
• **`network`** (optional): Network being used
• **`toAddress`** (optional): Destination address
• **`qrUrl`** (optional): Generated QR code URL | | **`fundingOptionsViewed`** | Triggered when the user views the funding options page. | No additional payload. | | **`fundingOptionsUpdated`** | Triggered when the user makes updates to the selected funding options and clicks save (ie. saves a new funding strategy). | No additional payload. When a user saves a new funding strategy, it would build a new Preview, which would fire a new **`transferPreviewed`** event with new funding options enumerated. | | **`transferInitiated`** | Triggered when the user clicks to Proceed from the Preview screen. This will send a request to transfer to the exchange or wallet, which the user then needs to approve (via 2FA for exchange, or signing for wallet). | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`symbol`**: Currency symbol
• **`networkId`**: Selected network identifier
• **`networkName`**: Selected network name
• **`amount`**: Crypto amount of the transfer
• **`amountInFiat`** (optional): Amount in fiat currency
• **`fiatCurrency`**: fiat currency symbol for the **`amountInFiat`** value.
• **`toAddress`**: Destination address
• **`fees`**:
• **`institutionTransferFee`**
• **`estimatedNetworkGasFee`**
• **`customClientFee`**
• **`fiatPurchaseStrategy`**: an enumeration of a fiat funding option used to fund this transaction plus any applicable **`tradingFee`**.
• **`cryptocurrencyFundingOptionType`**: an enumeration of any funding options used to fund this transaction plus any applicable **`cryptocurrencyConversionFee`** or **`depositFee`**. | | **`executeFundingStep`** | Triggered when there is a success or error on a funding operation before a transfer. | • **`fundingOptionType`**: The operation that has completed or failed.
• **`status`**: Outcome of the funding operation.
• **`errorMessage`**: Descriptive error message. | | **`gasIncreaseWarning`** | Triggered after funding operations and before initiation of transfer if the cost of gas has gone higher than the buffered estimate shown to the user on the Preview page. | No additional payload. | | **`transferMfaRequired`** | Triggered when the user is prompted to enter an MFA code to perform an exchange transfer. | No additional payload. | | **`transferMfaEntered`** | Triggered when the user submits their MFA code to perform an exchange transfer. | No additional payload. | | **`transferKycRequired`** | Triggered when the user has not completed KYC in the linked account, is prompted to do so before being able to transfer assets. | No additional payload. | | **`transferExecuted`** | Do not use. Obsolete. | Do not use. Obsolete. | | **`transferCompleted`** | Triggered when the user views the Success page at the conclusion of the transfer flow. We recommend using the **`onTransferFinished()`** callback function to handle the experience when a user returns to your app after a successful transfer. | **`transferFinished`** payload:
• **`status`**: pending / succeeded / failed
• **`txId`**: A unique client identifier
• **`transferId`**: A unique Mesh identifier
• **`txHash?`**: A unique blockchain identifier
• **`fromAddress`**: Address transfer is sent from
• **`toAddress`**: Address transfer is sent to
• **`symbol`**: Symbol of asset being transferred
• **`amount`**: Amount being transferred
• **`amountInFiat`**: Fiat equivalent of transfer amount
• **`totalAmountInFiat`**: Total amount transferred, including transfer-related fees
• **`networkId`**: Selected network identifier
• **`networkName`**: Selected network name
• **`refundAddress`**: The address that the user can receive back to | | **`transferExecutionError`** | Triggered when there is an error in executing a transfer. | • **`errorMessage`**: Descriptive error message. | | **`seeWhatHappenedClicked`** | Triggered when the user clicks the See what happened link on the Transfer Success screen | No additional payload. | | **`connectionUnavailable`** | Triggered when a timeout occurs when attempting to open a wallet on mobile, most likely because the DeFi wallet app is not installed on the device. | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`reason`**: string | | **`connectionDeclined`** | Triggered when the user rejects a connection request in their wallet, or some error causes an auto-rejection. | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`reason`**: string.
• **`networkId`**: Selected network identifier
• **`networkName`**: Selected network name
• **`toAddress`**: Address transfer is sent to.
• **`errorMessage`**: Descriptive error message. | | **`transferDeclined`** | Triggered when the user rejects the transfer request in their wallet, or some error causes an auto-rejection. | • **`integrationType`**: For exchanges, this is the same as the name. For wallets, this is 'deFiWallet'.
• **`integrationName`**: Name of the selected integration.
• **`reason`**: string
• **`networkId`**: Selected network identifier
• **`networkName`**: Selected network name
• **`toAddress`**: Address transfer is sent to
• **`symbol`**: Symbol of asset being transferred
• **`amount`**: Amount being transferred
• **`status`**: pending / succeeded / failed | | **`walletMessageSigned`** | Triggered when a user signs a message in their wallet to verify ownership. | • **`address`**: wallet address that signed the message
• **`isVerified`**: true / false indicator of whether the user has signed the exact message sent for signature from this address
• **`message`**: Message that was signed
• **`signedMessageHash`**: The hash of the message signed by the wallet
• **`timeStamp`**: Time stamp of when the signature happened | | **`verifyDonePage`** | Triggered when the user views the Success page after successfully completing the wallet verification flow. | No additional payload. | | **`verifyWalletRejected`** | Triggered when the user rejects the message signature request in their wallet, or some error causes an auto-rejection. | No additional payload. | | **`done`** | Triggered when the user exits Link after successfully completing a read-only account connection flow (ie. for Mesh Verify) or after successfully completing the wallet verification flow (ie. for Mesh Verify). | • **`page`**: the page the user was on when they exited.
• Note: In wallet verification (ie. Mesh Verify) flow, **`page: verifyDonePage`** would indicate successful completion of the flow. And in a read-only (ie Mesh Portfolio) flow, **`page: integrationConnectedPage`** would indicate successful completion of the flow. | | **`registerTransferError`** | Shown when we are unable to receive the transfer hash from a self-custody wallet at the conclusion of a transfer flow. | • **`errorMessage`**: Descriptive error message. | ## What's next For platform-specific setup instructions for consuming these events with callback functions, see [Use Mesh's callback functions](/build/callbacks). *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Mesh SDK events** Complete reference of all SDK events fired via the `onEvent` callback during a Link session. **Core events to wire first**: * `pageLoaded`: Link launched successfully. No payload. * `integrationConnected`: User connected account. Payload: `integrationType`, `integrationName`, `accessToken` (has `tokenId`). * `transferPreviewed`: User viewed preview. Payload: `symbol`, `amount`, `amountInFiat`, `toAddress`, `fees` (institutionTransferFee, estimatedNetworkGasFee, customClientFee), `previewId`. * `transferInitiated`: User clicked Proceed. Same payload structure as `transferPreviewed`. * `transferCompleted`: User reached Success page. Triggers `onTransferFinished`. Use `onTransferFinished` for business logic; use this event for analytics. * `transferNoEligibleAssets`: No matching assets. Payload: `arrayOfTokensHeld` (each has `symbol`, `amount`, `amountInFiat`, `ineligibilityReason`). * `close`: User dismissed Link. Payload: `page`. If `page === 'transferExecutedPage'` → full flow completed. **Wallet verification events**: `walletMessageSigned` (payload: `signedMessageHash`, `message`, `address`, `timeStamp`, `isVerified`) | `verifyWalletRejected` | `verifyDonePage` **Other notable events**: `integrationSelected` | `transferDeclined` | `connectionDeclined` | `transferExecutionError` | `linkTransferQRGenerated` (manual QR deposit) | `executeFundingStep` | `done` (read-only or verify flow complete, payload has `page`) **Platform note**: `onEvent` setup varies by platform — see Use Mesh's callback functions guide for platform-specific initialization. # Managing sub-clients Source: https://docs.meshconnect.com/resources/sub-clients By the end of this guide, you'll have sub-clients registered for your downstream merchants, with each user session applying the right per-merchant branding and compliance settings. **Before you start** * You've confirmed with your Mesh representative that sub-clients are the right approach for your use case * Sub-client functionality has been enabled on your account ## Overview If Mesh is part of a product you offer to other businesses, sub-clients are how you represent each of your clients individually to Mesh. Each sub-client gets its own branding and compliance record, so the end-users of each merchant see an experience tailored to that merchant's product rather than yours. This guide covers what sub-clients are, how to register them (via dashboard or API), and how to apply them to individual user sessions. For a concise introduction to the sub-client model, see [Concepts](/resources/concepts). ## What are Sub-Clients? If your product is used directly by end-users, you can skip this guide. However, if your product is embedded within other platforms (e.g., you are a Payment Service Provider powering payments for multiple retailers), then sub-clients are critical. A Sub-Client represents the specific merchant or platform where your product is embedded, and where Mesh will be used. Registering them allows you to: * **Ensure Consistent Branding:** Display the specific merchant's name and logo in the Mesh Link modal, creating a seamless experience for the end-user. * **Maintain Compliance:** Mesh requires visibility into the legal entity associated with each transaction for compliance purposes. ## How to Register a Sub-Client First, reach out to your Mesh representative to enable sub-client functionality for your account. Then, you can register and manage sub-clients in two ways: 1. Manually, via the Mesh developer dashboard 2. Programmatically, via Mesh's Account Management API endpoints ### Option 1: Manually, via the Mesh developer dashboard 1. Navigate to **Account** > **Link Configuration** > **Clients** tab. 2. Click **"Add a client"**. 3. Enter the Business Legal Name, Display Name, Callback URL(s), and upload the relevant icon. 4. Click **Save**. ### Option 2: Via the Account Management API For high-volume or automated setups, you can use a collection of Account Management API endpoints ([see here](https://docs.meshconnect.com/api-reference/account-management/registered-clients/add-new-registered-client)) to create, read, update, and delete sub-clients. To use this API, you must first generate a dedicated API key in the developer dashboard and configure security settings. 1. **Generate an API Key:** * Go to **Account** > **API Keys > Account management API keys**. * Click to generate a new key. * Use this key to authenticate your requests to the **`https://admin-api.meshconnect.com`** endpoints. 2. **Configure IP Whitelisting (Security Requirement):** * Just like calls to any production Mesh endpoint, access to these endpoints are restricted to pre-approved IP addresses. * Go to **Account > API keys > Access** and add the specific IP address ranges from which your servers will make API calls. ## How to use a sub-client in a user session Once a sub-client is registered (via Dashboard or API), you will receive a unique `subClientId`. To apply the sub-client's branding and settings to a transaction, you will pass this ID in your Link Token request as shown below (payment example): ```bash theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "subClientId": "SUB_CLIENT_ID", // Replace (the unique identifier for this registered sub-client) "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "transferOptions": { "transactionId": "UNIQUE_TRANSACTION_ID", // Replace "transferType": "payment", "isInclusiveFeeEnabled": false, "generatePayLink": false, "toAddresses": [ // Replace (example array below) { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana "symbol": "USDC", // Replace "address": "xxx", // Replace "amount": 99.99 // Replace }, { "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611", // Ethereum "symbol": "USDC", // Replace "address": "xxx", // Replace "amount": 99.99 // Replace }, { "networkId": "aa883b03-120d-477c-a588-37c2afd3ca71", // Base "symbol": "USDC", // Replace "address": "xxx", // Replace "amount": 99.99 // Replace }, { "networkId": "03dee5da-7398-428f-9ec2-ab41bcb271da", // Bitcoin "symbol": "BTC", // Replace "address": "xxx", // Replace "amount": 0.00141, // Replace "displayAmountInFiat": 99.99 // Replace } ] } } ' ``` ## Test your implementation Before going live, verify that sub-client branding and session routing are working end-to-end. **What to test:** * **Branding**: Launch a Link session using a Link Token that includes your `subClientId`. Confirm that Link displays the sub-client's logo and display name — not your top-level account's branding. * **Fallback**: Launch a session *without* a `subClientId`. Confirm that the default top-level branding appears correctly. * **API path** *(if applicable)*: Create a sub-client via the Account Management API and confirm you receive a valid `subClientId` before using it in a Link Token request. * **Sandbox first**: Register a sub-client under your sandbox account and test with your sandbox credentials (`https://sandbox-integration-api.meshconnect.com`). Sandbox and production sub-client registrations are separate. **If sub-client branding isn't appearing:** Double-check that the `subClientId` in your Link Token request exactly matches the ID returned when the sub-client was created. An incorrect or missing ID silently falls back to top-level branding. ## What's next With sub-clients registered and `subClientId` flowing through your Link Token requests, the natural next steps are customizing the experience further and controlling which assets each merchant's users can access: * [Polish the experience](/build/polish) — customize themes, embed vs. overlay behavior, and other presentation options across your sub-clients *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Managing sub-clients** Register and manage sub-clients (child Mesh accounts) for B2B/PSP contexts where you serve multiple downstream merchants. **Register via Dashboard**: Account > Link Configuration > Clients > Add a client. Fields: Business Legal Name, Display Name, Callback URL(s), icon upload. **Register via API**: POST to Account Management API at `https://admin-api.meshconnect.com`. Requires separate Account Management API key (Dashboard: Account > API keys > Account management API keys) and IP allowlisting. **Apply to user session**: Pass `subClientId` in Link Token request body. Result: Link displays sub-client branding instead of top-level account branding. **Testing**: Register sandbox sub-clients separately from production. Use `https://sandbox-integration-api.meshconnect.com` for sandbox sessions. **Fallback**: Omitting `subClientId` or using incorrect value silently falls back to top-level branding — no error thrown. **Use case**: Required for PSPs/payment platforms serving multiple merchants. Single-product direct-to-consumer apps do not need sub-clients. **Link Token with `subClientId` — canonical request body** (payment example): ```bash theme={null} curl --request POST \ --url https://sandbox-integration-api.meshconnect.com/api/v1/linktoken \ // This is pointing to sandbox --header 'Content-Type: application/json' \ --header 'X-Client-Id: YOUR_CLIENT_ID' \ // Replace --header 'X-Client-Secret: YOUR_API_KEY' \ // Replace --data ' { "subClientId": "SUB_CLIENT_ID", // Replace (the unique identifier for this registered sub-client) "userId": "UNIQUE_USER_ID", // Replace "restrictMultipleAccounts": true, "transferOptions": { "transactionId": "UNIQUE_TRANSACTION_ID", // Replace "transferType": "payment", "isInclusiveFeeEnabled": false, "generatePayLink": false, "toAddresses": [ // Replace (example array below) { "networkId": "0291810a-5947-424d-9a59-e88bb33e999d", // Solana "symbol": "USDC", // Replace "address": "xxx", // Replace "amount": 99.99 // Replace }, { "networkId": "e3c7fdd8-b1fc-4e51-85ae-bb276e075611", // Ethereum "symbol": "USDC", // Replace "address": "xxx", // Replace "amount": 99.99 // Replace }, { "networkId": "aa883b03-120d-477c-a588-37c2afd3ca71", // Base "symbol": "USDC", // Replace "address": "xxx", // Replace "amount": 99.99 // Replace }, { "networkId": "03dee5da-7398-428f-9ec2-ab41bcb271da", // Bitcoin "symbol": "BTC", // Replace "address": "xxx", // Replace "amount": 0.00141, // Replace "displayAmountInFiat": 99.99 // Replace } ] } } ' ``` # Network and token support Source: https://docs.meshconnect.com/resources/supported-tokens A reference of every blockchain network, token, exchange, and wallet currently supported by Mesh — plus the bridging routes available for cross-network transfers. ## At a glance * **24** networks * **18** stablecoins * **225** wallet integrations * **7** major exchange integrations ## Supported networks Mesh supports 24 blockchain networks across three categories: EVM-compatible chains with full wallet support, non-EVM chains, and exchange-only networks. ### EVM networks (wallet + exchange) These networks support direct wallet connections and transfers via MetaMask, Phantom, Trust Wallet, and other EVM-compatible wallets. | Network | Tokens | Available assets | | --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Ethereum** | 41 | AAVE, APE, ARB, BLUR, BUSD, CAKE, CRO, DAI, DEXE, ENA, ETH, FDUSD, FTM, GRT, IMX, INJ, LDO, LEO, LINK, MANA, MATIC, MKR, MNT, PAXG, PYUSD, QNT, RLUSD, RNDR, SAND, SHIB, SNX, TUSD, UNI, USD1, USDC, USDD, USDP, USDT, VIRTUAL, WBTC, WETH, USDG, EURC, WLFI | | **Polygon** | 6 | POL, USDC, USDT, DAI, MATIC, WMATIC | | **BSC** | 10 | BNB, USDC, USDT, CAKE, FDUSD, WBNB, LTC, DEXE, FORM, USD1, WLFI | | **Arbitrum** | 7 | ETH, USDC, USDT, DAI, ARB, PYUSD, WETH | | **Optimism** | 7 | ETH, USDC, USDT, DAI, OP, SNX, WETH | | **Base** | 4 | ETH, USDC, VIRTUAL, WETH, EURC | | **Avalanche C** | 5 | AVAX, USDC, USDT, DAI, WAVAX, EURC | | **Blast** | 3 | BLAST, ETH, WETH | | **Hyper EVM** | 5 | HYPE, USDC, USDH, USD₮0, WHYPE | | **MegaETH** | 3 | ETH, USDm, USD₮0 | | **Monad EVM** | 5 | MON, USDC, USDT, USD1, WMON | | **WorldChain** | 2 | WLD, ETH, USDC | | **Sei** | 2 | SEI, USDC | | **Tempo** | 4 | USDC, ETH, PATHUSD, USD₮0 | | **Ton** | 2 | TON, USDT | | **Tron** | 3 | TRX, USDC, USDT | | **Ink** | 2 | USDC, ETH | ### Non-EVM networks (wallet + exchange) These networks use their own wallet ecosystems (e.g. Phantom, Solflare, and other Solana-compatible wallets). | Network | Tokens | Available assets | | ---------- | ------ | ---------------------------------------------------------------------------- | | **Solana** | 12 | SOL, USDC, USDT, FARTCOIN, WIF, PENGU, PYUSD, FDUSD, TRUMP, USDG, EURC, CASH | ### Exchange-only networks These networks are supported through exchange integrations (Coinbase, Binance, etc.) but do not currently support direct web3 wallet connections. Users can transfer assets on these networks via their exchange accounts. | Network | Tokens | Available assets | | --------------- | ------ | ---------------- | | **Bitcoin** | 1 | BTC | | **Litecoin** | 1 | LTC | | **Dogecoin** | 1 | DOGE | | **Ripple** | 1 | XRP | | **Aptos** | 3 | APT, USDC, USDT | | **Cardano** | 1 | ADA | | **Stellar** | 2 | XLM, USDC, EURC | | **Sui** | 3 | SUI, USDC, FDUSD | | **Sonic** | 1 | S, EURC | | **Injective** | 1 | INJ | | **Avalanche X** | 1 | AVAX | ### Test networks (developer resources) Use these testnets in the Mesh Sandbox to develop and test your integration before going live. | Wallet | Testnet | Native token | Stablecoins | Get tokens | | -------- | ------------- | ------------ | ----------- | ---------------------------------------------------------- | | MetaMask | Sepolia | SEPOLIAETH | PYUSD, USDG | [sepoliafaucet.com](https://sepoliafaucet.com) | | MetaMask | Base Sepolia | SEPOLIAETH | USDC, EURC | [sepoliafaucet.com](https://sepoliafaucet.com) | | Rainbow | Sepolia | SEPOLIAETH | PYUSD, USDG | [sepolia-faucet.pk910.de](https://sepolia-faucet.pk910.de) | | Phantom | Solana Devnet | DEVNETSOL | PYUSD, USDG | [faucet.solana.com](https://faucet.solana.com) | ## Stablecoin support Mesh supports 18 stablecoins across multiple networks. USDC and USDT have the broadest coverage. | Stablecoin | Networks | Available on | | ----------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **USDC** | 18 + testnet | Ethereum, Polygon, BSC, Arbitrum, Optimism, Base, Avalanche, Solana, Tron, Aptos, Stellar, Sui, Hyper EVM, Monad EVM, Sei, WorldChain, Ink, Tempo (+ Base Sepolia) | | **USDT** | 13 | Ethereum, Polygon, BSC, Arbitrum, Optimism, Avalanche C, Solana, Tron, Aptos, Sui, TON, Monad, MegaETH | | **EURC** | 6 + testnet | Ethereum, Base, Avalanche C-Chain, Solana, Stellar, Sonic (+ Base Sepolia) | | **DAI** | 5 | Ethereum, Polygon, Arbitrum, Optimism, Avalanche | | **FDUSD** | 4 | Ethereum, BSC, Solana, Sui | | **PYUSD** | 3 + testnets | Ethereum, Arbitrum, Solana (+ Sepolia, Solana Devnet) | | **USD1** | 3 | Ethereum, BSC, Monad | | **USDG** | 2 + testnets | Ethereum, Solana (+ Sepolia, Solana Devnet) | | **USD₮0** | 3 | MegaETH, Hyper EVM, Tempo | | **CASH** | 1 | Solana | | **TUSD** | 1 | Ethereum | | **USDm** | 1 | MegaETH | | **USDP** | 1 | Ethereum | | **USDD** | 1 | Ethereum | | **USDH** | 1 | Hyper EVM | | **RLUSD** | 1 | Ethereum | | **BUSD** | 1 | Ethereum | | **PATHUSD** | 1 | Tempo | ## Exchange integrations Mesh integrates with major exchanges, enabling users to connect their exchange accounts for transfers and payments. | Exchange | Networks | Tokens | Coverage | | ------------------------------------- | -------- | ------ | --------- | | **Coinbase** | 17 | 45+ | US + EMEA | | **Binance** | 19 | 67+ | Global | | **Bitfinex** ⁎ | 12 | 33 | Global | | **Paribu** | 11 | 42 | Global | | **Robinhood** | 18 | 26 | US | | **Uphold** | 18 | 89 | Global | | **Bybit Pay** *(in testing)* | TBD | TBD | Global | | **Crypto.com Pay** *(in development)* | TBD | TBD | Global | | **Krak Pay** *(in development)* | TBD | TBD | US | | **Kraken** *(planning)* | TBD | TBD | US | | **OKX** *(planning)* | TBD | TBD | Global | ⁎ Bitfinex is currently limited to \$1,000 transfers. ## Supported wallets Mesh supports 225 wallets across EVM and non-EVM ecosystems. ### Most popular wallets These wallets represent the majority of user connections and transfer volumes: * MetaMask * Trust Wallet * Phantom * Exodus * Ledger * Trezor * Rabby * Crypto.com Onchain * Solflare * OKX Wallet * Coinbase (Base) Wallet * Binance Wallet * Tangem * SafePal * Uniswap * Bitget Wallet * Edge Wallet * Rainbow * Zerion * ZenGo ### Extended wallet list (206 wallets) 1inch Wallet, 3S Wallet, A4 Wallet, Abra Wallet, AbsoluteWallet, Aktionariat, Alice, AlphaWallet, Ambire Wallet, Ancrypto Wallet, Anybit, ApolloX, ArchiPage, Argent, Assure, AT.Wallet, ATON, Atomic, Authereum, Avacus, Ballet Crypto, BC Vault, BCERTin wallet, Bee Wallet, Bifrost Wallet, Bitcoin.com Wallet, BitKeep, BitPay, Bitpie, Bitizen, Bitski, BlockBank, Blockchain.com, Boba Multisig, bobablocks, Brave Wallet, Bridge Wallet, Bull App, Bycoin, ByteBank, Card Wallet, Celo Wallet, Chainge Finance, cmorq, Coin98, CoinCircle, Coingrig, Coinhub, Coinomi, CoinStats, CoolWallet, Core, Cosmostation, Crossmint, Cryptnox Wallet, Ctrl Wallet, CYBAVO Wallet, CypherD Wallet, D'CENT Wallet, Defiant, Dentacoin Wallet, DID Wallet, Dok Wallet, Dohrnii Wallet, DopamineApp, Dracula Metaverse, Earth Wallet, EASY, Eidoo, Elastos Essentials, Ellipal, Enno Wallet, f(x) Wallet, FILWallet, FirstWallet, Flooz, Freedom Wallet, Frontier, fuse.cash, GameStop Wallet, Go Pocket, GridPlus, Gryfyn, Guarda Wallet, HashKey Me, helix id, Hippo Wallet, Holdstation Wallet, HUMBL WALLET, HyperPay, imToken, Infinity Wallet, ioPay, ISLAMIwallet, iToken Wallet, Jade Wallet, JulWallet, Keeper, Keplr, KEYRING PRO, Keywallet Touch, Klever Wallet, Krystal, KryptoGO Wallet, KyberSwap, LATOKEN Multichain DeFi Wallet, Linen, LOBSTR Wallet, LocalTrade Wallet, Locker Token, Loopring Wallet, LZ Wallet, Marble, MathWallet, MDAO Wallet, MEW wallet, Midas Wallet, Minerva Wallet, Monarch Wallet, MyWalliD, Nash, NEFTiPEDiA, Nitrogen Wallet, O3Wallet, Obvious Wallet, Okse Wallet, Omni, OneKey, ONTO, Open Wallet, Opera Crypto Browser, Opto Wallet, Orange, Ownbit, Oxalus Wallet, Paper Wallet, ParaSwap Wallet, Payperless, PayTube, PEAKDEFI Wallet, Pera Wallet, pier, Pillar, Pitaka, Plasma Wallet, PlasmaPay, PREMA Wallet, Qubic Wallet, QuiverX, RealT Wallet, RiceWallet, Robinhood Wallet, RWallet, S-ONE Wallet, Safe, SafeMoon, Safematrix, SahalWallet, SecuX, Sequence Wallet, Shinobi Wallet, SimpleHold, Slavi Wallet, Snowball, SoCap Wallet, SparkPoint, Spatium, Spot, StarBase, Status, StrikeX Wallet, Talk+, Talken Wallet, TAP Wallet, The Parallel, THORWallet, TokenPocket, Tokenary, Tongue Wallet, Tonkeeper, Torus, TronLink, Trustee Wallet, UniPass, Unstoppable Domains, Unstoppable Wallet, UPBOND Wallet, UvToken, Valora, Verso, ViaWallet, Vision, Vision: Crypto Wallet, Volt: DeFi, Wallet 3, wallet.io, WallETH, WATT ME, Wirex Wallet, Xcapit, XDEFI Wallet, XFUN Wallet, XinFin XDC Network, yiToken, ZelCore, Zelus ## Bridging support | Source network | Supported symbols | Target networks | Embedded | Manual QR | | -------------- | --------------------------- | --------------- | -------- | --------- | | Arbitrum | ETH, USD1, USDC, USDT | Base, Ethereum | Yes | Yes | | Avalanche C | USDC | Base, Ethereum | Yes | Yes | | BSC | BNB, USD1, USDC, USDT, WLFI | Base, Ethereum | Yes | Yes | | Base | ETH, USDC | Ethereum | Yes | Yes | | Bitcoin | BTC | Base, Ethereum | No | Yes | | Blast | ETH | Base | Yes | Yes | | Ethereum | ETH, SHIB, WBTC, WLFI | Base | Yes | Yes | | HyperEVM | USDC, USDH | Base, Ethereum | Yes | Yes | | MegaETH | ETH | Ethereum | No | Yes | | Monad | ETH, MON, USD1, USDC, USDT | Base, Ethereum | Yes | Yes | | Optimism | ETH, OP, USD1, USDC, USDT | Base, Ethereum | Yes | Yes | | Polygon | ETH, USD1, USDC, USDT | Base, Ethereum | Yes | Yes | | Sei | SEI, USDC | Base, Ethereum | Yes | No | | Solana | SOL, USDC, USDT | Base, Ethereum | Yes | Yes | | Tempo | USDC | Base, Ethereum | Yes | Yes | | Tron | USDT | Base, Ethereum | No | Yes | | WorldChain | ETH, USDC | Base, Ethereum | Yes | Yes | *Last updated: April 25, 2026* # Transfer status webhooks Source: https://docs.meshconnect.com/resources/webhooks By the end of this guide, you'll have a working webhook endpoint that receives and verifies Mesh transfer status events — so you can reliably know when a transfer succeeds, fails, or is pending. **Before you start** * You have a Mesh account with a sandbox API key (see [Prepare to build](/build/prepare-to-build)) * You have a server with an endpoint that can receive POST requests from the public internet ## Overview If your business relies on transfer status to make business decisions (releasing inventory, allocating funds, etc.), then polling Mesh's managed transfers history endpoint ([/api/v1/transfers/managed/mesh](https://docs.meshconnect.com/api-reference/managed-transfers/get-transfers-initiated-by-mesh)) is an inefficient and ineffective solution. Mesh offers webhooks to solve this problem. A webhook is a callback function that allows lightweight, event-driven communication between two systems. In this case, the events are updates to transfer statuses. For an overview of how Mesh uses webhooks and how they differ from the `onTransferFinished` SDK callback, see [Concepts](/resources/concepts). ## Listen for Mesh webhook events ### 1. Create and register your callback URI * Create an endpoint that can receive a POST request with application/json content. * In the Mesh developer dashboard, go to **Account > API keys > Webhooks**. * Register a Callback URI for the appropriate environment. Image **Notes:** * When registering an endpoint, you'll be prompted to store your secret key, as you won't be able to view it again. * You can only save one production URI and one sandbox URI, but you can deactivate one and save a new one at any time. ### 2. Whitelist Mesh's IP * All webhook events will come from this static IP: `20.22.113.37` ### 3. Verify the signature Mesh uses HMAC-SHA256 (Hash-based Message Authentication Code) to sign each webhook event. When you register your Webhook URI, you receive a secret key from Mesh. Mesh uses this key to sign each outgoing request and includes the result in the `X-Mesh-Signature-256` header. You verify the signature on your end using the same secret to confirm the payload is authentic and hasn't been tampered with. Compute an HMAC-SHA256 over the **raw request body** using your webhook secret, Base64-encode the result, and compare it to the value in the `X-Mesh-Signature-256` header.
⚠️
Use the raw request body bytes — not parsed/re-serialized JSON. Parsing the body first then re-serializing it before computing the HMAC silently changes the byte sequence: JSON parsers can reorder keys, normalize decimal precision, or strip whitespace — producing a different result than what Mesh signed. In Node.js: use express.raw() instead of express.json().
```csharp theme={null} public string GenerateHmacSignature(string payload, string webhookSecret) { using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookSecret)); byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)); return Convert.ToBase64String(hash); } ``` ```javascript theme={null} app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => { const rawBody = req.body; const receivedSig = req.headers['x-mesh-signature-256']; const computedSig = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(rawBody) .digest('base64'); if (computedSig !== receivedSig) return res.status(400).send('Invalid signature'); res.sendStatus(200); }); ``` ```python theme={null} raw_body = request.get_data() received_sig = request.headers.get("X-Mesh-Signature-256") computed_sig = base64.b64encode( hmac.new(WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256).digest() ).decode() if computed_sig != received_sig: return "Invalid signature", 400 ``` **Critical: always use the raw request body.** The HMAC is computed over the exact byte sequence of the HTTP request body — as received, before any parsing. A common mistake is parsing the JSON first and then re-serializing it before computing the HMAC. This silently changes the byte sequence: JSON parsers normalize decimal precision, reorder keys, or strip whitespace — producing a different result than what Mesh signed, and your verification will fail. **Never:** * Parse and re-serialize the JSON body before computing the HMAC * Round or normalize decimal values (`SourceAmount`, `DestinationAmount`, etc.) * Use `express.json()` middleware in Node.js without separately capturing the raw body (use `express.raw()` instead) **Debugging tip:** If your signatures aren't matching, log these three things side-by-side: the raw request body as a string, the `X-Mesh-Signature-256` header value, and your locally computed HMAC. Comparing them directly will quickly surface where the divergence is. ### 4. Respond to Mesh webhook events * Respond with a `200` response in \< 200ms to confirm receipt of the event. * **Process the event asynchronously.** Return `200` immediately, then handle your business logic in a background process. If your handler does work before returning `200`, you risk exceeding the 200ms timeout. * **Mesh does not guarantee exactly-once delivery.** Webhooks may be retried and delivered more than once — for example, if your server times out or returns a non-200 response. Design your handler to be idempotent: retries carry the same `EventId` but a new `Id` — use `EventId` as your idempotency key to safely deduplicate. ## Webhook Event Model The webhook payload contains the core information related to a transfer update, and also includes additional fields specific to the webhook event. ### Webhook call data
EventId Guid
A unique identifier for the event. Stays the same across all retries — use this as your idempotency key to deduplicate.
Id Guid
A unique identifier for each individual webhook delivery attempt (`SentID`). A new `Id` is generated on every retry for the same event.
SentTimestamp long
Unix timestamp (seconds) indicating when the webhook was dispatched.
### Transfer data
TransferId Guid
Mesh's unique identifier for the transfer.
Timestamp long
Unix timestamp (seconds) indicating when the transfer event occurred.
TransferStatus string
Transfer status at the time of this event. See Transfer Status Values below.
TransactionId string
The `transactionId` you provided in the Link Token request. Use this to match webhook events to transactions in your own system.
TxHash string
The blockchain transaction hash. Present on `succeeded` events; absent on `failed`.
UserId string
The `userId` you provided in the Link Token request.
Token string
Symbol of the transferred token (e.g. `USDC`, `ETH`).
Chain string
Name of the network the transfer occurred on (e.g. `Ethereum`, `Solana`).
SourceAmount decimal?
Token amount that left the source account. Always compute your HMAC over the **raw bytes** of this value — never parse/re-serialize the decimal, as precision changes will break signature verification.
SourceAccountProvider string
Name of the integration used to send the token (e.g. `Binance`, `MetaMask`).
DestinationAmount decimal?
Token amount received at the destination (after fees). May differ from `SourceAmount` if bridging or conversion occurred.
DestinationAddress string
The destination wallet address where funds were sent.
SourceAddress string
The address from which the transfer was posted to the blockchain. This is not the same as RefundAddress for centralized exchange transfers.
RefundAddress string
The address funds can be returned to. Save this for refund flows and compliance. See [Add Mesh to your withdrawal flow](/extend/withdrawal).
### Transfer Status Values | Status | Description | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `pending` | The transfer has been initiated via Mesh, but has not yet reached a final state. Mesh does not yet have a Transfer Hash for this transfer. | | `succeeded` | A final state that indicates the transfer was successfully delivered to the destination address. Mesh has a Transfer Hash for this transfer. | | `failed` | A final state that indicates the transfer has failed. No transfer hash available. | ### Payload The payload format is JSON. Here is an example. ```json theme={null} { "Id": "358c6ab7-4518-416b-9266-c680fda3a8dd", "EventId": "56713e70-be74-4a37-0036-08da97f5941a", "SentTimestamp": 1720532648, "UserId": "user_id_provided_by_client", "TransactionId": "transaction_id_provided_by_client", "TransferId": "dd4063e5-f317-441c-3f07-08dc7353b6f8", "TransferStatus": "Pending", "TxHash": "0x7d4ec1ce50952a377452c95fdf5a787ff551f08c0343093f866c84f57c473495", "Chain":"Ethereum", "Token":"ETH", "DestinationAddress":"0x0Ff0000f0A0f0000F0F000000000ffFf00f0F0f0", "SourceAccountProvider" :"Binance", "SourceAmount":0.004786046226555188, "DestinationAmount":0.004786046226555188, "SourceAddress": "0x0Ff0000f0A0f0000F0F000000000ffFf00f0F0f0", "RefundAddress": "0x0Ff0000f0A0f0000F0F000000000ffFf00f0F0f0", "Timestamp": 1715175519038 } ``` ## What's next Now that your webhook endpoint is set up, see the [Polish the experience](/build/polish) guide for tips on using transfer status data to build a complete user-facing notification experience. *** *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](https://llmstxt.org/)). Human readers can safely ignore this.* **llms.txt — Transfer status webhooks** Receive, authenticate, and process Mesh transfer status events. Webhooks confirm on-chain transfer status (pending → succeeded/failed). **Setup**: (1) Register callback URI: Dashboard > Account > API keys > Webhooks. Save secret key — shown once only. (2) Whitelist Mesh IP: `20.22.113.37`. (3) Verify HMAC-SHA256 signature. **Signature verification**: Compute HMAC-SHA256 over **raw request body bytes** (never parse/re-serialize first). Base64-encode the result. Compare to `X-Mesh-Signature-256` header value. **Node.js/Express — HMAC verification pattern**: ```javascript theme={null} app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => { const rawBody = req.body; const receivedSig = req.headers['x-mesh-signature-256']; const computedSig = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(rawBody) .digest('base64'); if (computedSig !== receivedSig) return res.status(400).send('Invalid signature'); res.sendStatus(200); }); ``` **C# — HMAC-SHA256 verification**: ```csharp theme={null} public string GenerateHmacSignature(string payload, string webhookSecret) { using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookSecret)); byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)); return Convert.ToBase64String(hash); } ``` **Python (Flask) — HMAC-SHA256 verification**: ```python theme={null} raw_body = request.get_data() received_sig = request.headers.get("X-Mesh-Signature-256") computed_sig = base64.b64encode( hmac.new(WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256).digest() ).decode() if computed_sig != received_sig: return "Invalid signature", 400 ``` **Response**: Return `200` in \< 200ms. Process event asynchronously after responding. **Idempotency**: `EventId` is stable across retries; `Id` changes per delivery attempt. Always deduplicate on `EventId`. **Webhook call data**: `EventId` | `Id` | `SentTimestamp` **Transfer data fields**: `TransferId` | `Timestamp` | `TransferStatus` (pending/succeeded/failed) | `TransactionId` (your ID) | `TxHash` (on succeeded) | `UserId` | `Token` | `Chain` | `SourceAmount` (decimal — compute HMAC over raw bytes, never re-serialize) | `SourceAccountProvider` | `DestinationAmount` | `DestinationAddress` | `SourceAddress` (blockchain posting address; differs from `RefundAddress` for CEX transfers) | `RefundAddress` **Statuses**: `pending` (initiated, no hash) | `succeeded` (final, on-chain confirmed, hash present) | `failed` (final, no hash)