# 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 |
| --- | --- | --- | --- | --- |
| 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 |
| Botswana | `BW` | `BWP` | 9 | from BWP 151 |

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/KE \
  -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": 25000,
    "currency": "KES",
    "country": "KE",
    "transaction_channel": "bank",
    "customer": {
      "first_name": "Wanjiru",
      "last_name": "Kamau",
      "phone_number": "+254712345678",
      "email": "wanjiru@example.com",
      "country": "KE"
    },
    "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": "+254712345678",
      "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 Kenya
const options = await fetch('https://api.payd.money/api/v2/payments/discovery/KE', { 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: 25000,
    currency: 'KES',
    country: 'KE',
    transaction_channel: 'bank',
    customer: { first_name: 'Wanjiru', last_name: 'Kamau', phone_number: '+254712345678', country: 'KE' },
    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: '+254712345678',
      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 Kenya
options = requests.get("https://api.payd.money/api/v2/payments/discovery/KE", 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": 25000,
        "currency": "KES",
        "country": "KE",
        "transaction_channel": "bank",
        "customer": {"first_name": "Wanjiru", "last_name": "Kamau", "phone_number": "+254712345678", "country": "KE"},
        "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": "+254712345678",
            "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 | No | The customer's email. |
| `customer.country` | string | No | The same country code. |
| `customer.address` | object | No | `line_1`, `city`, `state`, `postal_code` and `country`. Payd fills a default for the country when you leave it out. |
| `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. |

### Nigeria

Nigerian banks need to know who is paying. Send the customer's BVN or NIN in `customer.additional_document_type` 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": "Equity Bank",
      "account_number": "1180289374615",
      "account_name": "Payd Collections",
      "branch_code": "",
      "reference": "PD7Q4MZ2K8",
      "amount": 25000,
      "currency": "KES",
      "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.
