Skip to main content
In this guide, you will add a withdrawal flow to an Expo app with the CrossmintEmbeddedOfframp component and run a withdrawal in the Staging environment. You will:
  • Create an offramp session on your server for the user and the wallet they withdraw from
  • Render CrossmintEmbeddedOfframp with the session and the user’s Crossmint wallet
  • Handle the component’s events
  • Test a withdrawal on staging
The component runs the whole withdrawal inside your app: the user picks, adds, renames or removes a bank account, enters an amount, verifies their identity if the order needs it, reviews the quote and approves the deposit from their wallet. Your server only creates the session.
Bank accounts in the Philippines, Indonesia, Thailand, Vietnam and Nigeria are listed in Add a bank account and can be saved, but withdrawals to them fail after identity verification for now. See Countries and Rails for the markets Offramp supports.

Prerequisites

  • Expo: Expo SDK 58 (React Native 0.88), on the iOS and Android versions it supports. A bare React Native app needs Expo modules installed.
  • API keys: a server-side API key with the orders.create and payment-methods.create scopes, and a client-side API key for the component, both from the staging console.
  • Project setup: KYC configured for your project and, for production, Offramp, bank account payment methods and the embedded offramp enabled on it. Crossmint sets these up per project; contact Crossmint to enable them. You can test the embedded offramp on staging before they are enabled for production.
  • Crossmint wallet: an EVM or Solana wallet owned by a user of your project with a userId: locator. The session refuses email and phone locators. With your own authentication, Crossmint sets the wallet’s owner to the user ID in your JWT. Set up the providers as in the Wallets React Native quickstart, with base-sepolia as the chain.
  • USDC: testnet USDC in the wallet, from the Circle faucet.
  • Camera access: identity verification captures documents with the camera. Declare android.permission.CAMERA in AndroidManifest.xml and set NSCameraUsageDescription in Info.plist; neither is added for you. In an Expo app, set them in app.json under android.permissions and ios.infoPlist.
Install the Crossmint client SDK, version 2.1.1 or later:
The SDK needs Expo SDK 58. npx expo install picks the versions of these packages that match your Expo SDK. The example App component also uses react-native-safe-area-context:
Add your client-side API key to .env.local. The providers from the Wallets quickstart read it:
.env.local
Your backend reads the server-side API key from CROSSMINT_SERVER_SIDE_API_KEY.
1

Create the session on your server

A session lets one user make one withdrawal from one wallet. Create it with Create Offramp Session on your backend, so your server API key never reaches the app. This example uses Express, but you can use any backend framework:
server/offrampSession.ts
  • userLocator is the user who withdraws, as userId:<USER_ID>.
  • payer is the wallet the deposit is sent from: its address, and its chain (base, base-sepolia, polygon, polygon-amoy or solana). It must be owned by or linked to the user.
  • receiptEmail is where the withdrawal receipt is sent.
The response carries sessionId and clientSecret, which the component takes as its credentials, and expiresAt, when the session stops accepting new withdrawals (one hour after it is created). The component keeps showing the withdrawal's status for 24 hours. Calling the endpoint again for the same user and wallet returns the same open session until it is used or expires, and that session keeps the receiptEmail it was created with: the one in the new request is ignored.
This example takes the user from the request body to stay short. In your backend, read the signed-in user’s ID and email from your own authenticated session, so no one can open a session for another user.
2

Render the offramp

Create a screen that creates the session and renders CrossmintEmbeddedOfframp. createCrossmintWalletSigner turns the user’s Crossmint wallet into the payer that signs and sends the deposit. With fill, the offramp fills the screen and scrolls inside itself:
screens/WithdrawScreen.tsx
Render the screen inside the Crossmint providers, with the signed-in user’s ID and email:
App.tsx
Without fill, the component is at least 650px tall plus padding, and grows when a screen needs more. See Sizing.
3

Handle the events

