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

# Accept NGN payments with virtual bank accounts

> Issue NGN virtual bank accounts to your customers and track deposits as they move through the PENDING_FX → SETTLED lifecycle in Dubu Pay.

Virtual bank accounts are the primary way your customers pay you in Nigerian Naira through Dubu Pay. When a customer sends a bank transfer to the account number you provide, Dubu Pay detects the deposit, converts it to USDT (or credits your internal balance), and notifies your server via webhook. This guide walks you through every step: creating a customer, choosing the right account type, issuing the account, sharing the details, and confirming the payment.

<Steps>
  <Step title="Create a customer">
    Before issuing a virtual bank account, you need a customer record. The customer ties the account to a real person and is required for permanent accounts.

    ```bash cURL theme={null}
    curl -X POST https://api.dubupay.com/api/v1/payments/customers \
      -H "X-Api-Key: dubu_sk_live_your_api_key" \
      -H "Content-Type: application/json" \
      -d '{
        "email": "amara@example.com",
        "first_name": "Amara",
        "last_name": "Okafor"
      }'
    ```

    ```javascript Node.js theme={null}
    const response = await fetch(
      "https://api.dubupay.com/api/v1/payments/customers",
      {
        method: "POST",
        headers: {
          "X-Api-Key": "dubu_sk_live_your_api_key",
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          email: "amara@example.com",
          first_name: "Amara",
          last_name: "Okafor",
        }),
      }
    );
    const { data: customer } = await response.json();
    ```

    The response contains the new customer's `id`, which you'll use in the next step.

    ```json theme={null}
    {
      "success": true,
      "data": {
        "id": "cus_01hx7k9p2qr3st4uv5wx6yz",
        "email": "amara@example.com",
        "first_name": "Amara",
        "last_name": "Okafor",
        "tier1_status": "NONE",
        "tier2_status": "NONE",
        "created_at": "2024-11-01T09:00:00.000Z"
      }
    }
    ```
  </Step>

  <Step title="Choose an account type">
    Dubu Pay offers two virtual bank account types. Pick the one that fits your use case:

    | Type        | TTL        | Rate               | When to use                                     |
    | ----------- | ---------- | ------------------ | ----------------------------------------------- |
    | `TEMPORARY` | 25 minutes | Locked at creation | One-off payments with a known amount            |
    | `PERMANENT` | No expiry  | Floating           | Recurring top-ups, wallets, long-lived accounts |

    <Warning>
      Permanent accounts require the customer to have completed BVN (Tier 1) verification. If `tier1_status` is not `APPROVED`, the request will be rejected.
    </Warning>

    <Note>
      Temporary onramps require you to pass an `amount` when creating the account. The rate is locked at that moment so the customer always knows exactly how much to send.
    </Note>
  </Step>

  <Step title="Issue a virtual bank account">
    Call `POST /payments/onramps` to create the virtual bank account. You can also use `POST /customers/:id/ngn-virtual-account` if you want the account linked directly to a specific customer record.

    <CodeGroup>
      ```bash cURL — temporary account theme={null}
      curl -X POST https://api.dubupay.com/api/v1/payments/onramps \
        -H "X-Api-Key: dubu_sk_live_your_api_key" \
        -H "Content-Type: application/json" \
        -d '{
          "type": "TEMPORARY",
          "customer_id": "cus_01hx7k9p2qr3st4uv5wx6yz",
          "amount": 5000,
          "settlement": {
            "mode": "INTERNAL_BALANCE",
            "asset": "USDT"
          }
        }'
      ```

      ```bash cURL — permanent account theme={null}
      curl -X POST https://api.dubupay.com/api/v1/payments/onramps \
        -H "X-Api-Key: dubu_sk_live_your_api_key" \
        -H "Content-Type: application/json" \
        -d '{
          "type": "PERMANENT",
          "customer_id": "cus_01hx7k9p2qr3st4uv5wx6yz",
          "settlement": {
            "mode": "ONCHAIN",
            "asset": "USDT",
            "chain": "TRON",
            "destination_address": "TYour_TRON_wallet_address"
          }
        }'
      ```

      ```javascript Node.js — temporary account theme={null}
      const response = await fetch(
        "https://api.dubupay.com/api/v1/payments/onramps",
        {
          method: "POST",
          headers: {
            "X-Api-Key": "dubu_sk_live_your_api_key",
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            type: "TEMPORARY",
            customer_id: "cus_01hx7k9p2qr3st4uv5wx6yz",
            amount: 5000,
            settlement: {
              mode: "INTERNAL_BALANCE",
              asset: "USDT",
            },
          }),
        }
      );
      const { data: onramp } = await response.json();
      ```
    </CodeGroup>

    The response includes the `virtual_account` object with the bank account details to show your customer.

    ```json theme={null}
    {
      "success": true,
      "data": {
        "id": "onramp_01hx7kbp2qr3st4uv5wx6ab",
        "type": "TEMPORARY",
        "status": "ACTIVE",
        "customer_id": "cus_01hx7k9p2qr3st4uv5wx6yz",
        "virtual_account": {
          "bank_name": "Wema Bank",
          "account_number": "9901234567",
          "account_name": "Dubu Pay / Amara Okafor",
          "expires_at": "2024-11-01T09:25:00.000Z"
        },
        "settlement": {
          "mode": "INTERNAL_BALANCE",
          "asset": "USDT"
        },
        "created_at": "2024-11-01T09:00:00.000Z"
      }
    }
    ```

    **Settlement modes:**

    * `INTERNAL_BALANCE` — converted USDT is credited to your merchant balance or the customer's internal ledger
    * `ONCHAIN` — converted USDT is sent directly to the `destination_address` you provide

    **Supported settlement assets:** `USDT`, `USDC`

    **Supported chains for onchain settlement:** `APTOS`, `BASE`, `CELO`, `ETHEREUM`, `POLYGON`, `SOLANA`, `TRON`
  </Step>

  <Step title="Share the account details with your payer">
    Present the `virtual_account` fields to your customer in your UI or payment flow:

    * **Bank name:** `Wema Bank`
    * **Account number:** `9901234567`
    * **Account name:** `Dubu Pay / Amara Okafor`
    * **Amount to send:** ₦5,000 (for temporary accounts)
    * **Expiry:** 25 minutes from creation (for temporary accounts)

    <Tip>
      For temporary accounts, always show the exact amount and the countdown timer. If the account expires before payment, create a new onramp with a fresh rate.
    </Tip>
  </Step>

  <Step title="Detect the deposit">
    You have two options for detecting when a payment arrives:

    **Option A — Webhooks (recommended)**

    Register a webhook endpoint to receive real-time `deposit.*` events. See the [Webhooks guide](/guides/webhooks) for setup instructions. The events you'll receive are:

    | Event                        | Meaning                                          |
    | ---------------------------- | ------------------------------------------------ |
    | `deposit.received`           | NGN transfer received; FX conversion in progress |
    | `deposit.pending_withdrawal` | FX complete; USDT being sent to destination      |
    | `deposit.settled`            | USDT delivered; settlement complete              |
    | `deposit.failed`             | Something went wrong                             |
    | `deposit.flagged`            | Deposit held for compliance review               |

    **Option B — Polling**

    Poll `GET /payments/deposits` filtered by the onramp ID:

    <CodeGroup>
      ```bash cURL theme={null}
      curl "https://api.dubupay.com/api/v1/payments/deposits?onramp_id=onramp_01hx7kbp2qr3st4uv5wx6ab" \
        -H "X-Api-Key: dubu_sk_live_your_api_key"
      ```

      ```javascript Node.js theme={null}
      const response = await fetch(
        "https://api.dubupay.com/api/v1/payments/deposits?onramp_id=onramp_01hx7kbp2qr3st4uv5wx6ab",
        {
          headers: { "X-Api-Key": "dubu_sk_live_your_api_key" },
        }
      );
      const { data } = await response.json();
      ```
    </CodeGroup>

    <Note>
      Prefer webhooks over polling in production. Polling introduces latency and adds unnecessary API calls.
    </Note>
  </Step>

  <Step title="Check deposit status">
    Fetch a specific deposit by ID to get the full status and settlement details:

    <CodeGroup>
      ```bash cURL theme={null}
      curl https://api.dubupay.com/api/v1/payments/deposits/dep_01hx7kcp2qr3st4uv5wx6cd \
        -H "X-Api-Key: dubu_sk_live_your_api_key"
      ```

      ```javascript Node.js theme={null}
      const response = await fetch(
        "https://api.dubupay.com/api/v1/payments/deposits/dep_01hx7kcp2qr3st4uv5wx6cd",
        {
          headers: { "X-Api-Key": "dubu_sk_live_your_api_key" },
        }
      );
      const { data: deposit } = await response.json();
      ```
    </CodeGroup>

    ```json theme={null}
    {
      "success": true,
      "data": {
        "id": "dep_01hx7kcp2qr3st4uv5wx6cd",
        "onramp_id": "onramp_01hx7kbp2qr3st4uv5wx6ab",
        "status": "SETTLED",
        "amount_ngn": "5000.00",
        "amount_asset": "3.21",
        "asset": "USDT",
        "chain": "TRON",
        "tx_hash": "0xabc123...",
        "payment_reference": "TRF20241101090012",
        "settled_at": "2024-11-01T09:04:32.000Z"
      }
    }
    ```

    **Deposit status lifecycle:**

    ```
    PENDING_FX → PENDING_WITHDRAWAL → SETTLED
                                   ↘ FAILED
                                   ↘ FLAGGED
                                   ↘ REVERSED
    ```

    Only take action (e.g., credit your customer's account or fulfil an order) when the status is `SETTLED`.
  </Step>
</Steps>

## Testing in sandbox

Use the sandbox endpoint to simulate a deposit without sending real funds:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.dubupay.com/api/v1/payments/sandbox/deposits \
    -H "X-Api-Key: dubu_sk_test_your_test_key" \
    -H "Content-Type: application/json" \
    -d '{
      "onramp_id": "onramp_01hx7kbp2qr3st4uv5wx6ab"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch(
    "https://api.dubupay.com/api/v1/payments/sandbox/deposits",
    {
      method: "POST",
      headers: {
        "X-Api-Key": "dubu_sk_test_your_test_key",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        onramp_id: "onramp_01hx7kbp2qr3st4uv5wx6ab",
      }),
    }
  );
  ```
</CodeGroup>

Pass `"should_flag": true` to simulate a flagged deposit instead of a settled one.

<Warning>
  Sandbox deposits only work with test API keys (`dubu_sk_test_*`). Never use test keys in production.
</Warning>
