A customer pays, Paystack redirects them back to your website, and your success page loads.
That is not yet proof that your business should mark the order as paid.
Paystack’s current documentation explicitly says that visiting the callback URL does not prove a transaction was successful. Payment confirmation belongs on a trusted server path, using the transaction reference and Paystack’s verification mechanisms.
This distinction becomes especially important in AI-built MVPs, ecommerce stores, booking systems, school portals and SaaS products. The interface can look finished while the payment state underneath is still fragile.
Callback, verification and webhook do different jobs
A useful mental model is:
| Mechanism | What it is good for | What it should not be trusted to do alone |
|---|---|---|
| Callback URL | Return the customer to your app after payment | Prove that money was successfully received |
| Verify Transaction API | Ask Paystack for the current status of a known reference | Replace all asynchronous payment handling |
| Webhook | Let Paystack notify your server of successful payment independently of the customer’s browser | Blindly trigger fulfillment without signature and duplicate checks |
Paystack recommends webhooks for confirming successful transactions because the customer’s browser or network may disappear before a callback completes. Its payment verification documentation also provides a server-side Verify Transaction endpoint for checking a known reference.
The reliable architecture is not “callback versus webhook.” It is deciding what each signal is allowed to change.
Do not make the browser your payment authority
The browser is useful for user experience. It is a poor source of financial truth.
A customer can close the tab. Their connection can fail. A redirect can be interrupted. Frontend state can be stale or manipulated.
So a route such as:
/payment/success?reference=abc123
should not become paid simply because reference=abc123 exists in the URL.
Instead, send that reference to your backend. The backend can call Paystack’s Verify Transaction endpoint using the secret key that must remain server-side.
Paystack’s Accept Payments documentation says to confirm the transaction status before delivering value and to verify the amount as well.
That last part matters. A real successful transaction for the wrong amount is not a successful payment for the order you expected.
Verify the business facts, not only success
Before changing an order, invoice, booking or subscription to paid, your application should be able to answer at least:
- Is this a transaction reference my system expects?
- Does Paystack report the transaction as successful?
- Does the paid amount match the amount this order requires?
- Does the transaction belong to the correct customer/order context?
- Has this transaction already been applied?
The fifth question prevents one of the nastier payment bugs: double fulfillment.
Imagine a digital product. The callback reaches your server and verification succeeds, so access is granted. Seconds later, the charge.success webhook arrives and the same fulfillment code runs again.
If fulfillment is not idempotent, one payment can create two credits, two receipts, two inventory deductions or two downstream jobs.
Paystack itself warns developers to confirm that value has not already been delivered for a transaction, particularly when webhooks and verification are both used.
Make payment application idempotent
The safe rule is simple:
One successful transaction reference should be applied to the business ledger once.
Your exact database design will vary, but the payment-application operation should have a durable uniqueness boundary around the provider transaction/reference or another stable provider identifier.
Conceptually:
receive payment signal
↓
validate trusted source / verify with Paystack
↓
check status + expected amount + business context
↓
start transaction
↓
has this Paystack transaction already been applied?
↙ yes ↘ no
acknowledge record payment
without replay update business state
commit once
This is stronger than keeping a temporary processed = true variable in memory. Your server can restart, two requests can arrive together, or the provider can retry later.
Financial state needs a durable database rule.
Verify webhook origin before processing it
Your webhook endpoint is public. Anyone who discovers the URL can send a POST request to it.
Paystack documents an x-paystack-signature header containing an HMAC SHA-512 signature created with your secret key. Verify that signature before treating the payload as a genuine Paystack event.
See Paystack’s current webhook documentation.
Do not put the Paystack secret key in frontend JavaScript to perform this verification. Keep it in your server, Worker, Edge Function or other trusted backend environment.
Acknowledge the webhook quickly
Paystack expects a successful HTTP acknowledgement from your webhook endpoint. Its current documentation says failed live webhook deliveries are retried, initially every few minutes and then hourly for up to 72 hours.
That retry behaviour is another reason idempotency is not optional.
Avoid making the webhook wait while you generate PDFs, send several emails, call slow third-party APIs or perform other long-running work.
A cleaner design is:
- authenticate the event;
- validate and persist the payment transition safely;
- acknowledge the webhook;
- run non-critical downstream work through a job/queue or another retry-safe mechanism where appropriate.
The payment record should not depend on whether a receipt email provider happened to respond quickly.
Do not collapse payment and fulfillment into one boolean
For a simple product, paid: true feels convenient. As the system grows, it hides important distinctions.
A transaction can be initiated, pending, successful, reversed or otherwise require reconciliation. A successful payment can also exist while a downstream action such as admission activation, booking confirmation or receipt delivery fails.
Model the important states separately enough that you can answer:
- what Paystack says happened;
- what your database recorded;
- whether the payment was applied to the intended business obligation;
- whether downstream fulfillment completed.
This is particularly important for partial payments, subscriptions, wallet credit, invoices or systems where one customer can have several outstanding obligations.
Test the ugly paths before going live
A payment integration is not tested because one test card produced a green success screen.
Before production, deliberately test:
- successful payment with normal redirect;
- successful payment where the browser closes before callback;
- duplicate webhook delivery;
- callback and webhook arriving close together;
- wrong or unknown reference;
- amount mismatch;
- invalid webhook signature;
- slow downstream email or receipt generation;
- repeated requests after the payment is already applied;
- failure after Paystack succeeds but before your local database update completes.
For every case, inspect the resulting database state, not only the UI.
The production rule
The most useful boundary is:
Paystack confirms the payment. Your application decides how that confirmed payment changes business state.
Keep those responsibilities separate.
The callback improves the customer’s journey. Server-side verification checks a known transaction. The webhook gives you an asynchronous provider-to-server signal. Your database makes applying that payment exactly once a business invariant.
That is the difference between a checkout that demos well and a payment system you can reconcile when real money is involved.
If you are building an AI-assisted product, use my AI-built app production checklist before launch. If the product needs payment-state, permissions or backend architecture reviewed before real users depend on it, see my MVP development service or contact me.



