How Paystack webhooks work
When something happens on your account, Paystack sends a POST to the webhook URL in Settings → API Keys & Webhooks. The body is {"event": "...", "data": {...}}. For a payment the event is charge.success, anddata.reference is the reference you chose when you created the charge. For a payout it is transfer.success,transfer.failed or transfer.reversed.
Every request carries x-paystack-signature: an HMAC SHA512 of the body, signed with your secret key. There is no separate webhook secret. Test-mode webhooks are signed with your test key, live ones with your live key. Amounts are in the currency's subunit: 2550 is 25.50 GHS.
Five rules for a safe handler
- Check the signature over the raw body. Parse the JSON only after the check; refuse (401) if it fails.
- Don't trust the body. Read only
data.reference, then callGET /transaction/verify/:reference(or/transfer/verify/:reference). - Check the verified transaction. The reference must be yours,
statusmust besuccess, the currency must match, and the amount must equal your order in subunits. - Answer 200 quickly. Paystack retries until it gets one: every 3 minutes for the first 4 tries in live mode, then hourly for 72 hours.
- Be idempotent. Retries repeat events. Update an order only while it is still pending.
Paystack sends charge webhooks only for successful payments, so don't wait for a failure webhook. Check pending orders from a scheduled job with the verify endpoint. For payouts, remember that a successful transfer can still be reversed later.
Common problems
- The signature never matches. A JSON middleware parsed the body first. In Express use
express.raw()on the webhook route; in Laravel use$request->getContent(); in Django userequest.body. - It matched in test mode but not live. Your server still uses the test key. Live webhooks are signed with the live key.
- The amount is 100 times too small. The charge was created in major units. Send
amountin subunits. - Webhooks stop arriving. Your endpoint returns an error or redirects. Check that it answers 200 over HTTPS.
Facts from Paystack's documentation (Webhooks, Transfers and Verify transaction), checked November 2026. The samples are illustrative: their shapes follow Paystack's verify responses.
Frequently asked questions
How do I verify a Paystack webhook signature?
Compute an HMAC SHA512 of the raw request body, keyed with your secret key, as hex. Compare it with the x-paystack-signature header using a constant-time comparison, and refuse the request (401) if they differ. Hash the exact bytes you received, not a parsed and re-serialised copy.
Why does my x-paystack-signature never match?
Usually because the body was parsed before the check: express.json(), a framework middleware or a JSON re-encode changes the bytes. Read the raw body on the webhook route. The other common cause is using the secret key of the other mode: test webhooks are signed with your test key, live ones with your live key.
Does Paystack send a webhook when a payment fails?
Not for charges: Paystack sends charge webhooks only for successful payments (charge.success). Check pending orders from a scheduled job with GET /transaction/verify/:reference so failed and abandoned ones get settled. Transfers are different: transfer.failed and transfer.reversed are sent.
How often does Paystack retry a webhook?
Until your server answers 200. In live mode, every 3 minutes for the first 4 tries, then hourly for the next 72 hours. In test mode, hourly for 72 hours. Answer 200 first and do slow work after, and make your handler idempotent.
Is it safe to paste my secret key here?
Only a test key, and only if you want the signature checked. The HMAC is computed in your browser with Web Crypto; nothing is sent or saved. The page refuses live keys (sk_live_...). Leave the key empty and every other check still works.
Related
- Paystack mobile money with Node.js: MTN, Telecel, AT and M-Pesa charges, signed webhooks, verify and reconcile.
- Paystack mobile money with Laravel and with Django.
- Paystack mobile money payouts: transfers that never pay twice, and reversals.
- African Phone Number Formatter: get Ghanaian and Kenyan numbers into the right format.
- Flutterwave Webhook Checker: the same checks for Flutterwave.