# Custodial vs. Non-Custodial Off-Ramp

> For the complete documentation index, see [llms.txt](https://docs.banxa.com/llms.txt). Append `.md` to any page URL for its markdown version.


For the off-ramp (sell) flow, the customer's crypto has to reach a Banxa deposit address. How that happens is the fulfilment flow. The right choice depends on whether your platform holds custody of your customers' crypto, and whether your app can sign the transfer itself.

The fulfilment flow is set on your partner account by Banxa, not per order. → See [Choosing a flow](#choosing-a-flow).

```mermaid
flowchart TD
    A([Sell order initiated]) --> B[POST /v2/sell]
    B --> C[Customer completes checkout<br/>KYC + bank account details]
    C --> D[Banxa issues a deposit address]
    D --> E{Account fulfilment flow}

    E -->|QR code / MetaMask| F[Customer sends crypto<br/>from their own wallet in checkout]
    F --> Z

    E -->|Native Wallet| H[Banxa redirects to your redirectUrl<br/>with the deposit details]
    H --> I[Your app signs and sends<br/>the transfer]
    I --> Z

    E -->|Custodial| J[Webhook fires to partner endpoint]
    J --> K[Partner calls GET /v2/orders/:id<br/>to get the deposit address]
    K --> L[Partner sends crypto, then<br/>POST /v2/orders/:id/confirm]
    L --> Z

    Z[Banxa detects the deposit] --> M([Fiat payout released to customer])
```

## Non-custodial flows

The crypto is sent from the customer's own wallet. Depending on the flow, the customer sends it from inside the Banxa checkout, or your wallet app sends it after Banxa hands back the deposit details.

| Flow | Best for | What the customer sees |
|  --- | --- | --- |
| [QR code](#qr-code) | Any external wallet | QR code plus deposit details |
| [MetaMask](#metamask) | Web dApps on EVM chains | Connect wallet button |
| [Native Wallet](#native-wallet-return-to-wallet) | Wallet apps that sign the transfer themselves | Redirected straight back to your app |
| [WebView bridge](#webview-bridge) | Mobile apps embedding checkout in a WebView | Your app receives the deposit details via `postMessage` |


HashPack (Hedera) and zkSync wallet flows are also available on request.

If no fulfilment flow is configured for your account, the customer sees error `50422` at the deposit step.

**Best for:**

- Self-custody wallets where the customer controls their private keys.
- Consumer wallet applications.


### QR code

Banxa shows a QR code of the deposit address, a table of the deposit details (address, network, amount, and memo if any) and an "I've deposited" button. The customer scans the code or copies the address and sends from any wallet.

### MetaMask

Banxa shows a Connect wallet button. After the customer connects, Banxa checks the wallet is on the correct network (prompting the customer to switch if not), builds the transfer, and asks the customer to sign it in MetaMask.

Any EVM network and token that Banxa has configured is supported. Use `GET /v2/crypto/sell` to see which assets are available to you.

### Native Wallet (Return to Wallet)

Once the deposit address is ready, Banxa automatically redirects the customer to the order's `redirectUrl` (or, if that's missing, your account's default success URL). Your app then builds, signs and sends the transfer.

**Query parameters added to the redirect:**

| Parameter | Description |
|  --- | --- |
| `orderId` | Banxa order ID. |
| `providerWalletAddress` | Banxa's deposit address. Send the crypto here. |
| `providerWalletAddressTag` | Memo or tag to include with the transfer (memo chains only). |
| `blockchain` | Network code. |
| `coinType` | Asset code. |
| `coinAmount` | Exact amount to send (up to 8 decimal places). |
| `customerWalletAddress` | The customer wallet given at order creation (`walletAddress`). |


Parameters with no value are left out. Any query parameters already on your `redirectUrl` are kept.

**Example redirect:**

```
myapp://banxa/sell?orderId=7dbb7f5c2ad8bbbd022e4426fe98fb3a
  &providerWalletAddress=0x03EA4c7ED1d8199510017D88f2783Ef43ab5a4fd
  &blockchain=ETH&coinType=USDT&coinAmount=39.29000000
  &customerWalletAddress=0xcbf80ee36fbf03307962576f338fc50b942e0591
```

Send exactly `coinAmount` to `providerWalletAddress`, and include `providerWalletAddressTag` as the memo when it is present.

This flow requires the deposit address to be visible on your account (the "hide address" setting must be off). If it is hidden, the deposit details are never released and the customer stays on the waiting screen. If no redirect URL is available, the customer sees error `50422`.

### WebView bridge

For mobile apps that embed checkout in a WebView. Instead of redirecting, Banxa posts the deposit details to the host app with `postMessage`, and your app signs and sends the transfer. Contact Banxa for the message format.

## Custodial flow

Your platform holds custody of the customer's crypto and executes the blockchain transfer on their behalf. The customer never sees deposit instructions: Banxa shows them an "in progress with merchant" status page and doesn't send them the order email.

**Requirements:**

- Your account is set up as custodial. This is a B2B account setting configured by Banxa.
- You have a [webhook](/products/hosted-checkout/docs/transaction-lifecycle/webhooks) URL configured.


The custodial setting replaces your account's non-custodial flow, except the WebView bridge, which still applies.

Hiding the address is not the same as custodial
Hiding the deposit address on your account does not make the flow custodial. Without the custodial setting, a hidden address leaves the customer waiting with no way to deposit, and no webhook-driven flow takes over.

**How it works:**

1. Customer creates a sell order and completes checkout (KYC, bank account details).
2. Banxa updates the order status to `waitingPayment` and triggers a webhook to your endpoint. The webhook payload does not include the deposit address.
3. Your platform calls `GET /v2/orders/{orderId}` to retrieve the deposit address (`walletAddress`), memo (`walletAddressTag`) and `cryptoAmount`.
4. Your platform sends the crypto from the customer's `walletAddress` (the one given at order creation) to Banxa's deposit address.
5. Your platform confirms the transfer with `POST /v2/orders/{orderId}/confirm`.
6. Banxa detects the deposit and processes the fiat payout.


### Confirm the transfer

```
POST /{partnerRef}/v2/orders/{orderId}/confirm
```

`orderId` can be the Banxa order ID or your `externalOrderId`.

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| `transactionHash` | string | Yes | Hash of the on-chain transfer to Banxa's deposit address. |
| `sourceAddress` | string | Yes | Address the crypto was sent from. Must match the `walletAddress` given at order creation, or the request is rejected. |
| `sourceAddressTag` | string | No | Memo or tag of the source address. |
| `targetAddress` | string | Yes | Banxa's deposit address (`walletAddress` from `GET /v2/orders/{orderId}`). |
| `targetAddressTag` | string | No | Memo or tag sent with the transfer (`walletAddressTag` from `GET /v2/orders/{orderId}`). |


```bash
curl -X POST "https://api.banxa-sandbox.com/{partnerRef}/v2/orders/7dbb7f5c2ad8bbbd022e4426fe98fb3a/confirm" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transactionHash": "0x5ea06c4724e8119704a1b57c918acf31742eb06cdf3f8678fdb17f41bbaf968e",
    "sourceAddress": "0xcbf80ee36fbf03307962576f338fc50b942e0591",
    "targetAddress": "0x03EA4c7ED1d8199510017D88f2783Ef43ab5a4fd"
  }'
```

**202 Accepted**

```json
{
  "message": "This transaction hash has been accepted for validation."
}
```

A `202` means the hash was accepted for validation, not that the deposit is confirmed. Track the order through [webhooks](/products/hosted-checkout/docs/transaction-lifecycle/webhooks) or [order lookup](/products/hosted-checkout/docs/transaction-lifecycle/order-lookup).

**Best for:**

- Exchanges or custodial wallet products where your platform manages customer funds.
- Cases where you want to automate the crypto transfer without customer action.


## Choosing a flow

Contact Banxa to configure your preferred flow. The flow is set at the partner account level and applies to all sell orders for your integration.

| Consideration | QR code / MetaMask | Native Wallet / WebView bridge | Custodial |
|  --- | --- | --- | --- |
| You hold customer crypto | No | No | Yes |
| Who sends the crypto | Customer, in checkout | Your app, from the customer's wallet | Your platform |
| Webhook integration required | Optional | Optional | Yes |
| Calls `POST /v2/orders/{orderId}/confirm` | No | No | Yes |


→ See [Expiry and incorrect deposits](/products/hosted-checkout/docs/on-ramp-off-ramp/off-ramp-overview#expiry-and-incorrect-deposits) for what happens when a deposit is late or incorrect.