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
CrossmintEmbeddedOfframpwith the session and the user’s Crossmint wallet - Handle the component’s events
- Test a withdrawal on staging
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
- API keys: a server-side API key with the
orders.createandpayment-methods.createscopes, 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 quickstart, withbase-sepoliaas the chain. - USDC: testnet USDC in the wallet, from the Circle faucet.
.env.local. The providers from the Wallets quickstart read the client-side key:
.env.local
Create the session on your server
A session lets one user make one withdrawal from one wallet. Create it with Create Offramp Session from a server action, so your server API key never reaches the browser:
app/actions/createOfframpSession.ts
userLocatoris the user who withdraws, asuserId:<USER_ID>.payeris the wallet the deposit is sent from: itsaddress, and itschain(base,base-sepolia,polygon,polygon-amoyorsolana). It must be owned by or linked to the user.receiptEmailis where the withdrawal receipt is sent.
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.Example response (201)
Example response (201)
Render the offramp
Create a client component that creates the session and renders Render The component takes the width of its container. It is at least 650px tall plus padding, and grows when a screen needs more. To give it a fixed area instead, pass
CrossmintEmbeddedOfframp. createCrossmintWalletSigner turns the user’s Crossmint wallet into the payer that signs and sends the deposit:app/components/Withdraw.tsx
<Withdraw /> inside the Crossmint providers, with the signed-in user’s ID and email:app/page.tsx
fill and give the container a definite height. See Sizing.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:
onCompletefires with theorderIdand the deposit'stxIdonce 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 byorderId. The payout can still fail afterwards, so confirm the payout on your server.onCancelfires 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. WithoutonCancel, those buttons do nothing. Closing the view while the deposit is confirming does not stop the deposit or the payout. Keep the latestorderIdfromonStatusChangeand follow it with Manage Orders or webhooks.onErrorreports areason, whether it isretriable, and amessagefor your logs. Whenretriableisfalse, the session cannot continue, so unmount the component. When the component shows a failure screen, its Close button callsonCancel. Forinvalid-credentials, create a new session to start over.onStatusChangereports each step of the flow with the currentorderId. TheorderIdcan change within one withdrawal, for example after identity verification or when the user takes a new rate, so keep the latest one. Thefailedstatus always ends the session, whateverretriablesays.
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.Test on staging
- Fund the wallet with testnet USDC from the Circle faucet.
- Click Withdraw to bank, add a US bank account, and enter an amount. Use a bank account number that is unique to your integration.
- If the user is not verified yet, verify their identity when the component asks.
- Review the quote and confirm with Hold to confirm. The wallet signs and sends the deposit, and the status moves through
confirming-deposittocompleted, whenonCompletefires.
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 ownpayer. The component calls its signAndSendTransaction with the deposit to send:
serializedTransaction: a serialized transaction on EVM chains, or a base58 legacy transaction withfeePayerandrecentBlockhashset on Solana.chainandpayerAddress: 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.
OfframpSignatureRejectedError only when the user declines, so the component can offer the withdrawal again with a new rate:
payer.ts
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
The session request fails
The session request fails
Create Offramp Session returns a
code that names the problem:invalid-user-locator(400): the locator is notuserId:<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 theorders.createorpayment-methods.createscope. Create the session from your backend with a server API key that has both scopes. Themessagenames the missing scopes. - No
code(429): your project sent too many requests in a minute. Wait and retry.
Cancel, Continue or Close does nothing
Cancel, Continue or Close does nothing
Those buttons call
onCancel. Pass an onCancel handler that closes the component.The session has expired
The session has expired
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.
Launching in Production
- Ask Crossmint to enable Offramp, bank account payment methods and the embedded offramp for your production project (contact Crossmint).
- Create production API keys with the same scopes in the production console, and replace your staging keys.
- Create sessions at
https://www.crossmint.com/api/unstable/offramp-sessions. - 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

