Why Webhook Verification Matters
When integrating payment providers, webhooks are essential for receiving real-time updates about transaction statuses. However, anyone can send an HTTP POST request to your webhook URL. Without verification, an attacker could easily forge payment success notifications, leading your system to release goods or services for unpaid transactions. Webhook verification is a critical security requirement, not an optional feature.
The Two Verification Patterns
Nigerian payment providers typically employ one of two distinct patterns for verifying incoming webhooks:
- HMAC Signature Verification (Paystack, Monnify): The provider computes an HMAC-SHA512 hash of the raw HTTP request body using your account’s secret key. This hash is sent along with the request in an HTTP header. Your server recomputes the hash using the same secret key and the raw body it received, then compares the two.
- Secret Hash Matching (Flutterwave): The provider sends a pre-shared secret value in a header (
verif-hash). Your server compares this value against the secret hash you configured in the provider’s dashboard.
IMPORTANT: These are fundamentally different mechanisms. HMAC proves that the payload hasn’t been tampered with in transit AND authenticates the sender. Secret hash matching only authenticates the sender.
HMAC-SHA512 Verification: Paystack and Monnify
The HMAC pattern involves cryptographic verification. Here is the conceptual process:
- Extract the signature from the request header.
- Get the raw request body as bytes. Do not parse it as JSON first, as this can alter the byte order or formatting, invalidating the hash.
- Compute an HMAC-SHA512 hash using your secret key and the raw body.
- Compare the computed hash with the received signature. Always use a constant-time comparison function to prevent timing attacks.
- Reject the request if the signatures do not match.
Here is a conceptual pseudocode example:
function verifyHmacWebhook(rawBody, receivedSignature, secretKey):
computedHash = hmac_sha512(secretKey, rawBody)
return constantTimeEquals(computedHash, receivedSignature)
Note: Paystack uses HMAC-SHA512, and Monnify uses HMAC-SHA512 via the monnify-signature header.
Secret Hash Verification: Flutterwave
Flutterwave uses a simpler secret hash pattern:
- Configure a custom secret hash string in your Flutterwave dashboard.
- On each incoming webhook, extract the value from the
verif-hashheader. - Compare this header value against your stored secret hash using a constant-time comparison.
- Reject the request if there is a mismatch.
Note: Unlike HMAC, this method does not cryptographically prove that the request body hasn’t been modified in transit. For additional security when using this pattern, it is strongly recommended to always verify the transaction status by calling the Flutterwave API directly after receiving a webhook.
Server-Side Verification: Never Trust the Client
Webhook verification must strictly happen server-side. Your webhook URLs and the logic handling them should never be exposed to the client application. After successfully verifying the webhook signature or secret hash, always confirm that the transaction details (such as the amount, currency, and reference) match what you expect in your database before fulfilling the order.
Common Implementation Mistakes
When implementing webhook endpoints, avoid these frequent pitfalls:
- Parsing JSON before computing HMAC: Parsing and re-serializing the payload changes its raw bytes, which will cause the HMAC verification to fail.
- Using standard string comparison: Use constant-time comparison (like
crypto.timingSafeEqualin Node.js) instead of==or===to mitigate timing attacks. - Not verifying transaction amounts: A verified webhook only proves the payment happened, but you must ensure the amount paid matches the expected order total.
- Not handling duplicate webhooks: Providers may retry webhooks. Use idempotency keys or check transaction status in your database to avoid processing the same event twice.
- Exposing webhook secrets: Never include your provider secret keys or hashes in client-side code or public repositories.
Architectural Best Practices
To build robust webhook handlers, consider the following best practices:
- Acknowledge quickly: Respond with a
200 OKstatus immediately, then process the payload asynchronously. This prevents timeout errors on the provider’s end. - Log everything: Log all received webhooks (both successful and failed verifications) for debugging and auditing purposes.
- Implement idempotency: Design your system to handle duplicate webhook deliveries gracefully.
- Use a message queue: For high reliability, place validated webhook payloads onto a message queue (like RabbitMQ or SQS) rather than processing them inline.
- Monitor and alert: Set up monitoring and alerting for webhook verification failures or processing errors.
Official Documentation
For more information, refer to the official documentation of each provider:
- Paystack: API Documentation and General Docs
- Flutterwave: Developer Documentation
- Monnify: Developer Documentation