Skip to main content
We send events to the webhook URL set in your dashboard. Every request is signed with your webhook secret, so you can check it came from us. You must have a webhook secret before you can create virtual accounts. Creating one without a secret returns 400 with the code webhook_secret_required.

Verifying a signature

Each request carries two headers: Compute the signature over the raw request body, before any JSON parsing, and compare it in constant time. Reject requests whose timestamp is more than five minutes old.

Delivery and retries

Answer with 200 once you have stored the event. Any other response, or a timeout, is retried up to five times with exponentially growing delays. The same event can arrive more than once, so make your handler idempotent: use data.transaction.reference as the key.

Virtual account events

All amounts are decimal strings in the account currency, except destinationAmount, which is in USD. sender.accountNumber is masked to its last four digits.

virtual_account.payment.received

virtual_account.payment.manual_review

Same shape, with transaction.status set to Manual Review and a review object:
For reasons other than amount_variance, expectedAmount and variance are null. After the review you receive either virtual_account.payment.received (approved) or virtual_account.payment.rejected (declined) for the same transaction.reference.

virtual_account.payment.rejected

Same shape as received, with transaction.status set to Rejected.

Account status events

Creating an account returns one of these statuses right away. Some change later, and you get a webhook when they do, so you never need to poll. An account that is rejected while you are creating it returns 422 straight away with the reason in errors[0].reason, and no account is created:

virtual_account.created

virtual_account.failed

Same shape with status: "failed", bankDetails: null and a reason:
The same statusReason is on the account in GET /virtual-accounts/{uuid}.

virtual_account.expired

Same shape with status: "expired", no bankDetails and no reason.

Checkout events

Checkout payments send success, failed, pending, partially_paid and the chargeback events with the existing payload, unchanged.