# Guide to Paystack Webhooks: Features and Best Practices

Paystack webhooks notify your backend when payment activity happens: a charge succeeds, a transfer completes, a subscription is created. Because a webhook is often the authoritative signal that money moved, verifying and handling them correctly is core to any Paystack integration.

This guide covers how Paystack webhooks work, the events you'll handle, how to verify the `x-paystack-signature` (which is HMAC-SHA512, not SHA256), the source IPs and retry behavior, and the best practices for production.

## What are Paystack webhooks?

Paystack webhooks are HTTP POSTs sent to a URL you configure, each carrying a JSON body with a top-level `event` (a dot-separated name) and a `data` object. You configure the webhook URL per mode (test and live are separate), and Paystack signs every delivery so you can confirm it came from Paystack.

The signature uses SHA-512, which is the detail most integrations get wrong on the first try.

## Paystack webhook features

| Feature | Details |
| --- | --- |
| Configuration | Dashboard > Settings > API Keys & Webhooks (test and live separate) |
| Signature header | `x-paystack-signature` |
| Signature scheme | HMAC-SHA512 of the raw body, keyed with your secret key, hex-encoded |
| Payload | Top-level `event` (dot-separated) + `data` |
| Source IPs | `52.31.139.75`, `52.49.173.169`, `52.214.14.220` (test and live) |
| Acknowledgement | Respond 200 immediately; request timeout 30 seconds |
| Retries (live) | Every 3 min for the first 4 tries, then hourly for up to 72 hours |
| Retries (test) | Hourly for 10 hours |
| SDK | Official clients are general API clients; no verification helpers |

## Common events

Paystack events are dot-separated names. The core ones:

| Event | Fires when |
| --- | --- |
| `charge.success` | A charge is completed successfully |
| `transfer.success` | A transfer completes |
| `transfer.failed` | A transfer fails |
| `transfer.reversed` | A transfer is reversed |
| `subscription.create` | A subscription is created |
| `subscription.disable` | A subscription is disabled |
| `invoice.update` | An invoice is updated |
| `refund.processed` | A refund is processed |

Subscribe your handler to the events you process, and consult Paystack's supported-events reference for the full list.

## Setting up Paystack webhooks

Set the webhook URL in Dashboard > Settings > API Keys & Webhooks. Test and live modes have separate webhook URLs, so configure both. Optionally allowlist Paystack's source IPs (`52.31.139.75`, `52.49.173.169`, `52.214.14.220`, the same for test and live) at your firewall. Respond 200 immediately and defer work; the request times out after 30 seconds.

## Securing Paystack webhooks

Every delivery carries an `x-paystack-signature` header: a hex-encoded HMAC-SHA512 of the request body, keyed with your Paystack secret key. Note the algorithm: it's SHA-512, not the SHA-256 most providers use. Verify against the raw request body.

Paystack's own documentation example hashes `JSON.stringify(req.body)`, which works only if your framework reproduces the exact bytes Paystack sent. To avoid re-serialization mismatches, hash the raw body directly.

```javascript
const crypto = require("crypto");

const SECRET_KEY = process.env.PAYSTACK_SECRET_KEY;

function verify(rawBody, signature) {
  const expected = crypto
    .createHmac("sha512", SECRET_KEY)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.headers["x-paystack-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge immediately
  processQueue.add(JSON.parse(req.body)); // process asynchronously
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

SECRET_KEY = os.environ["PAYSTACK_SECRET_KEY"].encode()

def verify(raw_body: bytes, signature: str) -> bool:
    expected = hmac.new(SECRET_KEY, raw_body, hashlib.sha512).hexdigest()
    return hmac.compare_digest(expected, signature or "")

```

## Paystack webhook limitations and pain points

### It's SHA-512, over the raw body

The Problem: `x-paystack-signature` is HMAC-SHA512. A SHA-256 verifier (the common default) fails every delivery. And hashing a re-serialized body instead of the raw bytes produces a mismatch even with the right algorithm.

Why It Happens: Paystack chose SHA-512, and its example hashes a stringified body, which hides the raw-body requirement.

Workarounds:

* Use `sha512` and verify against the exact raw body.
* Compare in constant time.

How Hookdeck Can Help: Hookdeck verifies the SHA-512 signature at the edge against the raw bytes, so your application receives pre-verified events without the algorithm or serialization gotchas.

### No timestamp, so no replay window

The Problem: The signature covers the body only, with no timestamp, so a captured, validly signed payload can be replayed and still verify.

Why It Happens: Paystack's scheme doesn't include a signed timestamp.

Workarounds:

* Make processing idempotent and dedupe on `event` plus `data.id`/`reference`.
* Optionally restrict to Paystack's source IPs to reduce exposure.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, filtering replays and retries before they reach your app. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Retries bring duplicates

The Problem: Live retries run every 3 minutes for the first 4 attempts, then hourly for up to 72 hours (test retries hourly for 10 hours). The same event can arrive many times, and there's no documented idempotency header.

Why It Happens: At-least-once delivery favors eventual delivery of payment events.

Workarounds:

* Dedupe on `event` + `data.id`/`reference`.
* Make downstream effects idempotent (fulfillment, ledger writes).

How Hookdeck Can Help: Hookdeck deduplicates and durably queues deliveries, so the 72-hour retry window doesn't cause double-fulfillment. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Verify the webhook before trusting the amount

The Problem: Treating the webhook payload as the source of truth for how much was paid, without verifying, risks acting on a forged or replayed event.

Why It Happens: Webhooks are a notification, and a payment amount is exactly the kind of value an attacker would forge.

Workarounds:

* Always verify the signature first, then confirm the transaction by calling Paystack's verify endpoint before fulfilling.
* Reconcile against the API for high-value flows.

How Hookdeck Can Help: Hookdeck ensures only verified events reach your app, and its observability lets you trace exactly what was delivered when reconciling payments.

## Best practices

### Verify with HMAC-SHA512 over the raw body

Use `sha512`, key it with your secret key, verify against the exact raw body, and compare in constant time.

### Acknowledge immediately, process asynchronously

Return 200 right away and defer work to a queue, well inside the 30-second timeout. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Confirm the transaction before fulfilling

After verifying the signature, call Paystack's verify endpoint to confirm the amount and status before granting value.

### Dedupe on event + reference

Use `event` plus `data.id`/`reference` as an idempotency key so the retry window doesn't double-process.

### Configure test and live separately

Set both webhook URLs, and optionally allowlist Paystack's source IPs.

## Conclusion

Paystack webhooks are often the authoritative signal that a payment happened, so verifying them correctly matters, and the detail that catches people is the algorithm: HMAC-SHA512 over the raw body, keyed with your secret key. Add the lack of a replay window, a 72-hour retry schedule that brings duplicates, and separate test/live configuration, and there's real care required before you fulfill on an event.

That leaves correct verification, transaction confirmation, deduplication, and fast acknowledgement on you. [Hookdeck](https://hookdeck.com) verifies the SHA-512 signature, deduplicates, and durably queues every event at the edge, so your backend only ever acts on verified, unique payment events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Paystack webhooks reliably in minutes.