Skip to main content

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                     Flint (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 ──►(LiFi)│
   │                           │── Send CNGN ─────────────────►│
   │                           │                    Process NGN│
   │                           │                    to bank ───►
   │                           │                              │
   │◄── Webhook: offramp.completed ◄──────────────────────────┤
   │                           │                              │

Step 1: Get available banks

Fetch the list of supported Nigerian banks:
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" },
//   ...
// ]
Cache the bank list — it changes infrequently. Refresh it once per day.

Step 2: Verify the bank account

Before quoting, verify the user’s bank account to confirm the recipient name:
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"
Always verify and display the account name to the user before proceeding. This prevents sending NGN to the wrong person.

Step 3: Get a quote

Request a quote with the crypto amount to sell, token, and bank details:
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

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

Fee breakdown

FeeWhenAmount
Platform feeAlways20 NGN
Processing feeAlways0.1% of fiat amount (max 200 NGN)
Stamp dutyAmount ≥ 10,000 NGN50 NGN
Swap feeSource ≠ CNGN20 NGN

Step 4: Initiate the off-ramp

Custodial flow (Zet-managed wallet)

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)

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

{
  "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:
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

StepEstimated Time
Swap Token → CNGN (if needed)10-30 seconds
CNGN → Flint deposit30-60 seconds
Flint → NGN bank transfer3-15 minutes
Total5-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