> ## 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.

# On-Ramp Integration

> Accept NGN payments and deliver crypto to your users

# 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                     Ramp 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          │
   │                           │── 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.

<CodeGroup>
  ```javascript Node.js theme={null}
  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: Platform ₦50, Provider ₦50, Swap ₦20"
  console.log(quote.data);
  ```

  ```python Python theme={null}
  import requests

  quote = requests.post(
      'https://api.zet.money/v1/onramp/quote',
      headers={'x-api-key': ZET_API_KEY},
      json={
          'amount': '50000',
          'tokenSymbol': 'USDC',
          'chain': 'base',
      },
  ).json()

  print(f"Pay ₦{quote['data']['fiatAmount']} → Receive {quote['data']['cryptoAmount']} USDC")
  ```
</CodeGroup>

### Quote response

```json theme={null}
{
  "success": true,
  "data": {
    "quoteId": "qt_01H8X3xyz",
    "fiatAmount": "50000",
    "fiatCurrency": "NGN",
    "cryptoAmount": "32.45",
    "tokenSymbol": "USDC",
    "chain": "base",
    "rate": "1540.50",
    "fees": {
      "providerFee": "50",
      "platformFee": "50",
      "swapFee": "20",
      "totalFee": "120"
    },
    "expiresAt": "2026-03-02T10:05:00Z"
  }
}
```

<Info>
  Quotes expire in \~5 minutes. If the user doesn't proceed in time, request a new quote.
</Info>

### Fee breakdown

| Fee          | When          | Amount                            |
| ------------ | ------------- | --------------------------------- |
| Provider fee | Always        | 0.1% of CNGN amount (max 200 NGN) |
| Platform fee | Always        | 50 NGN                            |
| Swap fee     | Target ≠ CNGN | 20 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`:

```javascript theme={null}
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

```json theme={null}
{
  "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"
  }
}
```

<Warning>
  The user must transfer the **exact** amount shown (`amount`) to the deposit account. Partial or excess transfers may delay processing.
</Warning>

## Step 3: Wait for completion

### Option A: Webhooks (recommended)

Register a webhook and listen for `onramp.completed`:

```javascript theme={null}
// 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:

```javascript theme={null}
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`:

```javascript theme={null}
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

| Token | Chain     | Swap fee |
| ----- | --------- | -------- |
| CNGN  | Base, BSC | None     |
| USDC  | Base, BSC | 20 NGN   |
| ETH   | Base      | 20 NGN   |
| cbBTC | Base      | 20 NGN   |
| BNB   | BSC       | 20 NGN   |
| USDT  | BSC       | 20 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
