# 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

| Role | Capabilities |
|------|-------------|
| **Admin** | Initiate transfers **and** manage the wallet via REST API (add/remove members, update settings). Only the wallet owner can promote an API key to admin. |
| **Member** | Initiate transfers only. Cannot perform wallet management operations. |

#### Spending Limits

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

| Limit | Description |
|-------|-------------|
| **Single transaction limit** | Maximum amount per individual transfer |
| **Daily limit** | Maximum total amount transferred in a rolling 24-hour window |
| **Monthly limit** | Maximum 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](/api/wallet#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](/api/transfer) endpoints.

If the API key has an **admin** role, it can also manage the wallet programmatically via the [Wallet Management API](/api/wallet).

## 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](/webhooks/events#transfer-success).

## 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](/api/wallet#remove-member).

## 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](/api/transfer#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

| Role | Can manage |
|------|-----------|
| **Owner** | Everyone (admins + members) |
| **Admin** | Members only (not other admins or the owner) |
| **Member** | No management access |

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

### Available Operations

| Operation | Description |
|-----------|-------------|
| Get wallet details | View wallet info, balance, members, and settings |
| Update wallet settings | Change name, description, notifications, and privacy |
| List members | View all people and API key members |
| Add a member | Add a ZevPay user by their PayID |
| Remove a member | Remove a member (members only, not admins or owner) |
| Update member settings | Change 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](/api/wallet).