The component shows its own screens for every step and every error, so the events are for your app's state and your logs:
  • onComplete fires with the orderId and the deposit's txId once the deposit is confirmed and the payout to the bank is on its way. It can fire again for the same order when the user reopens a completed withdrawal, so dedupe by orderId. The payout can still fail afterwards, so confirm the payout on your server.
  • onCancel fires when the user closes the flow: Continue on the Withdrawal sent screen, Close on a failure screen, or Cancel when the order's bank account cannot be loaded. Unmount the component there. Without onCancel, those buttons do nothing. Closing the view while the deposit is confirming does not stop the deposit or the payout. Keep the latest orderId from onStatusChange and follow it with Manage Orders or webhooks.
  • onError reports a reason, whether it is retriable, and a message for your logs. When retriable is false, the session cannot continue, so unmount the component. When the component shows a failure screen, its Close button calls onCancel. For invalid-credentials, create a new session to start over.
  • onStatusChange reports each step of the flow with the current orderId. The orderId can change within one withdrawal, for example after identity verification or when the user takes a new rate, so keep the latest one. The failed status always ends the session, whatever retriable says.
Only those three buttons call onCancel. No other screen has a Cancel button, so give the user your own way to close the component, like the Close button in the example above.The Embedded Offramp Reference lists every status and error reason.
4

Test on staging

  1. Fund the wallet with testnet USDC from the Circle faucet.
  2. Tap Withdraw to bank, add a US bank account, and enter an amount. Use a bank account number that is unique to your integration.
  3. If the user is not verified yet, verify their identity when the component asks.
  4. Review the quote and confirm with Hold to confirm. The wallet signs and sends the deposit, and the status moves through confirming-deposit to completed, when onComplete fires.
On staging every payout is simulated and succeeds. To see a failed payout, add the US test bank account from Test a Failed Payout in the component. The SEPA one appears only when SEPA is offered (see Supported Markets).

Use an External Wallet

To withdraw from a wallet that Crossmint did not create, link it to the user with Link External Wallet to User before you create the session, and pass your own payer. The component calls its signAndSendTransaction with the deposit to send:
  • serializedTransaction: a serialized transaction on EVM chains, or a base58 legacy transaction with feePayer and recentBlockhash set on Solana.
  • chain and payerAddress: the chain and wallet the deposit was prepared for. Do not send from a wallet that does not match: check it before you render the component. See Refuse a mismatched wallet.
Sign and broadcast the transaction, then resolve with its transaction ID. Throw OfframpSignatureRejectedError only when the user declines, so the component can offer the withdrawal again with a new rate:
payer.ts
If an order needs proof that the user owns the wallet, the component stops with the wallet-ownership-required error. Link the wallet again with an ownership proof, then create a new session. For Solana deposits, see Offramp Orders on Solana.

Troubleshooting

Create Offramp Session returns a code that names the problem:
  • invalid-user-locator (400): the locator is not userId:<USER_ID>.
  • chain-not-supported (400): the chain is not supported, or the address is not valid on it.
  • payer-not-linked (400): the wallet is not owned by or linked to this user.
  • user-not-found (404): no user with this locator exists in your project.
  • kyc-not-configured (403): KYC is not configured for your project. Contact Crossmint to set it up.
  • addon-required (403): Offramp, bank account payment methods or the embedded offramp is not enabled for your production project. Contact Crossmint to enable it.
  • No code (403): the API key is missing or invalid, is not a server key, or lacks the orders.create or payment-methods.create scope. Create the session from your backend with a server API key that has both scopes. The message names the missing scopes.
  • No code (429): your project sent too many requests in a minute. Wait and retry.
Those buttons call onCancel. Pass an onCancel handler that closes the component.
A session accepts new withdrawals for one hour after it is created. A withdrawal whose deposit was never recorded also ends on this screen when the session's 24-hour status window closes. Create a new session and pass its credentials to the component.
If the offramp page fails to load in the WebView, onError reports provider-error (retriable) for a connection failure or an HTTP 5xx, and invalid-configuration (not retriable) for an HTTP 4xx. The message carries the cause. The WebView then shows an error page with no retry or Close button, and onReady does not fire. To retry a connection failure or an HTTP 5xx, remount the component, for example by changing its key. For an HTTP 4xx, fix the integration.

Launching in Production

  1. Ask Crossmint to enable Offramp, bank account payment methods and the embedded offramp for your production project (contact Crossmint).
  2. Create production API keys with the same scopes in the production console, and replace your staging keys.
  3. Create sessions at https://www.crossmint.com/api/unstable/offramp-sessions.
  4. Follow Launch in Production for the remaining go-live steps.

Learn More

Embedded Offramp Reference

Props, events, statuses and errors

Manage Orders

Status, webhooks, and failures