Skip to main content

On-Ramp Integration Guide

This guide walks you through integrating the Zet On-Ramp API to let your users buy crypto with Nigerian Naira (NGN) via bank transfer.

Overview

Your App                    Zet API                     Flint (Provider)
   │                           │                              │
   ├── POST /onramp/quote ────►│                              │
   │◄── Quote (fees, rate) ────┤                              │
   │                           │                              │
   ├── POST /onramp/initiate ─►│── Create order ─────────────►│
   │◄── depositAccount ────────┤◄── Bank details ─────────────┤
   │                           │                              │
   │  (User transfers NGN      │                              │
   │   to depositAccount)      │                              │
   │                           │                              │
   │                           │◄── Deposit detected ─────────┤
   │                           │                              │
   │                           │── Swap CNGN → Token ──►(LiFi)│
   │                           │── Deliver to wallet           │
   │                           │                              │
   │◄── Webhook: onramp.completed ◄───────────────────────────┤
   │                           │                              │

Step 1: Get a quote

Before initiating a transaction, get a real-time quote that shows the user exactly what they’ll pay and receive.
const quote = await fetch('https://api.zet.money/v1/onramp/quote', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.ZET_API_KEY,
  },
  body: JSON.stringify({
    amount: '50000',        // NGN to spend
    tokenSymbol: 'USDC',   // Crypto to receive
    chain: 'base',         // Target chain
  }),
}).then(r => r.json());

// Display to user:
// "Pay ₦50,070 → Receive 32.45 USDC"
// "Fees: Provider ₦50, Swap ₦20"
console.log(quote.data);

Quote response

{
  "success": true,
  "data": {
    "quoteId": "qt_01H8X3xyz",
    "fiatAmount": "50000",
    "fiatCurrency": "NGN",
    "cryptoAmount": "32.45",
    "tokenSymbol": "USDC",
    "chain": "base",
    "rate": "1540.50",
    "fees": {
      "providerFee": "50",
      "flatFee": "0",
      "swapFee": "20",
      "totalFee": "70"
    },
    "expiresAt": "2026-03-02T10:05:00Z"
  }
}
Quotes expire in ~5 minutes. If the user doesn’t proceed in time, request a new quote.

Fee breakdown

FeeWhenAmount
Provider feeAlways0.1% of CNGN amount
Flat feeAmount > 50,000 NGN50 NGN
Swap feeTarget ≠ CNGN20 NGN
Buying CNGN directly? No swap fee — cheapest option.

Step 2: Initiate the on-ramp

Once the user confirms, initiate the transaction with the quoteId:
const onramp = await fetch('https://api.zet.money/v1/onramp/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',      // Zet-managed wallet
    reference: `order_${orderId}`,   // Your unique reference
    callbackUrl: 'https://yourapp.com/webhooks/zet', // Optional per-tx override
  }),
}).then(r => r.json());

Response — display these bank details to the user

{
  "success": true,
  "data": {
    "transactionId": "txn_01H8X3def",
    "reference": "order_12345",
    "status": "pending",
    "depositAccount": {
      "bankName": "Wema Bank",
      "bankCode": "035",
      "accountNumber": "0123456789",
      "accountName": "Zet / John Doe"
    },
    "amount": "50070",
    "expiresAt": "2026-03-02T10:30:00Z"
  }
}
The user must transfer the exact amount shown (amount) to the deposit account. Partial or excess transfers may delay processing.

Step 3: Wait for completion

Register a webhook and listen for onramp.completed:
// Express.js webhook handler
app.post('/webhooks/zet', (req, res) => {
  // Verify signature
  const signature = req.headers['x-zet-signature'];
  if (!verifyWebhook(req.body, signature, process.env.ZET_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const { event, data } = req.body;

  switch (event) {
    case 'onramp.completed':
      // Crypto delivered! Update your database
      await markOrderComplete(data.reference, {
        cryptoAmount: data.amount,
        tokenSymbol: data.tokenSymbol,
        transactionHash: data.transactionHash,
      });
      break;

    case 'onramp.failed':
      // Handle failure
      await markOrderFailed(data.reference, data.errorMessage);
      break;
  }

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

Option B: Polling

Poll the status endpoint every 30 seconds:
async function waitForOnramp(transactionId) {
  while (true) {
    const { data } = await fetch(
      `https://api.zet.money/v1/onramp/${transactionId}`,
      { headers: { 'x-api-key': process.env.ZET_API_KEY } }
    ).then(r => r.json());

    if (data.status === 'completed') return data;
    if (data.status === 'failed') throw new Error(data.errorMessage);

    await new Promise(r => setTimeout(r, 30000)); // Wait 30s
  }
}

Step 4: Non-custodial flow

If you’re not using Zet-managed wallets, provide destinationAddress instead of walletId:
const onramp = await fetch('https://api.zet.money/v1/onramp/initiate', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.ZET_API_KEY,
  },
  body: JSON.stringify({
    quoteId: quote.data.quoteId,
    destinationAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f5bA16',
    reference: `order_${orderId}`,
  }),
}).then(r => r.json());
The flow is identical — crypto is delivered to the provided address once the bank deposit is confirmed.

Supported tokens

TokenChainSwap fee
CNGNBase, BSCNone
USDCBase, BSC20 NGN
ETHBase20 NGN
cbBTCBase20 NGN
BNBBSC20 NGN
USDTBSC20 NGN

Best practices

  1. Always display the quote before initiating — show fees, rate, and estimated crypto amount
  2. Use idempotent references — if a user refreshes, use the same reference to avoid duplicate orders
  3. Set a reasonable expiry UI — quotes expire in ~5 minutes, deposit windows in ~30 minutes
  4. Prefer webhooks over polling — more efficient and real-time
  5. Handle the onramp.failed event — notify the user and offer to retry