# Collect by bank transfer

Let a customer pay from their bank account. Payd gives you bank details for the payment: you show them to the customer, they send a transfer from their bank app or branch, and Payd confirms it.

<div class="endpoint"><span class="method post">POST</span><code>https://api.payd.money/api/v2/payments/payin</code></div>

## How it works

1. You look up the banks for the customer's country with [payment options by country](./network-discovery.md).
2. The customer picks their bank, and you send the payin with that bank's ids.
3. Payd answers with `bank_instructions`: the bank, the account number, the account name, the reference and the exact amount to send.
4. You show those details to the customer. They send the transfer before `expires_at`.
5. Payd posts the result to your `callback_url`, and the status endpoint shows `success` or `failed`.

## Where you can collect

| Country | `country` | `currency` | Banks today | Limits per payment |
| --- | --- | --- | --- | --- |
| Botswana | `BW` | `BWP` | 9 | from BWP 151 |
| Kenya | `KE` | `KES` | 44 | KES 301 to 999,999 |
| Nigeria | `NG` | `NGN` | 23 | NGN 2,501 to 30,000,000 |
| Tanzania | `TZ` | `TZS` | 35 | TZS 2,501 to 150,000,000 |

These limits are a snapshot from 16 October 2026. The API is the source of truth: read each bank's `min_amount`, `max_amount` and `settlement_seconds` from [payment options by country](./network-discovery.md) before each payment. See [limits and settlement](./limits-and-settlement.md).

## 1. Pick the bank

Fetch the options for the customer's country and keep the banks for collections:

```bash
curl https://api.payd.money/api/v2/payments/discovery/NG \
  -u "$PAYD_API_USERNAME:$PAYD_API_PASSWORD"
```

Keep networks where `channel_type` is `bank`, `ramp_type` is `deposit` and `status` is `active`. Show `network_name` to the customer, with a search box, since some countries list dozens of banks. When they choose one, keep its `network_id`, `channel_id`, `min_amount` and `max_amount`.

## 2. Send the payin

::: code-group

```bash [cURL]
curl https://api.payd.money/api/v2/payments/payin \
  -u "$PAYD_API_USERNAME:$PAYD_API_PASSWORD" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "order-8842",
    "amount": 50000,
    "currency": "NGN",
    "country": "NG",
    "transaction_channel": "bank",
    "customer": {
      "first_name": "Chinedu",
      "last_name": "Okafor",
      "phone_number": "+2348031234567",
      "email": "chinedu@example.com",
      "country": "NG",
      "document_type": "national_id",
      "document_number": "12345678901",
      "dob": "1991-07-22T00:00:00Z",
      "address": { "line_1": "14 Admiralty Way", "city": "Lagos", "country": "NG" },
      "additional_document_type": "bvn",
      "additional_document_number": "22123456789"
    },
    "vendor_config": { "account_id": "4f28836c-2f1e-4d9b-9a51-6f3c2b1d8e70" },
    "provider_config": {
      "network_id": "3f1c2a8e-5b7d-4e19-9c0a-2d6b8e4f1a73",
      "channel_id": "b8d2e6f4-1a3c-4b5d-8e7f-9a0b1c2d3e4f",
      "account_type": "bank"
    },
    "payment_config": {
      "account_number": "+2348031234567",
      "remark": "Order 8842",
      "reference": "order-8842",
      "callback_url": "https://example.com/webhooks/payd"
    }
  }'
```

