Skip to main content

1. Check availability

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

2. Create the customer

Required for an individual: firstname, lastname, email. See Virtual account requirements for every combination.
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

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.
active: Ready. Show the bank details to your payer.
Dynamic account (type: "dynamic", amount, optional expiresIn):
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.

4. Show the details to your payer

Show these fields from bankDetails: 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.
Verify the signature before you trust it, and use data.transaction.reference to ignore repeats. See 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.