Skip to main content
When a payment lands in a virtual account (or a Collect Payments link is paid), we send a POST request to your configured webhook URL instead of making you poll for it. Set your webhook URL and grab your webhook secret from Webhook Settings in the merchant dashboard.

Payload

For this event, data.reference is the bank transfer narration the payer’s own bank attached to the transfer — it is not something you sent, and it is not unique. Two completely different payments can carry identical narration text (a generic “Personal Transfer”, or the same payer transferring twice). Never use it as a lookup key or a duplicate-detection key — use transaction_id instead, below.
string
The payer’s bank transfer narration/description, exactly as their bank sent it to us. Free text, not guaranteed unique, and not something you provided. Useful only as a human-readable label to show alongside a transaction — never for matching or deduplication.
string
Ours, and unique. ABCMPay’s own internal ID for the transaction (shown as “Order Number” in the dashboard). This is the value to store and to key your duplicate-detection off.
string
Unique per delivery attempt — even a retry of the same transaction gets a new one. Covered by the signature. See Replay protection.
string
ISO 8601 timestamp of this specific delivery attempt. Also covered by the signature.

Request headers

Verifying the signature

Every payload is signed with HMAC-SHA256 using your webhook secret, over the entire raw request body (not a re-serialized version of it — hash the bytes exactly as received).
Always verify the signature before trusting a webhook. Anyone who finds your webhook URL can otherwise send you a fake payment.received event.
PHP
Use hash_equals() (or your language’s constant-time comparison) rather than == or === — a plain string comparison leaks timing information an attacker can use to guess a valid signature byte by byte.

Replay protection

A valid signature only proves the request came from us — it doesn’t stop someone from capturing and resending an old, still-valid request later. Every delivery carries a unique nonce and a sent_at timestamp, both covered by the signature above, so you can reject a resend even if its signature checks out.

Retries

If your endpoint doesn’t respond with a 2xx status, the delivery is treated as failed and may be retried or manually resent from your dashboard. A retry (or a manual resend from Webhook Events) carries the same transaction_id as the original delivery, with a fresh nonce/sent_at — this is expected and is exactly why you should key duplicate-detection off transaction_id, not reference: it’s the one field guaranteed to stay identical across every delivery attempt of the same payment, and guaranteed distinct between different payments. You can review delivery history (including HTTP status codes returned) from the Webhook Events page in your dashboard, which also shows both reference and transaction_id for each event so you can trace exactly what was sent.
Respond quickly (under a few seconds) and with a 2xx status as soon as you’ve accepted the payload — do slow work (emails, downstream API calls) after responding, not before.