Skip to content

Transfers

Programmable transfers let you send money from a ZevPay wallet to bank accounts or other ZevPay users via API. This is useful for payouts, vendor payments, refunds, and any scenario where you need to move funds programmatically.

How It Works

Unlike checkout (which collects money in), transfers move money out of your wallet. Because of this, transfers have additional security layers:

  1. Wallet-level consent — You must explicitly enable "Programmable Debit" on a wallet in your business dashboard
  2. API key permissions — The API key must have the transfers permission
  3. Fixed source — Each API key is linked to exactly one wallet. The transfer always debits from that wallet — you cannot override the source per request
  4. Secret key only — All transfer endpoints require a secret key (sk_*)

Setup

Step 1: Enable Programmable Debit on a Wallet

In the ZevPay Business Dashboard:

  1. Navigate to Wallets and select the wallet you want to use for transfers
  2. Open Settings on the wallet detail page
  3. Under the Developer API section, toggle Programmable Debit to enabled
  4. Read and confirm the consent dialog

WARNING

Enabling programmable debit allows API keys to debit this wallet without PIN confirmation. Only enable this on wallets designated for API payouts. Disabling it later will immediately unlink all connected API keys.

Step 2: Add an API Key as a Wallet Member

Once programmable debit is enabled, you can add API keys to the wallet from the Members tab:

  1. Go to the wallet detail page and open the Members tab
  2. Click the Add dropdown and select Add API Key
  3. Search for and select the API key you want to link
  4. Confirm — the API key is added as a member by default

INFO

A wallet can have multiple API key members. Each API key can only be linked to one transfer wallet at a time. The transfers permission is automatically granted when linked.

Step 3: Configure the API Key's Role and Limits

After adding an API key, tap on it in the members list to configure its settings:

Role

RoleCapabilities
AdminInitiate transfers and manage the wallet via REST API (add/remove members, update settings). Only the wallet owner can promote an API key to admin.
MemberInitiate transfers only. Cannot perform wallet management operations.

Spending Limits

Set per-key spending limits to control how much an API key can transfer:

LimitDescription
Single transaction limitMaximum amount per individual transfer
Daily limitMaximum total amount transferred in a rolling 24-hour window
Monthly limitMaximum total amount transferred in a rolling 30-day window

Amounts are in naira (not kobo). For example, setting a daily limit of 50000 means the API key cannot transfer more than ₦50,000 per day.

TIP

Use spending limits to minimize risk. For example, a payout key that only needs to send small amounts should have a low single transaction limit. A batch payout key might have a higher daily limit but still benefit from a monthly cap.

Balance Visibility

You can hide the wallet balance from an API key. When enabled, the API key cannot view the wallet's available balance via the Get Wallet endpoint. Admin API keys always have balance visibility — this setting only applies to member-role keys.

Step 4: Start Making Transfers

Your API key is now ready to initiate transfers. Use the Transfer API endpoints.

If the API key has an admin role, it can also manage the wallet programmatically via the Wallet Management API.

Bank Transfer Flow

The recommended flow for sending a bank transfer:

1. Get bank list

Fetch the list of supported banks to populate your UI or validate bank codes.

javascript
const { data } = await fetch('/v1/checkout/transfer/banks', {
  headers: { 'Authorization': `Bearer ${secretKey}` },
}).then(r => r.json());

// data.banks = [{ bankCode: "044", bankName: "Access Bank" }, ...]

2. Resolve bank account

Verify the account number and get the account holder's name. Always show this to the user for confirmation before sending.

javascript
const { data } = await fetch('/v1/checkout/transfer/banks/resolve', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${secretKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    account_number: '0123456789',
    bank_code: '058',
  }),
}).then(r => r.json());

// data.account_name = "JOHN DOE"

3. Check charges (optional)

Calculate fees before initiating so you know the total debit amount.

javascript
const { data } = await fetch('/v1/checkout/transfer/charges', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${secretKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: 500000,    // ₦5,000 in kobo
    bank_code: '058',
  }),
}).then(r => r.json());

// data = { amount: 500000, fee: 2625, vat: 197, stamp_duty: 0, total: 502822 }

4. Initiate transfer

