Mantissa Hooksnode is a Mantissa product A Mantissa product mantissatech.xyz

Why your Paystack webhook is not working (and how to fix it)

Author:
hooksnode
Published:
Updated:
  • paystack
  • webhooks
  • troubleshooting

The customer paid, Paystack shows a successful transaction, and your server has no record of it. Or the event is in the dashboard as failed or pending. Work through the causes below in order. Most of them show up in the delivery record before you change code.

The Paystack facts below come from Paystack's webhook docs and the dashboard webhook history article, as published when this page was written on 2026-11-06. Amounts in subunits are from Paystack's API reference.

Check the delivery record first

On the new Paystack dashboard, open Developers, then Webhooks, and select the same mode as the payment.

  • Test mode and Live mode keep separate webhook URLs and separate event history. An event that is missing is often sitting in the other mode.
  • Delivered means your server returned a 2xx. Paystack does not know whether your app then did the work.
  • Pending means Paystack is still processing the event or waiting for an automatic retry.
  • Failed means Paystack did not get a successful response. The detail view shows the URL, the payload, the HTTP status, and your response body.

The Retry button sends the event again to the webhook URL currently saved for that mode. That URL can differ from the one used on the first attempt. Retry is not idempotent: if you already applied the payment, a second delivery applies it again unless your handler dedupes on data.reference.

Paystack also has a Webhook Events API to list events, fetch one, and resend. Resend goes to the current URL for that destination and is not idempotent. Pass preview=true before resending every event that matches a filter, so you can see the count first.

Cause 1: wrong mode or wrong URL

Test and live do not share a webhook URL. Confirm both of these before you touch the handler:

  • The dashboard is on the mode that took the payment.
  • The URL saved for that mode is the public URL of the server you are watching.

Localhost cannot receive events. A URL copied from the other mode, or from an old deploy, fails the same way as a down server: the history shows failed, or the event never appears because you are looking at the wrong mode.

If no URL is saved for the mode, Retry stays unavailable until you add one and save.

Cause 2: the handler is too slow

Return 200 as soon as you have checked the signature and stored the event. Do emails, ledger writes, and other slow work after that, in a queue.

Paystack states two different time limits. Do not treat them as one number.

  • Test mode deliveries time out at 30 seconds. The docs state that timeout on the test mode retry schedule, not as a live mode figure.
  • The endpoint ping expects a response within 5 seconds, including 3 seconds to connect. That check belongs to the webhook endpoints ping (see cause 6). It is not the same limit as the 30 second test mode delivery timeout.

A handler that only answers after it finishes the business work will miss the shorter of those limits under load, and Paystack will mark the attempt failed.

Cause 3: a redirect

Paystack does not follow redirects. These all fail even when the page they point at would return 200:

  • http:// redirected to https://
  • a trailing slash added or removed
  • the bare domain redirected to www, or the other way around

If you use .htaccess, include the trailing / in the webhook URL you save. Paste the final URL into the dashboard. curl -si -X POST against that URL should not start with HTTP/1.1 301 or any other 3xx.

Cause 4: a firewall, WAF, or host block

Paystack sends webhooks only from these addresses, in both test and live mode:

  • 52.31.139.75
  • 52.49.173.169
  • 52.214.14.220

Allow those three if you filter by IP. A shared-host WAF or ModSecurity rule can still block the POST even when the IP is allowed: the request reaches a request bin and never reaches your app. Compare the HTTP status in the Paystack event detail with your own access log. If Paystack recorded no response (http_status null on an endpoint ping), the docs point at DNS, TLS, the firewall, a redirect, or a server that did not answer the ping within 5 seconds, including 3 seconds to connect.

Cause 5: the signature check fails

Verify the signature on the raw body, before any JSON parse. Parsing and serializing again changes spaces, key order, and numbers such as 1.50, so a real event fails the check.

Paystack can send two headers. Check the one the event actually has. The full check is in Verify Paystack webhook signatures (Paystack-Signature and x-paystack-signature).

  • Paystack-Signature is the timestamped header. It looks like t=<unix seconds>,v1=<key id>:<128 hex chars>. The hex is HMAC SHA512 of the timestamp, a ., and the raw body, keyed with the whole webhook signing secret, including the whsec_ prefix. Reject t when it is more than 300 seconds from your clock. An event can carry several v1 values while you rotate the secret (1 hour, 24 hours, 3 days, or 7 days). Compare every v1. You manage this secret under Developers, then API configuration. It is not the secret key.
  • x-paystack-signature is the older header. It is HMAC SHA512 of the raw body only, keyed with the secret key for that mode, and it has no timestamp. Paystack sends it while the secret key still exists.

Do not delete the secret key before events carry Paystack-Signature. Paystack's docs say that deleting it stops webhook delivery, and that x-paystack-signature stops once the secret key is gone. Keep verifying x-paystack-signature until Paystack-Signature is present, then verify Paystack-Signature.

Answer 401 when nothing matches. Answer 200 when one value matches, then process the event.

Cause 6: a non-2xx for an event you do not handle

Paystack counts only a 2xx as success. A 404, a 500, or a handler that errors on an unknown event name is a failed delivery, and the retry schedule in the FAQ runs.

That includes events you did not write a branch for. Return 200 and ignore them.

