# Transfer API

Programmatically send money from a wallet to bank accounts or other ZevPay users via PayID.

::: warning Security Notice
Transfer endpoints move funds **out** of your wallet. These endpoints require a **secret key** and a wallet with **programmable debit** enabled. Keep your secret keys secure and consider IP whitelisting.
:::

## Authentication

All transfer endpoints require:

1. A **secret key** (`sk_live_*` or `sk_test_*`) passed via `Authorization: Bearer sk_live_...`
2. The API key must have the `transfers` permission
3. A programmable-debit wallet must be linked to the API key

See [Authentication](/guide/authentication) for details.

---

## List Banks

Returns all supported banks for external transfers.

```
GET /v1/checkout/transfer/banks
```

### Authentication

Secret key required. No `transfers` permission needed.

### Example Request

::: code-group

```bash [cURL]
curl https://api.zevpaycheckout.com/v1/checkout/transfer/banks \
  -H "Authorization: Bearer sk_live_your_secret_key"
```

```javascript [Node.js]
const response = await fetch('https://api.zevpaycheckout.com/v1/checkout/transfer/banks', {
  headers: {
    'Authorization': 'Bearer sk_live_your_secret_key',
  },
});
const { data } = await response.json();
console.log(data.banks);
```

```python [Python]
import requests

response = requests.get(
    'https://api.zevpaycheckout.com/v1/checkout/transfer/banks',
    headers={'Authorization': 'Bearer sk_live_your_secret_key'},
)
banks = response.json()['data']['banks']
```

:::

### Response

```json
{
  "success": true,
  "data": {
    "banks": [
      {
        "bankCode": "044",
        "bankName": "Access Bank"
      },
      {
        "bankCode": "058",
        "bankName": "Guaranty Trust Bank"
      }
    ]
  }
}
```

---

## Resolve Bank Account

Verify a bank account number and retrieve the account holder's name (name enquiry).

```
POST /v1/checkout/transfer/banks/resolve
```

### Authentication

Secret key required. No `transfers` permission needed.

