# Error handling Handle errors that occur during onramp sessions. For general error handling, see the [Stripe error handling guide](https://docs.stripe.com/error-handling.md). For error codes specific to the crypto onramp, see [Crypto onramp error codes](https://docs.stripe.com/crypto/onramp/embedded-components-error-codes.md). # Web ## API errors ### Checkout errors When the [checkout API](https://docs.stripe.com/api/crypto/onramp_sessions/checkout.md) returns 200 or 202 but the purchase isn’t done, the response body includes the [CryptoOnrampSession](https://docs.stripe.com/api/crypto/onramp_sessions.md) object with `transaction_details.last_error` set. For SDK integrations, `performCheckout` handles payment next actions such as 3DS internally, so the table below only covers `last_error` values that require explicit action from your integration: | **last\_error** | **Description** | **How to handle** | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `missing_kyc` | KYC verification is required. | Have the user complete KYC with `submitKycInfo`. Then call checkout again. | | `missing_document_verification` | Identity document verification is required. | Have the user complete verification with `verifyDocuments`. Then call checkout again. | | `charged_with_expired_quote` | Quote expired. | Call [Refresh a Quote](https://docs.stripe.com/api/crypto/onramp_sessions/quote.md) API, then call checkout again. | | `quote_rate_drifted` | The exchange rate changed since the quote was locked. | Create a new session with an updated quote. | | `transaction_limit_reached` | User’s limit exceeded. | Display an error message. | | `location_not_supported` | User’s location isn’t supported. | Show that the service isn’t available in their region. | | `transaction_failed` | Generic failure. | Display a generic error message. | | `missing_consumer_wallet` | The wallet address doesn’t exist for the current user. | Have the user register the wallet, then call checkout again. | | `wallet_ownership_verification_required` | The destination wallet must complete EU Travel Rule ownership verification before a purchase at or above 1,000 EUR. | Run [wallet ownership verification](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md?platform=web#travel-rule-wallet-ownership) for the destination wallet, then call checkout again. This differs from `transaction_limit_reached`, which isn’t recoverable through verification. | The following pseudocode shows how to handle these cases in a retry loop: ```javascript async function handleCheckout() { // Step 1: Create a new session on your backend before starting checkout. let sessionId = await createSessionOnBackend(); for (let attempt = 0; attempt < 5; attempt++) { try { const result = await onramp.performCheckout(sessionId, async (onrampSessionId: string) => { // Call your back end, which calls POST /v1/crypto/onramp_sessions/{sessionId}/checkout // with the session ID, and return the client_secret from the response. const { client_secret } = await fetch(`/checkout/${onrampSessionId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }).then(r => r.json()); return client_secret; }); if (result.successful) { // Purchase complete. return; } } catch { // performCheckout threw; fall through to last_error handling below. } // Step 2: Inspect last_error from the session to decide the next action. // This runs whether performCheckout threw or returned { successful: false }. const session = await fetchSessionFromBackend(sessionId); const lastError = session.transaction_details?.last_error; if (lastError === 'missing_kyc') { // Prompt user to provide KYC info, then retry checkout on the same session. await onramp.submitKycInfo({ given_name, surname, id_number, date_of_birth, address }); } else if (lastError === 'missing_document_verification') { // Prompt user to complete identity verification, then retry on the same session. await onramp.verifyDocuments(); } else if (lastError === 'missing_consumer_wallet') { // Prompt user to register a wallet, then retry on the same session. await onramp.registerWalletAddress(walletAddress, network); } else if (lastError === 'charged_with_expired_quote') { // Quote expired. Refresh the quote on your backend, then retry checkout on the same session. await refreshQuoteOnBackend(sessionId); } else if (lastError === 'quote_rate_drifted') { // Exchange rate moved too far. Create a new session with a fresh quote. sessionId = await createSessionOnBackend(); } else if (lastError === 'wallet_ownership_verification_required') { // Verify ownership of the destination wallet, then retry checkout on the same session. try { const challenge = await onramp.getWalletOwnershipChallenge({ walletAddress, network }); const signature = await userWallet.signMessage(challenge.message); await onramp.submitWalletOwnershipSignature({ challengeId: challenge.challengeId, signature }); } catch (verificationError) { // Wallet ownership verification failed. Do not retry. showError(verificationError); return; } } else { // Terminal errors: transaction_limit_reached, location_not_supported, transaction_failed. // Do not retry. Show an appropriate error message. showError(lastError); return; } } } ``` ### Transaction blocked errors Transactions can be blocked to meet compliance or risk requirements. For compliance reasons, Stripe can’t provide additional details about why a specific transaction was blocked. If you believe a transaction was blocked incorrectly, contact Stripe. | Error code | Description | How to handle | | ----------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `crypto_onramp_transaction_blocked` | The transaction was blocked to meet compliance or risk requirements. | Show a generic error message. If you believe the transaction was blocked incorrectly, contact Stripe. | > When testing, use the [test values](https://docs.stripe.com/crypto/onramp/embedded-components-integration-guide.md?platform=android#test-values) to avoid triggering compliance or risk checks. ### Wallet ownership verification errors The `getWalletOwnershipChallenge` and `submitWalletOwnershipSignature` [EU Travel Rule](https://docs.stripe.com/crypto/onramp/eu-kyc-integration-guide.md?platform=web#travel-rule-wallet-ownership) wallet ownership methods reject with an error whose `code` is one of the following: | **code** | **Description** | **How to handle** | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | `WALLET_NOT_FOUND` | The wallet isn’t registered on the authenticated user. | Register the wallet with `registerWalletAddress`, then request a new challenge. | | `UNSUPPORTED_NETWORK` | The network doesn’t support wallet ownership verification. It only supports EVM-compatible networks and Solana. | Don’t retry with this network. | | `WALLET_OWNERSHIP_CHALLENGE_EXPIRED` | The challenge expired. | Request a new challenge, collect a new signature, then submit again. | | `INVALID_WALLET_OWNERSHIP_CHALLENGE` | The challenge is invalid or was already used. | Request a new challenge, then submit again. | | `INVALID_WALLET_OWNERSHIP_SIGNATURE` | The signature doesn’t prove control of the wallet. | Restart the full challenge and signing flow. Don’t resubmit the same signature. | These codes are consistent across the web, React Native, Android, and iOS SDKs. Other failures—such as an unauthenticated user or a rate-limited request—aren’t specific to wallet ownership verification and reject with the non-specific `GENERIC_ONRAMP_ERROR` code. Handle them with your general error handling.