The webhook endpoints feature is in pilot, by request. Its ping sends a signed event with event set to ping. The diagnosis table treats a non-2xx as "your server received the ping and returned an error". Return 200 for ping as well. You can ping up to 10 times every 60 seconds in each mode.

Paystack's verify docs say webhooks are currently sent for successful transactions only. The charge event in the webhook list is charge.success. A failed, abandoned, or pending transaction has a status on the verify API, and it may never produce a webhook. Check the transaction in the dashboard, or call verify, before you treat a missing webhook as a delivery failure.

Cause 7: delivered, but nothing happened

Delivered only means your server said 2xx. Open the application log for that request.

Two checks cover most "delivered, but the order did not move" reports:

  • Subunits. data.amount is in the subunit of the currency. For NGN, that is kobo: 100 naira is 10000, not 100. Dividing too late, or not dividing, makes the amount look 100 times off. Confirm data.amount, data.currency, and data.status with the verify transaction API before you give value.
  • Idempotency. Key the work on data.reference (with the event name if one reference can carry more than one event). If the key is already stored, answer 200 and do nothing else. A unique-key error that turns into a 500 makes Paystack retry a payment you already applied.

A scheduled verify of transactions you still show as pending is the backstop when every webhook attempt was missed.

Recovering missed events

  • Paystack's own retries. Live mode: every 3 minutes for the first 4 tries, then hourly for 72 hours. Test mode: hourly for 10 hours, with a 30 second timeout on each attempt.
  • Dashboard Retry, or resend through the Webhook Events API. Both send to the current URL, and neither is idempotent. Dedupe on data.reference first.
  • Verify on a schedule. Call the verify transaction API for references you still show as unpaid.

With Hooksnode

Point Paystack at one Hooksnode source URL that answers immediately. Hooksnode then delivers to each of your services on its own schedule: up to 25 attempts in total. An attempt fails on a non-2xx response or when the destination does not answer within 15 seconds. The wait between attempts is capped at 1 hour, so the full run is about 17.5 hours. Every attempt is in the event log and can be replayed. A HEAD health check is sent to each active destination every 15 minutes. Failure emails go to workspace members who can see that project.

Delivery is at least once, never exactly once. Hooksnode does not add an Idempotency-Key header of its own. Unsigned sources can dedupe on an inbound X-Idempotency-Key. A Paystack preset source is signed, so that header is ignored. Hooksnode drops an exact resend of the same body and signature for 10 minutes. Paystack can still send the same event again later, so keep the data.reference check in each service.

Hooksnode forwards the raw body and the original headers, including Paystack-Signature and x-paystack-signature. Retries and replays send those headers again. They do not re-sign them. Paystack-Signature has a 300 second tolerance, so a destination that checks it can pass on the first delivery and fail on a Hooksnode retry a few minutes later. x-paystack-signature has no timestamp, but it is still the original value, and a transform changes the body so neither Paystack signature matches. Destinations should verify X-Hooksnode-Signature instead. On the destination, open Signing, turn on Sign outbound requests, and copy Signing secret. Hooksnode generates that secret. You cannot set it when you create the destination, and the API does not accept a secret value. The default format is X-Hooksnode-Signature: sha256=<hex>, HMAC SHA256 of the body actually delivered. It has no timestamp, so a later retry still verifies. A destination can instead use the Mantissa format, t=<unix seconds>,v1=<hex>, signed when that attempt is sent.

The Paystack source preset verifies Paystack-Signature when that header is present (the whsec_ secret as pasted, every v1, 300 second tolerance). When Paystack-Signature is absent, it verifies x-paystack-signature with the secret key. A missing or wrong signature is answered with 401 and is not delivered.

Turn on Require signature verification, set Signature format to Paystack, paste the secret into Paystack secret, and press Save format. Use the whsec_ secret when events carry Paystack-Signature, and the secret key while they only carry x-paystack-signature. A Free project with no destination answers 400 ({"error":"no destinations configured"}). Add a destination before you point Paystack at it.

The setup with filters and per-destination retries is in Send one Paystack webhook to several services. Local copies are in Test real webhooks on your laptop. A listen copy keeps the original headers. A dashboard replay does too, and it does not re-sign Paystack-Signature. The Flutterwave version of this page is Why your Flutterwave webhook is not arriving. How Shuttlers uses Hooksnode is one payment stream reaching several products.

Try it free

The project Free plan is $0 and includes 100,000 credits a month, with a hard cap: when credits run out, new events get 402 instead of a bill. Checked against the public plans API on 2026-11-06. See pricing.

FAQ

Does Paystack retry failed webhooks?

In live mode Paystack retries every 3 minutes for the first 4 tries, then hourly for 72 hours. In test mode it retries hourly for 10 hours, and each test mode attempt times out at 30 seconds.

Why is my webhook pending?

Pending means Paystack is still processing the event or waiting for an automatic retry. If it stays pending, confirm the URL for that mode is public and that your server returns a 2xx in time.

Does Paystack follow redirects?

No. Paystack does not follow redirects. An http to https redirect, a trailing-slash change, or a www redirect is a failed delivery. If you use .htaccess, include the trailing slash in the webhook URL.

Which IPs does Paystack send from?

Paystack sends webhooks from 52.31.139.75, 52.49.173.169, and 52.214.14.220. The same addresses are used in test and live mode.

Further reading