---
title: Static NGN Account API
description: Create and manage permanent NGN account numbers for your customers. Secret key only; enabled for selected merchants on request.
---

# Static NGN Account

Issue permanent bank account numbers to your customers and receive every transfer as a booked payment with an instant webhook. See the [concept guide](/guide/static-accounts) for how the product behaves.

All endpoints require a **secret key** (`sk_*`). Static NGN Accounts are enabled for selected merchants on request; contact our business support team for access. Every endpoint except `GET /status` returns `403` with `error.code = "STATIC_ACCOUNT_NOT_ENABLED"` when the product is not enabled for your account; `GET /status` always answers so you can check before you build.

Base path: `/v1/checkout/static-account`

## Check availability

```
GET /v1/checkout/static-account/status
```

```json
{
  "success": true,
  "data": {
    "enabled": true,
    "currency": "NGN",
    "bank_names": ["PalmPay"],
    "identity_types": ["bvn", "nin", "rc"]
  }
}
```

`bank_names` lists the bank(s) new accounts will be issued at, so you can show the customer where to transfer.

## Create an account

```
POST /v1/checkout/static-account
```

### Request body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `customer_name` | string | Yes | Full legal name of the account holder, as on the identity record (2 to 200 characters) |
| `identity_type` | string | Yes | `"bvn"`, `"nin"` or `"rc"` |
| `identity_number` | string | Yes | 11-digit BVN or NIN, or the CAC number prefixed `RC` / `BN` (e.g. `"RC1234567"`) |
| `customer_email` | string | No | Account holder's email |
| `customer_phone` | string | No | Account holder's phone |
| `account_name` | string | No | Name printed on the account. Defaults to `customer_name`. The bank may append your business suffix |
| `reference` | string | Yes | Your unique reference for this account, typically your customer id (max 120 chars). Idempotent: sending the same reference again returns the existing account instead of creating another, so a customer never ends up with several numbers |
| `metadata` | object | No | Custom key-value data stored with the account and echoed on every `charge.success` |

The name is checked against the identity record before anything is created. If it does not match, the request fails with `IDENTITY_MISMATCH`.

### Example request

::: code-group

```bash [cURL]
curl -X POST https://api.zevpaycheckout.com/v1/checkout/static-account \
  -H "Content-Type: application/json" \
  -H "x-api-key: sk_live_your_secret_key" \
  -d '{
    "customer_name": "Adaeze Okafor",
    "identity_type": "bvn",
    "identity_number": "22345678901",
    "customer_email": "adaeze@example.com",
    "reference": "customer-1042",
    "metadata": { "plan": "pro" }
  }'
```

```javascript [Node.js]
const response = await fetch(
  "https://api.zevpaycheckout.com/v1/checkout/static-account",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": "sk_live_your_secret_key",
    },
    body: JSON.stringify({
      customer_name: "Adaeze Okafor",
      identity_type: "bvn",
      identity_number: "22345678901",
      customer_email: "adaeze@example.com",
      reference: "customer-1042",
      metadata: { plan: "pro" },
    }),
  }
);
const { data } = await response.json();
```

:::

### Response

```json
{
  "success": true,
  "data": {
    "id": "V1StGXR8_Z5jdHi6B-myT",
    "account_number": "6641234567",
    "account_name": "Adaeze Okafor",
    "bank_name": "PalmPay",
    "bank_code": "100039",
    "currency": "NGN",
    "status": "active",
    "customer_name": "Adaeze Okafor",
    "customer_email": "adaeze@example.com",
    "customer_phone": null,
    "identity_type": "bvn",
    "identity_number_masked": "*******8901",
    "reference": "customer-1042",
    "metadata": { "plan": "pro" },
    "inflow_count": 0,
    "total_received": "0.00",
    "last_payment_at": null,
    "disabled_at": null,
    "deleted_at": null,
    "created_at": "2026-09-08T09:12:00.000Z",
    "updated_at": "2026-09-08T09:12:00.000Z"
  }
}
```

Show the customer `account_number` and `bank_name`. The identity number is never returned in full.

### Account object

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Account id. Use it in every other call |
| `account_number` | string | The NUBAN the customer transfers to |
| `account_name` | string | Name on the account |
| `bank_name` | string | Bank the customer selects when transferring |
| `bank_code` | string \| null | NIP bank code from the live bank list, when known. Show `bank_name` to customers; codes differ between bank apps |
| `status` | string | `"active"`, `"disabled"` (paused by you), `"suspended"` (by ZevPay), `"deleted"` |
| `identity_type` | string | `"bvn"`, `"nin"` or `"rc"` |
| `identity_number_masked` | string | Last four characters only |
| `reference` | string \| null | Your reference |
| `inflow_count` | integer | Payments received so far |
| `total_received` | string | Gross amount received so far, in naira |
| `last_payment_at` | string \| null | ISO 8601 timestamp of the latest payment |

## List accounts