### Request Body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `account_number` | string | Yes | 10-digit bank account number |
| `bank_code` | string | Yes | Bank code from the [List Banks](#list-banks) endpoint |

### Example Request

::: code-group

```bash [cURL]
curl -X POST https://api.zevpaycheckout.com/v1/checkout/transfer/banks/resolve \
  -H "Authorization: Bearer sk_live_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "account_number": "0123456789",
    "bank_code": "058"
  }'
```

```javascript [Node.js]
const response = await fetch('https://api.zevpaycheckout.com/v1/checkout/transfer/banks/resolve', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_your_secret_key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    account_number: '0123456789',
    bank_code: '058',
  }),
});
const { data } = await response.json();
console.log(data.account_name); // "JOHN DOE"
```

```python [Python]
import requests

response = requests.post(
    'https://api.zevpaycheckout.com/v1/checkout/transfer/banks/resolve',
    headers={'Authorization': 'Bearer sk_live_your_secret_key'},
    json={
        'account_number': '0123456789',
        'bank_code': '058',
    },
)
data = response.json()['data']
print(data['account_name'])  # "JOHN DOE"
```

:::

### Response

```json
{
  "success": true,
  "data": {
    "account_name": "JOHN DOE",
    "account_number": "0123456789",
    "bank_code": "058"
  }
}
```

---

## Calculate Charges

Calculate the fees for a bank transfer before initiating it.

```
POST /v1/checkout/transfer/charges
```

### Authentication

Secret key required. `transfers` permission required.

### Request Body

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `amount` | integer | Yes | Transfer amount in **kobo** |
| `bank_code` | string | Yes | Destination bank code |

### Example Request

::: code-group

```bash [cURL]
curl -X POST https://api.zevpaycheckout.com/v1/checkout/transfer/charges \
  -H "Authorization: Bearer sk_live_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 500000,
    "bank_code": "058"
  }'
```

```javascript [Node.js]
const response = await fetch('https://api.zevpaycheckout.com/v1/checkout/transfer/charges', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_your_secret_key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: 500000,
    bank_code: '058',
  }),
});
const { data } = await response.json();
console.log(data);
```

:::

### Response

```json
{
  "success": true,
  "data": {
    "amount": 500000,
    "fee": 2625,
    "vat": 197,
    "stamp_duty": 0,
    "total": 502822,
    "currency": "NGN"
  }
}
```

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `amount` | integer | Original transfer amount in kobo |
| `fee` | integer | Transfer fee in kobo |
| `vat` | integer | VAT on the fee in kobo |
| `stamp_duty` | integer | Stamp duty in kobo (applied on amounts ≥ ₦10,000) |
| `total` | integer | Total debit amount (amount + fee + vat + stamp_duty) in kobo |
| `currency` | string | Currency code (`"NGN"`) |

---

## Get Balance

Retrieve the available balance of the linked wallet.

```
GET /v1/checkout/transfer/balance
```

### Authentication

Secret key required. `transfers` permission required.

### Example Request

::: code-group

```bash [cURL]
curl https://api.zevpaycheckout.com/v1/checkout/transfer/balance \
  -H "Authorization: Bearer sk_live_your_secret_key"
```

```javascript [Node.js]
const response = await fetch('https://api.zevpaycheckout.com/v1/checkout/transfer/balance', {
  headers: {
    'Authorization': 'Bearer sk_live_your_secret_key',
  },
});
const { data } = await response.json();
console.log(`Balance: ₦${(data.available_balance / 100).toFixed(2)}`);
```

:::

### Response

```json
{
  "success": true,
  "data": {
    "available_balance": 15000000,
    "currency": "NGN"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `available_balance` | integer | Available balance in kobo |
| `currency` | string | Currency code (`"NGN"`) |

---

## Initiate Transfer

Send money to a bank account or ZevPay user. The `type` field determines the transfer destination.

```
POST /v1/checkout/transfer
```

### Authentication

Secret key required. `transfers` permission required.

### Request Body — Bank Transfer

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `type` | string | Yes | Must be `"bank_transfer"` |
| `account_number` | string | Yes | 10-digit destination account number |
| `bank_code` | string | Yes | Destination bank code |
| `account_name` | string | Yes | Account holder name (from [Resolve](#resolve-bank-account)) |
| `amount` | integer | Yes | Amount in **kobo** (min: 10,000 = ₦100, max: 500,000,000 = ₦5,000,000) |
| `narration` | string | No | Transfer description (max 100 characters) |
| `reference` | string | No | Your unique reference for idempotency (max 128 characters) |
| `metadata` | object | No | Custom key-value metadata |

### Request Body — PayID Transfer

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `type` | string | Yes | Must be `"payid"` |
| `pay_id` | string | Yes | Recipient's ZevPay ID (e.g., `"john"` or `"@john"`) |
| `amount` | integer | Yes | Amount in **kobo** (min: 10,000, max: 500,000,000) |
| `narration` | string | No | Transfer description (max 100 characters) |
| `reference` | string | No | Your unique reference for idempotency (max 128 characters) |
| `metadata` | object | No | Custom key-value metadata |

### Example Request — Bank Transfer

::: code-group

```bash [cURL]
curl -X POST https://api.zevpaycheckout.com/v1/checkout/transfer \
  -H "Authorization: Bearer sk_live_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "bank_transfer",
    "account_number": "0123456789",
    "bank_code": "058",
    "account_name": "JOHN DOE",
    "amount": 500000,
    "narration": "Vendor payment",
    "reference": "payout_001",
    "metadata": {
      "invoice_id": "INV-2026-001"
    }
  }'
```

```javascript [Node.js]
const response = await fetch('https://api.zevpaycheckout.com/v1/checkout/transfer', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_your_secret_key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'bank_transfer',
    account_number: '0123456789',
    bank_code: '058',
    account_name: 'JOHN DOE',
    amount: 500000,
    narration: 'Vendor payment',
    reference: 'payout_001',
    metadata: { invoice_id: 'INV-2026-001' },
  }),
});
const { data } = await response.json();
console.log(data.reference, data.status);
```

```python [Python]
import requests

response = requests.post(
    'https://api.zevpaycheckout.com/v1/checkout/transfer',
    headers={'Authorization': 'Bearer sk_live_your_secret_key'},
    json={
        'type': 'bank_transfer',
        'account_number': '0123456789',
        'bank_code': '058',
        'account_name': 'JOHN DOE',
        'amount': 500000,
        'narration': 'Vendor payment',
        'reference': 'payout_001',
        'metadata': {'invoice_id': 'INV-2026-001'},
    },
)
data = response.json()['data']
print(data['reference'], data['status'])
```

:::

### Example Request — PayID Transfer

::: code-group

```bash [cURL]
curl -X POST https://api.zevpaycheckout.com/v1/checkout/transfer \
  -H "Authorization: Bearer sk_live_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "payid",
    "pay_id": "john",
    "amount": 250000,
    "narration": "Freelance payment"
  }'
