A customer completes payment for a ₦75,000 order and sees a successful checkout message. Seconds later, the store sends two order-confirmation emails and dispatches the same goods twice. On another day, a genuine transfer succeeds but the customer sees an unpaid invoice because the browser was closed before the website returned from checkout. Both incidents can stem from weak payment-event handling.

For Nigerian online businesses using Paystack, Flutterwave or Monnify, webhooks help a website learn what happened after a transaction was initiated. But simply connecting a webhook URL is not enough: a reliable integration must authenticate events, verify payment status, handle repeated notifications, protect against simultaneous workers and recover missed events. The goal is to accept a valid payment once and fulfil its corresponding business obligation once.

1. What is a payment webhook?

A payment webhook is an HTTP notification sent by a payment service to a merchant's server when a relevant event happens. Instead of constantly asking the provider whether an invoice has been paid, the merchant exposes a secure HTTPS endpoint and the provider sends an event to it. Events may relate to successful payments, failed transfers, refunds, chargebacks, subscription changes or settlement records, depending on the gateway.

Paystack's webhook documentation describes the approach as a server-to-server event notification and recommends it for delivering value instead of relying solely on a customer's browser callback. Flutterwave's webhook guide likewise explains why webhooks matter for asynchronous bank transfers, USSD and subscriptions. Monnify supports events for collections, refunds, disbursements and settlements.

2. Why the redirect page is not proof of payment

A checkout redirect can fail because the customer loses internet connectivity, closes the tab or leaves before the return URL loads. Worse, a browser-provided status or URL parameter may be manipulated. A merchant must not ship an order, issue a ticket, extend a subscription or credit a customer wallet merely because the page says 'successful'.

The backend should instead use an authenticated webhook or independently query the provider's transaction-verification API. Paystack's verify-payment guide documents a server-side reference lookup and cautions merchants not to grant digital value twice when both callbacks and webhooks are used. Browser redirects remain useful for telling customers a payment is being checked.

3. A safe webhook flow from checkout to fulfilment

  1. Create an internal order with a unique reference, expected amount, currency and pending status before initiating payment.
  2. Ask the selected gateway to initiate a payment and associate the provider transaction with the internal reference.
  3. When a webhook arrives, verify its signature or version-appropriate authentication and ensure the request is structurally valid.
  4. Durably record the event and a processing instruction before returning a successful HTTP acknowledgement.
  5. A background worker independently verifies the payment with the provider, confirming reference, status, amount and currency.
  6. Within a database transaction, record the payment and perform a single guarded transition from unpaid to paid.
  7. Record the required fulfilment action in a durable outbox so that email, stock dispatch or account credit happens once.
  8. Periodically reconcile still-pending payments against provider records in case notifications were missed.

This separates the quick request handler from slower business operations. The merchant's response should mean 'the event is safely accepted for processing', not 'all downstream work has already succeeded'. If the event cannot be stored durably, a failure response should allow provider retry according to the provider's delivery rules.

4. Why payment notifications arrive more than once

Webhooks are commonly delivered using retry-based behaviour: if the provider does not receive its required success response, it may resend the same notification. Network errors, maintenance, server timeouts and manual dashboard retries can also produce duplicates. Duplicate delivery is therefore a normal engineering condition, not automatically evidence of fraud.

Paystack currently documents live webhook retries every three minutes for the first four attempts, followed by hourly retries for up to 72 hours when it does not receive HTTP 200. The updated Paystack dashboard guidance also describes viewing failed deliveries and manually retrying them. Flutterwave and Monnify similarly advise duplicate-safe handlers. Never assume that receiving an event twice means the customer paid twice.

5. What idempotency means in plain English

Idempotency means that repeating the same operation has the same business outcome as performing it once. For a ₦75,000 purchase, one verified payment should produce one fulfilled order, even if the webhook is delivered five times or two background workers attempt to process it at the same moment.

An initial check such as 'if not paid, mark paid' is insufficient if two workers read the pending state simultaneously. Both could decide the order is unpaid and both may create shipping labels or wallet-credit entries. Use enforceable database uniqueness rules and atomic state transitions, not only an application-level conditional.

6. Apply idempotency at two different levels

LevelUnique keyPurpose
Webhook inboxProvider + merchant account + stable event ID, or documented event fingerprintRecognise a repeated notification
Payment transactionProvider + merchant account + provider transaction IDPrevent the same payment being booked twice
Business fulfilmentOrder or invoice + specific fulfilment actionPrevent duplicate dispatch, activation or credit
Ledger movementUnique payment reference + movement purposePrevent double wallet balance changes

Some providers do not expose the same event ID for every webhook type. When necessary, use a stable documented combination of transaction ID, event type and relevant final status. Keep separate keys for event deduplication and the financial action: receiving two different events for one successful payment should still result in one financial posting.

7. A realistic example: One payment, three notifications

An online store has order ORD-204 for ₦75,000. The gateway successfully collects the money and sends a payment-success event at 12:01. The server stores it, but a network interruption prevents the provider seeing the acknowledgement. The provider retries at 12:04 and 12:07. The event inbox may log the repeated attempts; the payment table retains one verified transaction; and the order's paid transition occurs exactly once.

The fulfilment outbox should also contain only one shipment instruction for ORD-204. If dispatch service is temporarily unavailable, the outbox worker retries delivery without reopening the original payment transaction. A courier or email system may itself need an idempotency key so an external retry does not create a second shipment or confirmation.

8. Secure webhook authentication differs by provider and API version

Paystack: HMAC-SHA512

Paystack signs webhook payloads using a secret key and places the resulting signature in the x-paystack-signature header. Its official guide uses HMAC-SHA512 of the payload and also documents approved source addresses. Validate the signature against the original raw request body before accepting the event. Keep the secret server-side and never place it in frontend code.

Flutterwave: Check the version

Flutterwave's v3 webhook documentation describes a verif-hash header matched against the merchant's configured secret hash. Its v4 documentation instead describes HMAC-SHA256 and a flutterwave-signature header. These are not interchangeable: an integration must implement the exact authentication scheme of its API version. Flutterwave warns that its IP addresses are dynamic, so strict IP-only allowlisting is not its preferred verification method.

Monnify: Verify signatures and environment

Monnify's webhook documentation describes validation using a monnify-signature header in production, together with a documented signature-computation procedure and source-address checks. It notes that the production signature header is absent from sandbox notifications. Because examples and formulas may differ across provider documentation revisions, use the exact signature algorithm and environment guidance for your deployed integration rather than guessing or disabling production verification.

9. Independently verify amount, currency and payment state

A valid webhook signature proves that a notification was produced by the party holding the signing secret; it does not by itself prove that an order has been paid for the correct amount. For a high-value or sensitive action, independently query the provider's verification endpoint and compare the resulting payment with the merchant's order.

Flutterwave's transaction verification guidance explicitly calls for checking the transaction reference, successful status, currency and amount. Monnify's verify transactions guide documents different states including pending, partial, overpaid, failed and reversed. Paystack cautions developers to inspect the transaction's data.status, not just the general API response's success flag.

Amounts may be represented in the smallest currency unit on some APIs. Normalise units before comparison, check the expected merchant account and avoid crediting a wallet for an underpayment simply because a response object contains the word 'success'.

10. Make the database the authority on whether an order is paid

In production, one database transaction should coordinate the payment record, order state and internal financial movement. Lock the relevant order row or use a guarded update, insert the provider transaction with a unique constraint, and create one outbox record for the next operation. Commit these together. Only after commit should workers send external notifications or fulfilment requests.

For example, a pending order should transition to paid only if the verified reference and amount match and it has not been paid already. A second worker should see no eligible state transition and exit without duplicating the side effects. Failed verification should leave the order pending or move it into a review state, not silently mark it complete.

A useful minimum schema includes orders, payment_attempts, webhook_inbox, payment_transactions, ledger_entries and outbox_messages. Enforce uniqueness at the database layer, including merchant or tenant context when IDs are only unique within that scope. Store event arrival time, provider timestamp and processing outcome separately.