```
GET /v1/checkout/static-account
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | integer | 1 | Page number |
| `page_size` | integer | 20 | Items per page (max 100) |
| `status` | string | none | `"active"`, `"disabled"`, `"suspended"` or `"deleted"` |
| `search` | string | none | Matches account number, customer name, account name, reference or email |

Returns `{ items, total, page, page_size, total_pages }` with account objects in `items`.

## Get an account

```
GET /v1/checkout/static-account/{id}
```

Returns the account object.

## List payments on an account

```
GET /v1/checkout/static-account/{id}/payments
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | integer | 1 | Page number |
| `page_size` | integer | 20 | Items per page (max 100) |

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "9f1c1e2a-8c1d-4d5c-9a44-1e0b2f6d7a10",
        "static_account_id": "V1StGXR8_Z5jdHi6B-myT",
        "amount": "25000.00",
        "fee": "375.00",
        "net_amount": "24625.00",
        "currency": "NGN",
        "status": "completed",
        "payer": { "name": "ADAEZE OKAFOR", "bank": "GTBank", "account": "0123456789" },
        "narration": "Top up",
        "session_id": "000013250908091200000123456789",
        "received_at": "2026-09-08T09:12:44.000Z",
        "booked_at": "2026-09-08T09:12:45.000Z",
        "settlement": {
          "status": "pending_settlement",
          "expected_payout_at": "2026-09-09T16:00:00.000Z",
          "settled_at": null,
          "payout_id": null
        },
        "refundable": true,
        "refund": null
      }
    ],
    "total": 1,
    "page": 1,
    "page_size": 20,
    "total_pages": 1
  }
}
```

`status` is `"completed"` once the payment is booked; `"processing"` is transient while a payment is being booked.

`settlement.status` follows the payment through settlement: `pending_settlement` (waiting for the next cutoff; `expected_payout_at` is the scheduled payout), `batched` (in a payout run), `paid_out` or `wallet_credit` (settled to you, `settled_at` set), `under_review` (held because a refund was requested), `refunded`, or `failed` (payout failed; support will retry). `refundable` is true while a refund can still be requested.

## Pause and resume

```
POST /v1/checkout/static-account/{id}/disable
POST /v1/checkout/static-account/{id}/enable
```

Pausing sets `status` to `"disabled"`; the bank rejects transfers until you resume. Both return the account object. You cannot resume an account that ZevPay has suspended.

## Delete an account

```
DELETE /v1/checkout/static-account/{id}
```

Retires the account number permanently (`status: "deleted"`). This cannot be undone and the number is not reused. Payment history stays available. Returns the account object.

## Refunds

A payment can be refunded to the payer as long as it has **not yet been settled to you**: the request is accepted only while the payment is still pending settlement (before the next payout batch picks it up). Refunds are two-step: your request holds the payment out of settlement and hands it to ZevPay; ZevPay then returns the money to the payer's bank account and fires [`charge.refunded`](/webhooks/events#charge-refunded). If ZevPay declines the request, the payment goes back into your next settlement and the refund shows as `rejected`.

### Request a refund

```
POST /v1/checkout/static-account/{id}/payments/{payment_id}/refund
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `reason` | string | No | Why you are refunding (max 500 chars). Shown to ZevPay's operations team |

`payment_id` is the `id` from the [payments list](#list-payments-on-an-account), where `refundable: true` tells you the payment can still be refunded. Repeating the request for the same payment returns the open refund.

```json
{
  "success": true,
  "data": {
    "id": "V1StGXR8_Z5jdHi6B-myT",
    "amount": "25000.00",
    "currency": "NGN",
    "status": "requested",
    "reason": "Duplicate payment",
    "rejection_reason": null,
    "failure_reason": null,
    "payment_id": "9f1c1e2a-8c1d-4d5c-9a44-1e0b2f6d7a10",
    "requested_at": "2026-09-08T10:02:11.000Z",
    "completed_at": null
  }
}
```

`status` moves `requested` → `processing` → `completed` (money returned) or `failed`; `rejected` means ZevPay declined and the payment settles to you as normal.

### List and get refunds

```
GET /v1/checkout/static-account/refunds?status=requested&page=1&page_size=20
GET /v1/checkout/static-account/refunds/{refund_id}
```

Both return refund objects in the shape above.

| Code | HTTP | Meaning |
|------|------|---------|
| `REFUND_NOT_ALLOWED` | 400 | The payment is not booked, or is already in a settlement batch or paid out |
| `ALREADY_REFUNDED` | 400 | This payment was refunded already |

## Webhook

Every payment into a static account fires [`charge.success`](/webhooks/events#charge-success) to the webhook URL of the API key that owns the account, with `channel: "static_account"` and a `static_account` object identifying which account was paid. See the [payload](/webhooks/events#static-ngn-account).

## Errors

| Code | HTTP | Meaning |
|------|------|---------|
| `STATIC_ACCOUNT_NOT_ENABLED` | 403 | Static NGN Accounts are not enabled for this merchant account |
| `STATIC_ACCOUNT_SUSPENDED` | 403 | Access is suspended: existing accounts and history stay visible on the dashboard, but every API call and every action is refused until it is lifted |
| `IDENTITY_MISMATCH` | 400 | `customer_name` does not match the identity record for `identity_number` |
| `IDENTITY_CHECK_UNAVAILABLE` | 503 | The identity registry could not be reached; retry shortly |
| `STATIC_ACCOUNT_REJECTED` | 400 | The bank declined to issue the account for the details given |
| `STATIC_ACCOUNT_UNAVAILABLE` | 503 | Account issuing is temporarily unavailable; retry later |
| `REFERENCE_IN_USE` | 409 | `reference` belongs to a deleted account; use a new reference |
| `ACCOUNT_SUSPENDED` | 400 | The account is suspended by ZevPay and cannot be changed |
| `ACCOUNT_DELETED` | 400 | The account has been deleted |
| `STATIC_ACCOUNT_RATE_LIMITED` | 429 | Too many create attempts, or too many identity mismatches, in the last hour for your account |
| `VALIDATION_ERROR` | 400 | A field has the right shape but an invalid value (message says which). Missing or wrongly typed fields return the standard `HTTP_ERROR` envelope with a list of messages |