```

```javascript [Node.js]
const response = await fetch('https://api.zevpaycheckout.com/v1/checkout/transfer', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_live_your_secret_key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'payid',
    pay_id: 'john',
    amount: 250000,
    narration: 'Freelance payment',
  }),
});
const { data } = await response.json();
console.log(data.reference, data.status);
```

:::

### Response

```json
{
  "success": true,
  "data": {
    "reference": "ZVP-TRF-abc123def456",
    "public_id": "xK9mQ2pL4vR7nW3jY",
    "type": "bank_transfer",
    "status": "completed",
    "amount": 500000,
    "fees": 2625,
    "vat": 197,
    "stamp_duty": 0,
    "currency": "NGN",
    "narration": "Vendor payment",
    "recipient": {
      "name": "JOHN DOE",
      "bank": "Guaranty Trust Bank",
      "account_number": "0123456789"
    },
    "merchant_reference": "payout_001",
    "metadata": {
      "invoice_id": "INV-2026-001"
    },
    "created_at": "2026-03-08T14:30:00.000Z",
    "completed_at": "2026-03-08T14:30:02.000Z"
  }
}
```

### Response Fields

| Field | Type | Description |
|-------|------|-------------|
| `reference` | string | ZevPay transaction reference |
| `public_id` | string | Public transaction identifier |
| `type` | string | `"bank_transfer"` or `"payid"` |
| `status` | string | `"pending"`, `"completed"`, or `"failed"` |
| `amount` | integer | Transfer amount in kobo |
| `fees` | integer | Transfer fees in kobo (0 for PayID transfers) |
| `vat` | integer | VAT in kobo |
| `stamp_duty` | integer | Stamp duty in kobo |
| `currency` | string | Currency code (`"NGN"`) |
| `narration` | string \| null | Transfer description |
| `recipient.name` | string | Recipient name |
| `recipient.bank` | string \| null | Recipient bank (bank transfers only) |
| `recipient.account_number` | string \| null | Recipient account number (bank transfers only) |
| `merchant_reference` | string \| null | Your idempotency reference |
| `metadata` | object \| null | Your custom metadata |
| `created_at` | string | ISO 8601 timestamp |
| `completed_at` | string \| null | ISO 8601 timestamp (when completed) |

### Transfer Status Lifecycle

| Status | Description |
|--------|-------------|
| `pending` | Transfer is being processed (bank transfers only) |
| `completed` | Transfer delivered successfully |
| `failed` | Transfer was rejected by the bank |

::: info
PayID transfers complete instantly — the status will always be `"completed"` on success.

Bank transfers may briefly show `"pending"` before resolving to `"completed"` or `"failed"`.
:::

### Idempotency

If you provide a `reference` and a transfer with that reference already exists, the API returns the existing transfer instead of creating a duplicate. The response will be identical to the original.

---

## List Transfers

Retrieve a paginated list of transfers from the linked wallet.

```
GET /v1/checkout/transfer
```

### Authentication

Secret key required. `transfers` permission required.

### Query Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `page` | integer | 1 | Page number |
| `page_size` | integer | 20 | Items per page (max 100) |
| `status` | string | — | Filter by status: `pending`, `completed`, `failed` |
| `type` | string | — | Filter by type: `bank_transfer`, `payid` |
| `search` | string | — | Search by reference, recipient name, or account number |

### Example Request

::: code-group

```bash [cURL]
curl "https://api.zevpaycheckout.com/v1/checkout/transfer?page=1&page_size=10&status=completed" \
  -H "Authorization: Bearer sk_live_your_secret_key"
```

```javascript [Node.js]
const params = new URLSearchParams({
  page: '1',
  page_size: '10',
  status: 'completed',
});

const response = await fetch(
  `https://api.zevpaycheckout.com/v1/checkout/transfer?${params}`,
  {
    headers: {
      'Authorization': 'Bearer sk_live_your_secret_key',
    },
  },
);
const { data } = await response.json();
console.log(`${data.total} transfers, page ${data.page}/${data.total_pages}`);
```

:::

### Response

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "reference": "ZVP-TRF-abc123def456",
        "public_id": "xK9mQ2pL4vR7nW3jY",
        "type": "bank_transfer",
        "status": "completed",
        "amount": 500000,
        "fees": 2625,
        "vat": 197,
        "stamp_duty": 0,
        "currency": "NGN",
        "narration": "Vendor payment",
        "recipient": {
          "name": "JOHN DOE",
          "bank": "Guaranty Trust Bank",
          "account_number": "0123456789"
        },
        "merchant_reference": "payout_001",
        "metadata": null,
        "created_at": "2026-03-08T14:30:00.000Z",
        "completed_at": "2026-03-08T14:30:02.000Z"
      }
    ],
    "total": 42,
    "page": 1,
    "page_size": 10,
    "total_pages": 5
  }
}
```

---

## Get Transfer

Retrieve a single transfer by its reference.

```
GET /v1/checkout/transfer/:reference
```

### Authentication

Secret key required. `transfers` permission required.

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `reference` | string | The transfer reference |

### Example Request

::: code-group

```bash [cURL]
curl https://api.zevpaycheckout.com/v1/checkout/transfer/ZVP-TRF-abc123def456 \
  -H "Authorization: Bearer sk_live_your_secret_key"
```

