Webhook events
FastPay sends a signed HTTPS request to your server whenever something happens to a payment, so you do not have to keep asking.
Set up an endpoint#
Add your webhook address in Settings → API Keys in the dashboard. In production it must be an https address. Addresses that point at private or internal networks are refused. The signing secret is shown once when you create the endpoint or rotate its secret, so store it safely.
Event format#
Every delivery is a POST with a JSON body:
{
"id": "0192f3a1-5d0b-7f7e-9a51-3c7c2f0d8e11",
"type": "payment.succeeded",
"version": "2026-01",
"created_at": "2026-10-28T09:30:00Z",
"data": {
"payment_id": "6f1c2a52-5d0b-4f7e-9a51-3c7c2f0d8e11",
"status": "succeeded",
"amount_minor": 500000,
"currency": "NGN",
"reference": "order-1001",
"metadata": { "cart_id": "c_42" }
}
}id: the event's unique id. It stays the same across every retry, so use it to ignore duplicates.type: what happened.version: the version of the payload format.created_at: when it happened.data: the details. Money is always an integer in the smallest unit, with itscurrency.
Events#
payment.succeededThe payment was completed.payment.failedThe payment did not go through.payment.cancelledThe payment was cancelled before it completed.payment.expiredThe customer did not pay in time.virtual_account.collection.receivedA transfer into a dedicated account arrived.virtual_account.collection.postedThat transfer was credited to you.virtual_account.collection.exceptionThat transfer is held for review.virtual_account.closedA dedicated account was closed.crypto.payment.receivedCoin arrived for a crypto payment.crypto.payment.confirmedThat coin was credited to your balance.crypto.payment.heldThat coin is held for review.crypto.withdrawal.requestedA confirmed withdrawal left your balance.crypto.withdrawal.sentThe withdrawal was sent.crypto.withdrawal.failedThe withdrawal failed and is back in your balance.
Payment events carry payment_id, status, amount_minor, currency and metadata, and, where they apply, reference, a failure_code and failure_message. By default an endpoint receives every event; you can choose which types it subscribes to.
reference: the checkout's reference: yours, as you sent it when you created the checkout, or the one generated (startingfp_) when you sent none.metadata: the metadata you sent with the checkout, unchanged. It is{}when you sent none. A checkout opened by a storefront, a payment link or an invoice carries asourceset by the platform, such as{"source": "invoice"}.
Since 6 Nov 2026 payment events carry reference and metadata, so you can match an event to your order without another call. Nothing was renamed or removed.
Transfer events carry collection_id, status, amount_minor and currency, and virtual_account_id when the transfer matched one of your accounts. They also carry fee_minor, vat_minor and charge_minor: what is taken from the amount, zero until the transfer is settled (added on 9 Nov 2026). Their provider and provider_transaction_id are deprecated and will be removed: do not rely on them.
virtual_account.closed carries virtual_account_id, status (closed) and closed_at, and, where they apply, your customer_ref, the bank_name and the bank_code. It is sent once, whether the account was closed through the API or the dashboard. Added on 9 Nov 2026.
Headers#
Fastpay-Webhook-IdThe event id.Fastpay-Webhook-EventThe event type.Fastpay-Webhook-SignatureThe signature to verify.
Respond and retry#
Return a 2xx as soon as you have received the event. Anything else counts as a failure:
408,425,429,5xx, timeouts and connection errors are retried for 24 hours: after 30 seconds, then 1, 2, 4, 8, 16 and 32 minutes, then every hour, with a last try when the 24 hours end. Each wait can be up to a quarter longer, so retries are spread out.- Other
4xxresponses and redirects are permanent failures and are not retried. Redirects are never followed. The delivery history says so on the attempt.
A delivery still failing after 24 hours is kept in your delivery history in the dashboard, with every attempt, never discarded, and you can replay it from there. A replay is retried for another 24 hours.