> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zet.money/llms.txt
> Use this file to discover all available pages before exploring further.

# Off-Ramp Integration

> Let your users sell crypto for NGN to their bank account

# Off-Ramp Integration Guide

This guide walks you through integrating the Zet Off-Ramp API to let your users sell crypto and receive Nigerian Naira (NGN) in their bank account.

## Overview

```
Your App                    Zet API                     Ramp Provider
   │                           │                              │
   ├── GET /onramp/banks ─────►│                              │
   │◄── Bank list ─────────────┤                              │
   │                           │                              │
   ├── POST /onramp/banks/verify ►│                           │
   │◄── Account name ──────────┤                              │
   │                           │                              │
   ├── POST /offramp/quote ───►│                              │
   │◄── Quote (fees, NGN amt) ─┤                              │
   │                           │                              │
   ├── POST /offramp/initiate ►│                              │
   │                           │── Swap Token → CNGN          │
   │                           │── Send CNGN ─────────────────►│
   │                           │                    Process NGN│
   │                           │                    to bank ───►
   │                           │                              │
   │◄── Webhook: offramp.completed ◄──────────────────────────┤
   │                           │                              │
```

## Step 1: Get available banks

Fetch the list of supported Nigerian banks:

```javascript theme={null}
const banks = await fetch('https://api.zet.money/v1/onramp/banks', {
  headers: { 'x-api-key': process.env.ZET_API_KEY },
}).then(r => r.json());

// banks.data = [
//   { bankCode: "044", bankName: "Access Bank", nibssCode: "000014" },
//   { bankCode: "035", bankName: "Wema Bank", nibssCode: "000017" },
//   ...
// ]
```

<Tip>
  Cache the bank list — it changes infrequently. Refresh it once per day.
</Tip>

## Step 2: Verify the bank account

Before quoting, verify the user's bank account to confirm the recipient name:

```javascript theme={null}
const verification = await fetch('https://api.zet.money/v1/onramp/banks/verify', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.ZET_API_KEY,
  },
  body: JSON.stringify({
    bankCode: '044',           // Access Bank
    accountNumber: '0123456789',
  }),
}).then(r => r.json());

// Show to user: "Sending to John Doe at Access Bank"
console.log(verification.data.accountName); // "John Doe"
```

<Warning>
  Always verify and display the account name to the user before proceeding. This prevents sending NGN to the wrong person.
</Warning>

## Step 3: Get a quote

Request a quote with the crypto amount to sell, token, and bank details:

```javascript theme={null}
const quote = await fetch('https://api.zet.money/v1/offramp/quote', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.ZET_API_KEY,
  },
  body: JSON.stringify({
    amount: '100',
    tokenSymbol: 'USDC',
    chain: 'base',
    bankCode: '044',
    accountNumber: '0123456789',
  }),
}).then(r => r.json());
```

### Quote response

```json theme={null}
{
  "success": true,
  "data": {
    "quoteId": "qt_01H8X4abc",
    "cryptoAmount": "100",
    "tokenSymbol": "USDC",
    "chain": "base",
    "fiatAmount": "148500",
    "fiatCurrency": "NGN",
    "rate": "1540.50",
    "fees": {
      "platformFee": "50",
      "providerFee": "148.50",
      "swapFee": "20",
      "totalFee": "218.50"
    },
    "bankAccount": {
      "bankName": "Access Bank",
      "bankCode": "044",
      "accountNumber": "0123456789",
      "accountName": "John Doe"
    },
    "expiresAt": "2026-03-02T10:05:00Z"
  }
}
```

### Fee breakdown

| Fee          | When          | Amount                            |
| ------------ | ------------- | --------------------------------- |
| Platform fee | Always        | 50 NGN                            |
| Provider fee | Always        | 0.1% of fiat amount (max 200 NGN) |
| Swap fee     | Source ≠ CNGN | 20 NGN                            |

## Step 4: Initiate the off-ramp

### Custodial flow (Zet-managed wallet)

```javascript theme={null}
const offramp = await fetch('https://api.zet.money/v1/offramp/initiate', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.ZET_API_KEY,
  },
  body: JSON.stringify({
    quoteId: quote.data.quoteId,
    walletId: 'wal_01H8X3abc',
    reference: `payout_${payoutId}`,
  }),
}).then(r => r.json());
```

For custodial wallets, the crypto is automatically debited and the process begins immediately.

### Non-custodial flow (user-managed wallet)

```javascript theme={null}
const offramp = await fetch('https://api.zet.money/v1/offramp/initiate', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.ZET_API_KEY,
  },
  body: JSON.stringify({
    quoteId: quote.data.quoteId,
    sourceAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f5bA16',
    reference: `payout_${payoutId}`,
  }),
}).then(r => r.json());

// User must send crypto to depositAddress
console.log(offramp.data.depositAddress); // "0xdef..."
```

For non-custodial, the response includes a `depositAddress` where the user must send the crypto. Once received, the off-ramp process begins.

### Response

```json theme={null}
{
  "success": true,
  "data": {
    "transactionId": "txn_01H8X4def",
    "reference": "payout_67890",
    "status": "pending",
    "depositAddress": null,
    "estimatedFiatAmount": "148500",
    "estimatedArrival": "5-15 minutes"
  }
}
```

## Step 5: Handle completion

Listen for the `offramp.completed` webhook:

```javascript theme={null}
app.post('/webhooks/zet', (req, res) => {
  const { event, data } = req.body;

  if (event === 'offramp.completed') {
    // NGN has been sent to the user's bank account
    await notifyUser(data.reference, {
      message: `₦${data.fiatAmount} sent to your bank account`,
      transactionHash: data.transactionHash,
    });
  }

  if (event === 'offramp.failed') {
    // Crypto is returned to the wallet on failure
    await notifyUser(data.reference, {
      message: 'Off-ramp failed. Your crypto has been returned.',
      error: data.errorMessage,
    });
  }

  res.status(200).send('OK');
});
```

## Processing timeline

| Step                          | Estimated Time   |
| ----------------------------- | ---------------- |
| Swap Token → CNGN (if needed) | 10-30 seconds    |
| CNGN → Provider deposit       | 30-60 seconds    |
| Provider → NGN bank transfer  | 3-15 minutes     |
| **Total**                     | **5-15 minutes** |

## Best practices

1. **Always verify the bank account first** — show the resolved name to your user
2. **Display clear fee breakdown** — users should know exactly what they'll receive
3. **Handle failures gracefully** — inform users their crypto is returned on failure
4. **Use webhooks** — don't poll for off-ramp status in production
5. **Cache bank list** — refresh once per day, not on every page load
