> ## Documentation Index
> Fetch the complete documentation index at: https://dev-docs.pagoramp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# KES virtual accounts (Kenya)

> Issue KES bank details for your customers and receive payments by Bank transfer.

| | |
| - | - |
| Country | Kenya |
| Payment rail | Bank transfer |
| Account types | dedicated, dynamic |
| Customers | Individuals and businesses |
| When it is ready | Dedicated accounts are issued shortly after the request and show `pending` until then. Dynamic accounts are ready immediately. |

## 1. Check availability

Ask what KES needs for your account. The answer lists the required fields per customer type and account type.

```bash theme={null}
curl "https://pay-api.pagoramp.com/api/v1/virtual-accounts/availability?currency=KES" \
  -H "x-api-key: YOUR_API_KEY"
```

## 2. Create the customer

Required for an individual: `firstname`, `lastname`, `email`. See [Virtual account requirements](https://dev-docs.pagoramp.com/virtual-accounts-requirements#kes) for every combination.

```bash theme={null}
curl -X POST https://pay-api.pagoramp.com/api/v1/customers \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"firstname":"Ada","lastname":"Okafor","email":"ada@example.com"}'
```

The customer is created with `kycStatus` `incomplete` or `pending`. Anything missing for KES can be added later with `PATCH /customers/{uuid}` before you request the account.

## 3. Request the account

```bash theme={null}
curl -X POST https://pay-api.pagoramp.com/api/v1/virtual-accounts \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"224ca5c5-f2a6-419b-bf36-73997434d945","currency":"KES","type":"dedicated","reference":"va_1001"}'
```

You must have a webhook secret set up first, or the request returns `400` with `webhook_secret_required`.

The response depends on the currency. For KES, expect `pending` then `active`:

**`pending`**: The account is still being issued or approved. Wait for `virtual_account.created`.

```json theme={null}
{
  "status": "success",
  "message": "Virtual account created successfully",
  "data": {
    "uuid": "9a1c1c52-7b8e-4a52-9f0e-3e6b0a8d6c11",
    "reference": "va_1001",
    "status": "pending",
    "currency": "KES",
    "type": "dedicated",
    "bankDetails": null,
    "customer": {
      "uuid": "224ca5c5-f2a6-419b-bf36-73997434d945",
      "email": "ada@example.com"
    },
    "expectedAmount": null,
    "expiresAt": null,
    "nextStep": null,
    "statusReason": null,
    "metadata": {
      "orderId": "12345"
    },
    "createdAt": "2026-10-16T09:31:00.000Z"
  }
}
```

**`active`**: Ready. Show the bank details to your payer.

```json theme={null}
{
  "status": "success",
  "message": "Virtual account created successfully",
  "data": {
    "uuid": "9a1c1c52-7b8e-4a52-9f0e-3e6b0a8d6c11",
    "reference": "va_1001",
    "status": "active",
    "currency": "KES",
    "type": "dedicated",
    "bankDetails": {
      "rail": "bank_transfer",
      "accountName": "ACME PAY / ADA OKAFOR",
      "accountNumber": "3977950118",
      "bankName": "KCB Bank",
      "bankCode": "000000"
    },
    "customer": {
      "uuid": "224ca5c5-f2a6-419b-bf36-73997434d945",
      "email": "ada@example.com"
    },
    "expectedAmount": null,
    "expiresAt": null,
    "nextStep": null,
    "statusReason": null,
    "metadata": {
      "orderId": "12345"
    },
    "createdAt": "2026-10-16T09:31:00.000Z"
  }
}
```

**Dynamic account** (`type: "dynamic"`, `amount`, optional `expiresIn`):

```json theme={null}
{
  "status": "success",
  "message": "Virtual account created successfully",
  "data": {
    "uuid": "9a1c1c52-7b8e-4a52-9f0e-3e6b0a8d6c11",
    "reference": "va_1001",
    "status": "active",
    "currency": "KES",
    "type": "dynamic",
    "bankDetails": {
      "rail": "bank_transfer",
      "accountName": "ACME PAY / ADA OKAFOR",
      "accountNumber": "1234567890",
      "bankName": "KCB Bank",
      "extra": {
        "note": "Pay the exact amount"
      }
    },
    "customer": {
      "uuid": "224ca5c5-f2a6-419b-bf36-73997434d945",
      "email": "ada@example.com"
    },
    "expectedAmount": 5000,
    "expiresAt": "2026-10-16T11:31:00.000Z",
    "nextStep": null,
    "statusReason": null,
    "metadata": {
      "orderId": "12345"
    },
    "createdAt": "2026-10-16T09:31:00.000Z"
  }
}
```

Create with `type: "dynamic"`, `amount` and optionally `expiresIn`. A payment of a different amount is held for review as an underpayment or overpayment.

If the request is rejected straight away you get `422` with the reason in `errors[0].reason` and no account is created. If it is rejected later you receive `virtual_account.failed` with a `reason`. See [Webhooks](https://dev-docs.pagoramp.com/webhooks#account-status-events).

## 4. Show the details to your payer

Show these fields from `bankDetails`:

| Label | Field |
| - | - |
| Account name | `accountName` |
| Account number | `accountNumber` |
| Bank | `bankName` |

How your payer sends the money:

1. Make a bank transfer to the account number shown, in Kenyan shillings.

## 5. Receive the payment

When the payment arrives you receive `virtual_account.payment.received`. Amounts are decimal strings in KES; `destinationAmount` is in USD, after any fees.

```json theme={null}
{
  "event": "virtual_account.payment.received",
  "data": {
    "transaction": {
      "reference": "va-17912345678901234",
      "status": "Success",
      "sourceAmount": "5000.00",
      "sourceCurrency": "KES",
      "destinationAmount": "38.46",
      "destinationCurrency": "USD",
      "fees": {
        "sourceTotalFee": "0.00"
      }
    },
    "virtualAccount": {
      "uuid": "9a1c1c52-7b8e-4a52-9f0e-3e6b0a8d6c11",
      "reference": "va_1001",
      "currency": "KES",
      "type": "dedicated"
    },
    "customer": {
      "uuid": "224ca5c5-f2a6-419b-bf36-73997434d945",
      "email": "ada@example.com",
      "externalReference": null
    },
    "sender": {
      "name": "ADA OKAFOR",
      "bank": "Equity Bank",
      "accountNumber": "******6789"
    },
    "occurredAt": "2026-10-16T10:02:00.000Z"
  }
}
```

Verify the signature before you trust it, and use `data.transaction.reference` to ignore repeats. See [Webhooks](https://dev-docs.pagoramp.com/webhooks).

## Things to handle

* A dedicated account is permanent. A dynamic account expects one exact amount and expires (7200 seconds by default, set with `expiresIn`).
* When a dedicated account is still `pending`, wait for `virtual_account.created` before showing details to your customer.
* A payment can be held for review. You receive `virtual_account.payment.manual_review` with a `review.reason`, then `received` or `rejected` after a decision.
* A dynamic account that expires sends `virtual_account.expired`. A late payment is held for review.
* `GET /virtual-accounts/{uuid}` returns the current status at any time, and `POST /virtual-accounts/{uuid}/deactivate` stops the account accepting payments.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.