```js [Node.js]
const auth = Buffer.from(
  `${process.env.PAYD_API_USERNAME}:${process.env.PAYD_API_PASSWORD}`,
).toString('base64')
const headers = { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json' }

// 1. Banks the customer can pay from in Nigeria
const options = await fetch('https://api.payd.money/api/v2/payments/discovery/NG', { headers })
  .then((r) => r.json())
const banks = Object.values(options.provider_networks ?? {})
  .flatMap((group) => group.networks)
  .filter((n) => n.channel_type === 'bank' && n.ramp_type === 'deposit' && n.status === 'active')

// 2. The customer picked one
const bank = banks.find((n) => n.network_name === customerChoice)

const res = await fetch('https://api.payd.money/api/v2/payments/payin', {
  method: 'POST',
  headers,
  body: JSON.stringify({
    idempotency_key: 'order-8842',
    amount: 50000,
    currency: 'NGN',
    country: 'NG',
    transaction_channel: 'bank',
    customer: {
      first_name: 'Chinedu',
      last_name: 'Okafor',
      phone_number: '+2348031234567',
      email: 'chinedu@example.com',
      country: 'NG',
      document_type: 'national_id',
      document_number: '12345678901',
      dob: '1991-07-22T00:00:00Z',
      address: { line_1: '14 Admiralty Way', city: 'Lagos', country: 'NG' },
      additional_document_type: 'bvn',
      additional_document_number: '22123456789',
    },
    vendor_config: { account_id: process.env.PAYD_ACCOUNT_ID },
    provider_config: { network_id: bank.network_id, channel_id: bank.channel_id, account_type: 'bank' },
    payment_config: {
      account_number: '+2348031234567',
      reference: 'order-8842',
      callback_url: 'https://example.com/webhooks/payd',
    },
  }),
  signal: AbortSignal.timeout(35_000),
})
const body = await res.json()
if (!res.ok) throw new Error(`${body.error_code}: ${body.error_message}`)

// 3. Show these to the customer
const { bank_instructions: pay, merchant_reference } = body.gateway_response
console.log(merchant_reference, pay.bank_name, pay.account_number, pay.account_name, pay.reference, pay.amount, pay.expires_at)
```

```python [Python]
import os
import requests

AUTH = (os.environ["PAYD_API_USERNAME"], os.environ["PAYD_API_PASSWORD"])

# 1. Banks the customer can pay from in Nigeria
options = requests.get("https://api.payd.money/api/v2/payments/discovery/NG", auth=AUTH, timeout=15).json()
banks = [
    n
    for group in (options.get("provider_networks") or {}).values()
    for n in group["networks"]
    if n["channel_type"] == "bank" and n["ramp_type"] == "deposit" and n["status"] == "active"
]

# 2. The customer picked one
bank = next(n for n in banks if n["network_name"] == customer_choice)

res = requests.post(
    "https://api.payd.money/api/v2/payments/payin",
    auth=AUTH,
    json={
        "idempotency_key": "order-8842",
        "amount": 50000,
        "currency": "NGN",
        "country": "NG",
        "transaction_channel": "bank",
        "customer": {
            "first_name": "Chinedu",
            "last_name": "Okafor",
            "phone_number": "+2348031234567",
            "email": "chinedu@example.com",
            "country": "NG",
            "document_type": "national_id",
            "document_number": "12345678901",
            "dob": "1991-07-22T00:00:00Z",
            "address": {"line_1": "14 Admiralty Way", "city": "Lagos", "country": "NG"},
            "additional_document_type": "bvn",
            "additional_document_number": "22123456789",
        },
        "vendor_config": {"account_id": os.environ["PAYD_ACCOUNT_ID"]},
        "provider_config": {"network_id": bank["network_id"], "channel_id": bank["channel_id"], "account_type": "bank"},
        "payment_config": {
            "account_number": "+2348031234567",
            "reference": "order-8842",
            "callback_url": "https://example.com/webhooks/payd",
        },
    },
    timeout=35,
)
res.raise_for_status()

# 3. Show these to the customer
gateway = res.json()["gateway_response"]
pay = gateway["bank_instructions"]
print(gateway["merchant_reference"], pay["bank_name"], pay["account_number"], pay["reference"], pay["amount"], pay["expires_at"])
```

:::

### Fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `idempotency_key` | string | No | Makes a resend safe. See [retries and duplicates](./core-concepts.md#retries-and-duplicates). |
| `amount` | number | Yes | Amount in the local currency, as a whole number, within the bank's limits. |
| `currency` | string | Yes | The country's currency from the table above. |
| `country` | string | Yes | The two-letter country code. |
| `transaction_channel` | string | Yes | `bank`. |
| `customer.first_name`, `customer.last_name` | string | Yes | The customer's name, as their bank knows it. |
| `customer.phone_number` | string | Yes | The customer's phone number in international form with a `+`. |
| `customer.email` | string | Yes | The customer's email. |
| `customer.country` | string | No | The same country code. |
| `customer.document_type` | string | Yes | The customer's ID: `national_id`, `passport`, `drivers_license` or `voters_card`. |
| `customer.document_number` | string | Yes | The number on that ID. |
| `customer.dob` | string | Yes | The customer's date of birth, as a timestamp such as `1991-07-22T00:00:00Z`. |
| `customer.address` | object | Yes | `line_1`, `city` and `country`, plus `state` and `postal_code` where they apply. |
| `customer.additional_document_type` | string | Nigeria | `bvn` or `nin`. |
| `customer.additional_document_number` | string | Nigeria | The customer's BVN or NIN. |
| `vendor_config.account_id` | string | Yes | Your [account id](./authentication.md#find-your-account-id). |
| `provider_config.network_id` | string | Yes | `network_id` of the bank the customer chose. |
| `provider_config.channel_id` | string | Yes | `channel_id` of that bank, from its `deposit` entry. |
| `provider_config.account_type` | string | Yes | `bank`. |
| `payment_config.account_number` | string | Yes | The customer's phone number again, in the same form. |
| `payment_config.reference` | string | No | Your own id for the payment. |
| `payment_config.callback_url` | string | No | Where Payd posts the result. See [webhooks](./webhooks.md). |
| `payment_config.remark` | string | No | A short note kept with the payment. |

### Who is paying

Bank collections check who sends the money, so every bank payin carries the customer's ID, date of birth, email and address, as in the example. Ask for them on your checkout before you send the payin.

### Nigeria

Nigerian banks also need the customer's BVN or NIN, in `customer.additional_document_type` (`bvn` or `nin`) and `customer.additional_document_number`. Without them, Payd refuses the payin with a message that names `additional_id_type`.

## Response

```json
{
  "gateway_response": {
    "success": true,
    "merchant_reference": "9BD103350517eR",
    "checkout_request_id": "c41f9a0e-7b2d-4c8e-a1f3-5d6e7f8a9b0c",
    "message": "request sent",
    "bank_instructions": {
      "bank_name": "Providus Bank",
      "account_number": "9912345678",
      "account_name": "Payd Collections",
      "branch_code": "",
      "reference": "PD7Q4MZ2K8",
      "amount": 50000,
      "currency": "NGN",
      "expires_at": "2026-10-17T11:42:03Z",
      "checkout_url": ""
    }
  }
}
```

| Field | Meaning |
| --- | --- |
| `merchant_reference` | Payd's reference for the payment. Store it, and use it to [check the status](./transaction-status.md). |
| `bank_instructions.bank_name` | The bank the customer sends money to. |
| `bank_instructions.account_number` | The account to pay. It can be a one-off account made for this payment, so never reuse it for another order. |
| `bank_instructions.account_name` | The name on that account. |
| `bank_instructions.branch_code` | The branch code, where the bank needs one. Often empty. |
| `bank_instructions.reference` | The reference the customer must put on the transfer. Show it clearly. |
| `bank_instructions.amount`, `currency` | The exact amount to send. |
| `bank_instructions.expires_at` | Pay before this time. After it, the details stop working. |
| `bank_instructions.checkout_url` | When set, send the customer to this page to pay instead of showing account details. |

If `success` is `false` or `bank_instructions` is missing, there is nothing to show. Log `message` and let the customer try again or pick another way to pay.

## Show the details

Show every field the customer needs on one screen, with a copy button next to the account number and the reference:

- Ask them to send exactly `amount`. A transfer for a different amount can fail or be held. Email [support](mailto:support@paydhq.com) with the `merchant_reference` to sort it out. If Payd settles a different amount, the webhook and the status show the amount that arrived.
- Ask them to use the reference, or the bank cannot match the transfer to the payment.
- Show `expires_at` in their time zone, and tell them to pay before then.

## Confirm the payment

Bank transfers are not instant. Payd checks for the transfer and posts `payin.succeeded` or `payin.failed` to your `callback_url` when it knows. See [webhooks](./webhooks.md).

- Most transfers confirm within minutes. Some Kenyan banks take up to two days. See [limits and settlement](./limits-and-settlement.md).
- If no webhook arrives, [check the status](./transaction-status.md) every few minutes for the first hour, then every hour.
- If the payment is still `pending` after `expires_at`, treat it as unpaid and ask the customer before you try again.

## Common errors

| `error_code` | Message | Fix |
| --- | --- | --- |
| `invalid_argument` | names a missing field | Send the field. `provider_config.network_id`, `channel_id` and `account_type` are all needed. |
| `invalid_argument` or `provider_error` | names `additional_id_type` | Send the customer's BVN or NIN. See [Nigeria](#nigeria). |
| `provider_error` | the network's reason | Usually an amount outside the bank's limits. Check `min_amount` and `max_amount`. |
| `resource_not_found` | `no pricing configured...` | Payd does not offer bank collections in this country and currency. Check the [options](./network-discovery.md) for the country. |

See [errors](./errors.md) for every code.