```javascript [Node.js]
const reference = 'ZVP-TRF-abc123def456';
const response = await fetch(
  `https://api.zevpaycheckout.com/v1/checkout/transfer/${reference}`,
  {
    headers: {
      'Authorization': 'Bearer sk_live_your_secret_key',
    },
  },
);
const { data } = await response.json();
console.log(data.status);
```

:::

### Response

Returns the same [transfer object](#response-fields) as the initiate endpoint.

---

## Verify Transfer

Check the current status of a transfer. For bank transfers, this queries the banking provider for the latest status.

```
GET /v1/checkout/transfer/:reference/verify
```

### Authentication

Secret key required. `transfers` permission required.

### Path Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `reference` | string | The transfer reference |

### Example Request

::: code-group

```bash [cURL]
curl https://api.zevpaycheckout.com/v1/checkout/transfer/ZVP-TRF-abc123def456/verify \
  -H "Authorization: Bearer sk_live_your_secret_key"
```

```javascript [Node.js]
const reference = 'ZVP-TRF-abc123def456';
const response = await fetch(
  `https://api.zevpaycheckout.com/v1/checkout/transfer/${reference}/verify`,
  {
    headers: {
      'Authorization': 'Bearer sk_live_your_secret_key',
    },
  },
);
const { data } = await response.json();
console.log(data.status); // "completed" | "pending" | "failed"
```

:::

### Response

Returns the same [transfer object](#response-fields) as the initiate endpoint, with the most up-to-date status.

::: tip
Use the verify endpoint after receiving a `transfer.failed` webhook to confirm the final status, or to poll pending transfers that haven't received a webhook yet.
:::

---

## Errors

| Status | Code | Description |
|--------|------|-------------|
| 400 | Bad Request | Invalid parameters (missing fields, invalid amount range) |
| 400 | Bad Request | Transfer not found (wrong reference) |
| 400 | Bad Request | Insufficient balance |
| 400 | Bad Request | Self-transfer not allowed (PayID transfers) |
| 403 | Forbidden | Secret key required |
| 403 | Forbidden | API key missing `transfers` permission |
| 403 | Forbidden | No transfer wallet linked to API key |
| 403 | Forbidden | Programmable debit disabled on wallet |
| 403 | Forbidden | Wallet does not belong to this business |

## Try it — List Banks

<ApiPlayground
  method="GET"
  endpoint="/v1/checkout/transfer/banks"
  authType="bearer"
/>

## Try it — Resolve Bank Account

<ApiPlayground
  method="POST"
  endpoint="/v1/checkout/transfer/banks/resolve"
  authType="bearer"
  :bodyFields="[
    { name: 'account_number', type: 'string', required: true, placeholder: '0123456789', description: '10-digit account number' },
    { name: 'bank_code', type: 'string', required: true, placeholder: '058', description: 'Bank code from List Banks' },
  ]"
/>

## Try it — Get Balance

<ApiPlayground
  method="GET"
  endpoint="/v1/checkout/transfer/balance"
  authType="bearer"
/>

## Try it — Initiate Transfer

<ApiPlayground
  method="POST"
  endpoint="/v1/checkout/transfer"
  authType="bearer"
  :bodyFields="[
    { name: 'type', type: 'select', required: true, options: [{ label: 'Bank Transfer', value: 'bank_transfer' }, { label: 'PayID', value: 'payid' }] },
    { name: 'account_number', type: 'string', placeholder: '0123456789', description: 'For bank_transfer' },
    { name: 'bank_code', type: 'string', placeholder: '058', description: 'For bank_transfer' },
    { name: 'account_name', type: 'string', placeholder: 'JOHN DOE', description: 'For bank_transfer' },
    { name: 'pay_id', type: 'string', placeholder: 'john', description: 'For payid' },
    { name: 'amount', type: 'integer', required: true, placeholder: '500000', description: 'Amount in kobo' },
    { name: 'narration', type: 'string', placeholder: 'Payment description' },
    { name: 'reference', type: 'string', placeholder: 'payout_001', description: 'Idempotency reference' },
  ]"
/>

## Try it — List Transfers

<ApiPlayground
  method="GET"
  endpoint="/v1/checkout/transfer"
  authType="bearer"
  :queryParams="[
    { name: 'page', type: 'integer', placeholder: '1', default: '1' },
    { name: 'page_size', type: 'integer', placeholder: '20' },
    { name: 'status', type: 'select', options: [{ label: 'Pending', value: 'pending' }, { label: 'Completed', value: 'completed' }, { label: 'Failed', value: 'failed' }] },
    { name: 'type', type: 'select', options: [{ label: 'Bank Transfer', value: 'bank_transfer' }, { label: 'PayID', value: 'payid' }] },
    { name: 'search', type: 'string', placeholder: 'Search by reference, name, or account' },
  ]"
/>