11. How to recover payments when webhooks never arrive

Even a well-designed webhook handler can miss notifications because the endpoint was unavailable, an account webhook URL was misconfigured, a provider stopped retrying or a network route failed. Therefore, a site needs a controlled reconciliation process. At suitable intervals, fetch pending payments older than a reasonable threshold, query the provider using the original transaction reference and apply the same idempotent payment-confirmation operation used by the webhook worker.

Flutterwave recommends periodic verification of unresolved pending transactions as a backup to webhooks. Paystack offers transaction verification by reference and webhook event-history tools. Monnify supports transaction-status queries for asynchronous collections. Use backoff and provider rate limits, and alert operators if a transaction remains unresolved beyond expected timing. This is not permission to poll the same payment continuously.

12. Do not confuse paid, fulfilled, refunded and settled

A payment can be successful without the order being shipped; an order can be fulfilled before the provider settles funds into the business bank account; and a previously successful payment may later be refunded, disputed or reversed. Model these as separate states and records.

EntityExample statesQuestion answered
PaymentPending, successful, failed, reversedWhat happened to the collection?
OrderPending, paid, cancelledHas the customer's obligation been paid?
FulfilmentQueued, dispatched, deliveredHas the business provided the product?
SettlementPending, settled, adjustedHas the merchant received net funds?

When a refund or chargeback occurs, create a separate compensating financial event with an auditable reference. Avoid deleting the original payment record or overwriting a settled record as though it never existed.

13. Webhook testing checklist for developers

  1. Confirm HTTPS endpoint availability and correct test/live webhook URLs.
  2. Test missing, invalid and mismatched signatures for the provider's deployed API version.
  3. Send the exact same successful event multiple times and verify a single order transition.
  4. Deliver different event types out of order and ensure older events do not undo a newer valid state.
  5. Simulate two workers processing the same payment concurrently and verify database uniqueness.
  6. Crash a worker after saving payment but before sending fulfilment; confirm durable outbox recovery.
  7. Test delayed events, missing events, invalid amounts and currencies, underpayments and reversals.
  8. Verify tenant, branch, customer and invoice assignment in a multi-business application.
  9. Monitor retry queues, age of pending payments and unmatched settlement or ledger movements.
  10. Run a controlled live transaction and check provider dashboard, app records and bank reconciliation.

For readers seeking a general introduction, FlyingEze's existing What is a Stripe Webhook? explains the concept from another provider's perspective. Its broader payment gateway integration guide covers trust and customer experience.

Frequently asked questions

Can Paystack, Flutterwave or Monnify send duplicate webhooks?

Yes. Providers may retry or resend events when the original delivery is not acknowledged or when an operator manually retries. Design handlers so repeated notifications do not repeat financial or fulfilment actions.

Should a webhook automatically mark an order paid?

Only after authenticating the event and verifying that the transaction matches the order's expected reference, amount, currency and success status. The update must be idempotent and atomic.

What if the payment succeeds but the webhook never arrives?

Keep the order pending while a reconciliation job checks the transaction through the provider's verification API. Apply the same guarded payment finalisation process when the payment is confirmed.

Should webhook processing return HTTP 200 immediately?

Respond promptly after authenticating and durably accepting the event. Do not acknowledge an event as successfully accepted if the system has failed to save it or queue it reliably; otherwise the provider may not retry the missed event.

Is a webhook the same as a bank settlement?

No. A webhook is a notification. Payment confirmation, order fulfilment and the bank settlement are separate business events that may occur at different times.

Conclusion

Payment webhooks are essential infrastructure for Nigerian online businesses, but a webhook URL alone does not make collections reliable. The safe pattern is to authenticate events, verify transactions independently, record notifications durably, enforce uniqueness in the database and process fulfilment through retryable, idempotent work.

Combine real-time notifications with reconciliation checks so no legitimate payment is forgotten. For merchants and developers, the objective is simple: every genuine payment should be accounted for accurately, while the same order or wallet receives value only once.