Bonaventure OgetoBy Bonaventure Ogeto|

Why Webhooks Beat Callbacks for Paystack Payments

Webhooks are server-to-server HTTP POST requests that Paystack sends to your backend whenever an event occurs. They are more reliable than browser redirect callbacks because they do not depend on the customer staying online, keeping their browser open, or completing the redirect. Use callbacks for UX (showing a success page) and webhooks for business logic (crediting accounts, fulfilling orders).

Two Ways Paystack Tells You About Payments

When a customer completes a payment through Paystack, there are two mechanisms that inform your application:

1. The redirect callback (browser-based). After the customer pays, Paystack redirects their browser back to a URL you specified. This URL receives the transaction reference as a query parameter. Your frontend or backend at that URL can then call the Paystack Verify Transaction API to check if the payment succeeded.

2. The webhook (server-to-server). Paystack sends an HTTP POST request directly to your backend with the full event payload. This happens regardless of what the customer's browser does. Your server verifies the signature, processes the event, and returns 200.

Both exist for a reason. The callback is about the customer's experience. The webhook is about your system's reliability. The problem starts when developers treat the callback as the primary confirmation mechanism and either skip webhooks entirely or treat them as a backup. That is backwards.

What Can Go Wrong with Callbacks

A redirect callback follows this path: Customer pays on Paystack checkout, Paystack redirects the browser to your callback URL, your server receives the request, you call the Verify Transaction endpoint, you grant value.

Here is where that chain breaks:

The customer closes the browser. Payment goes through on Paystack's side, but the browser never reaches your callback URL. Your system never learns about the payment. The customer paid, but their account shows nothing.

The customer loses internet. Mobile internet in Lagos, Nairobi, or Accra is not always stable. The payment succeeds, the redirect starts, and then the connection drops. Same outcome.

The browser crashes. It happens. Especially on low-end Android devices running multiple apps. The payment is done, the redirect never completes.

The customer navigates away. Impatient customers (most customers) tap the back button or close the tab before the redirect finishes. Payment succeeded, callback missed.

Ad blockers or browser extensions interfere. Some browser extensions block redirects to unfamiliar domains or strip query parameters. The callback arrives broken or not at all.

A network middlebox modifies the redirect. Some ISPs in Africa inject content or modify HTTP redirects. The URL your server receives might not match what Paystack sent.

Every one of these scenarios ends the same way: the customer paid real money, but your system did not record it. You now have a support ticket, a frustrated customer, and in some cases, a compliance issue.

Why Webhooks Are Reliable

A webhook follows a different path: Customer pays, Paystack's servers send an HTTP POST directly to your server. No browser involved. No redirect. No customer device in the loop.

The webhook arrives because:

  • It is server-to-server. Paystack's infrastructure sends the request to your infrastructure. The customer's browser, device, and internet connection are irrelevant.
  • Paystack retries on failure. If your server is down or returns a non-2xx response, Paystack retries the webhook multiple times over a period of hours. You get multiple chances to receive the event.
  • The payload is complete. The webhook body contains the full event data: amount, reference, customer, channel, metadata, everything. You do not need a second API call to get the details.

Webhooks are not perfect. Your server could be down for longer than Paystack's retry window. Your webhook handler could have a bug that makes it reject valid events. Your firewall could block Paystack's IPs. But all of these are problems on your side that you can fix. With callbacks, the failure is on the customer's side and you cannot fix it.

That asymmetry is why webhooks should always be the source of truth. See webhook retries: what to expect and how to cope for details on the retry behavior.

When to Use Each

The question is not "webhooks or callbacks?" but "what should each one do?"

Use callbacks for user experience:

  • Redirect the customer to a "thank you" page.
  • Show an order confirmation with a pending status.
  • Display a receipt or download link (if the webhook has already confirmed the payment).
  • Remove items from a shopping cart.

Use webhooks for business logic:

  • Credit the customer's account or wallet.
  • Mark the order as paid in your database.
  • Trigger fulfillment (ship the product, activate the subscription, unlock the course).
  • Send a confirmation email or SMS.
  • Update your accounting records.

Use the Verify Transaction API as a safety net:

  • When the callback arrives, call the Verify endpoint to check the transaction status. If the payment succeeded, you can show a confirmed success page instead of a pending one.
  • If the webhook is delayed (it sometimes takes a few seconds), the verification call on the callback gives you faster feedback for the customer.

The rule: never grant value based on the callback alone. The callback tells you the customer was redirected. It does not tell you the payment succeeded. Only the webhook (or a successful Verify API call) tells you that.

Handling the Race Condition

Sometimes the callback arrives before the webhook. Sometimes the webhook arrives before the callback. Sometimes they arrive within milliseconds of each other. Your code must handle all three cases.

The idempotent update pattern above handles this automatically. Both paths run the same UPDATE query with the same WHERE clause. The first one to execute updates the row. The second finds zero rows to update (because the status is no longer "pending") and does nothing.

The race condition becomes dangerous only when you do non-idempotent operations. For example, if both the callback and the webhook handler call a "credit wallet" function that adds KES 500 to the user's balance, the customer gets credited twice.

Rules to avoid this:

  • Use a single function for payment confirmation logic that both paths call.
  • That function must be idempotent. Check before you act.
  • Use database transactions with a unique constraint on the reference to prevent double-processing even under exact concurrency.