javascript
const { data } = await fetch('/v1/checkout/transfer', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${secretKey}`,
    '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 - March',
    reference: 'payout_vendor_001',
  }),
}).then(r => r.json());

// data.status = "completed" or "pending"
// data.reference = "ZVP-TRF-..."

5. Handle webhook

Listen for transfer.success or transfer.failed webhooks to confirm the outcome. See Webhook Events.

PayID Transfer Flow

PayID transfers are simpler — no bank resolution or fee calculation needed. PayID transfers are instant and free.

javascript
const { data } = await fetch('/v1/checkout/transfer', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${secretKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'payid',
    pay_id: 'john',
    amount: 250000,    // ₦2,500 in kobo
    narration: 'Freelance payment',
  }),
}).then(r => r.json());

// PayID transfers complete instantly
// data.status = "completed"

Handling Webhooks

Set up a webhook endpoint to receive transfer status updates:

javascript
app.post('/webhooks/zevpay', (req, res) => {
  const { event, data } = req.body;

  switch (event) {
    case 'transfer.success':
      console.log(`Transfer ${data.reference} completed`);
      // Update your records
      markPayoutComplete(data.reference, data.amount);
      break;

    case 'transfer.failed':
      console.log(`Transfer ${data.reference} failed`);
      // Funds are returned to your wallet automatically
      markPayoutFailed(data.reference);
      // Consider retrying or notifying your team
      break;
  }

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

INFO

When a bank transfer fails, the debited amount is automatically reversed back to your wallet. You do not need to handle the reversal manually.

Checking Balance

Monitor your wallet balance to ensure you have sufficient funds:

javascript
const { data } = await fetch('/v1/checkout/transfer/balance', {
  headers: { 'Authorization': `Bearer ${secretKey}` },
}).then(r => r.json());

const balanceNaira = data.available_balance / 100;
console.log(`Available: ₦${balanceNaira.toLocaleString()}`);

Revoking Access

Disable programmable debit

Disabling programmable debit on a wallet (from Settings → Developer API) automatically:

  • Removes all API key members from that wallet
  • Revokes the transfers permission from those keys
  • Takes effect immediately

Remove a single API key

To revoke access for one API key without affecting others:

  1. Go to the wallet's Members tab
  2. Find the API key in the list and open its settings
  3. Remove it from the wallet

Other API keys linked to the same wallet continue to work normally.

Via the API

Admin API keys can also remove members programmatically using DELETE /v1/checkout/wallet/members/:payId. See Wallet Management API.

Best Practices

Use idempotency references

Always provide a reference when initiating transfers. If your system retries a request (due to network issues), the API will return the existing transfer instead of creating a duplicate.

javascript
// Good — idempotent
{ reference: "payout_vendor_001_march", amount: 500000, ... }

// Risky — no reference means retries could create duplicates
{ amount: 500000, ... }

Store references

Save both your reference and the ZevPay reference returned in the response. You'll need them for:

  • Looking up transfers via the API
  • Matching webhook events to your records
  • Debugging with ZevPay support

Verify before sending

Always call the Resolve Bank Account endpoint and show the account name to your user before initiating a bank transfer. This prevents sending money to the wrong account.

Monitor balance

Check your wallet balance before initiating transfers to avoid insufficient balance errors. Consider setting up alerts when your balance drops below a threshold.

Secure your keys

  • Never expose secret keys in client-side code
  • Configure IP whitelisting in your API key settings
  • Use separate API keys for transfers and checkout
  • Rotate keys periodically

Wallet Management via API

API keys linked to a wallet with an admin role can manage the wallet programmatically — adding or removing members, updating member settings, and changing wallet settings.

Permission Hierarchy

RoleCan manage
OwnerEveryone (admins + members)
AdminMembers only (not other admins or the owner)
MemberNo management access

API keys are linked as either admin or member. Only admin API keys can use the wallet management endpoints.

Available Operations

OperationDescription
Get wallet detailsView wallet info, balance, members, and settings
Update wallet settingsChange name, description, notifications, and privacy
List membersView all people and API key members
Add a memberAdd a ZevPay user by their PayID
Remove a memberRemove a member (members only, not admins or owner)
Update member settingsChange spending limits, notifications, and balance visibility

Example: Add a Member

javascript
const { data } = await fetch('/v1/checkout/wallet/members', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${secretKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    pay_id: '@jane.personal',
  }),
}).then(r => r.json());

Example: Set Spending Limits on a Member

javascript
await fetch('/v1/checkout/wallet/members/@jane.personal', {
  method: 'PATCH',
  headers: {
    'Authorization': `Bearer ${secretKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    has_daily_limit: true,
    daily_limit: 50000,       // ₦50,000
    has_single_limit: true,
    single_limit: 10000,      // ₦10,000
  }),
});

For full endpoint details, request/response schemas, and error codes, see the Wallet Management API Reference.

ZevPay Checkout Developer Documentation