For a thorough treatment of this scenario, see race conditions between webhooks and redirect callbacks. For the general idempotency pattern, see idempotent webhook handlers.

What About Inline (Popup) Checkout?

If you use Paystack Inline (the JavaScript popup), the flow is slightly different. Instead of a redirect callback, you get a JavaScript callback function that fires when the popup closes:

var handler = PaystackPop.setup({
  key: 'pk_live_your_public_key',
  email: 'customer@example.com',
  amount: 50000,
  currency: 'NGN',
  ref: 'your-unique-ref-123',
  callback: function(response) {
    // This fires when payment completes
    // response.reference contains the transaction reference
    // Verify via your backend, show success UI
    window.location.href = '/payment/verify?reference=' + response.reference;
  },
  onClose: function() {
    // Customer closed the popup without completing payment
    console.log('Payment popup closed');
  },
});
handler.openIframe();

The same reliability concerns apply. The callback function runs in the browser. If the browser crashes, the tab closes, or JavaScript fails, the callback never fires. The webhook still fires on Paystack's side.

The approach is the same: treat the JavaScript callback as a UX mechanism (navigate to a verification page, show a spinner) and treat the webhook as the business logic trigger.

For more on the popup approach, see Paystack Popup InlineJS explained line by line.

Common Mistakes

Mistake 1: Granting value in the callback without verification. Some developers receive the callback, see the reference in the query string, and immediately mark the order as paid. Anyone can craft a URL with a fake reference and hit your callback endpoint. Always verify through the Paystack API or wait for the webhook.

Mistake 2: Not implementing webhooks at all. If you rely only on callbacks and the Verify API, you will miss payments whenever the customer's browser fails to redirect. This leads to support tickets like "I paid but my account was not credited." The webhook catches every payment, every time.

Mistake 3: Treating the webhook as a backup. The mental model matters. If you think "the callback handles payments, and the webhook catches edge cases," you will build your primary flow around the callback and add webhook handling as an afterthought. Flip it. The webhook is primary. The callback is the UX layer on top.

Mistake 4: Not handling the case where neither arrives. Build a reconciliation job that runs periodically (every hour or every few hours), fetches recent transactions from the Paystack API, and checks them against your database. If you find a successful transaction with no corresponding order update, flag it for review. This catches the rare case where both the callback and webhook failed.

Mistake 5: Different logic in the callback and webhook handlers. If the callback credits 500 and the webhook credits 500, a customer who triggers both gets 1000. Use a single, idempotent function that both call.

Putting It Together

The callback and the webhook serve different purposes:

AspectRedirect CallbackWebhook
MechanismBrowser redirectServer-to-server POST
Depends on customerYesNo
Contains full payloadNo (reference only)Yes
Retried on failureNoYes
Use for UXYesNo
Use for business logicNoYes
Requires API verification callYesNo (signature is enough)

Build your payment confirmation around webhooks. Use callbacks to make the customer's experience feel instant. Use the Verify API as the bridge between the two. And always, always make your confirmation logic idempotent.

This article is part of the Paystack webhooks engineering guide. For the next step, learn how to verify the x-paystack-signature header correctly.

Key Takeaways

  • Browser redirect callbacks depend on the customer staying online and their browser completing the redirect back to your site.
  • Webhooks are server-to-server. They arrive even if the customer closes their browser, loses connectivity, or navigates away.
  • Callbacks are good for user experience. They let you show a success or failure page immediately after checkout.
  • Webhooks are good for business logic. They are the only reliable way to confirm that a payment actually went through.
  • Never grant value based on a callback alone. Always wait for the webhook or verify the transaction via the Paystack API.
  • The recommended pattern: use the callback to show a pending state, then update to confirmed once the webhook arrives or the API verification succeeds.

Frequently Asked Questions

Can I use only callbacks without webhooks for Paystack payments?
You can, but you will miss payments whenever the customer's browser fails to redirect. Mobile network drops, browser crashes, impatient tab closes, and ad blocker interference all prevent the callback from reaching your server. Webhooks arrive server-to-server regardless of what happens on the customer's device. For any production application handling real money, webhooks are not optional.
What happens if both the callback and webhook confirm the same payment?
If your confirmation logic is idempotent, nothing bad happens. The first one to arrive updates the order to paid. The second one finds the order already paid and does nothing. Problems only arise if you perform non-idempotent operations (like adding money to a wallet balance) without checking if the payment was already processed.
Does the webhook always arrive before the callback?
No. The timing is not guaranteed. Sometimes the webhook arrives first, sometimes the callback does, and sometimes they arrive within milliseconds of each other. Your code must handle all three cases. The idempotent update pattern (only update if status is still pending) handles this automatically.
Should I call the Verify Transaction API inside my webhook handler?
No. The webhook payload already contains the full transaction details and the signature proves the data came from Paystack. Calling the Verify API inside the webhook handler adds an unnecessary network round trip that slows down your response. Use the Verify API in your callback handler or in a reconciliation job, not in the webhook handler.
How do I handle the case where a customer paid but neither the callback nor the webhook reached my server?
Build a reconciliation process. Periodically (every hour or every few hours) fetch recent transactions from the Paystack List Transactions API and compare them against your database. If you find a successful transaction with no corresponding order update, flag it for manual review or process it automatically. This catches edge cases that neither callbacks nor webhooks cover.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon