The single most common mistake in a first payment integration is marking an order paid because the customer's browser landed back on a success page. That page proves nothing on its own.
3 min read
Customer clicks "Pay" -> gateway's hosted checkout -> customer redirected
back to https://yourapp.com/checkout/success?order=123
It's tempting to write: "if the browser reaches /checkout/success, mark the order as paid." Don't. That URL is just a page the merchant's own frontend defined — nothing stops a customer from typing it in directly, bookmarking it from a previous session, or the redirect firing after a payment that was later declined. The browser landing on a "success" URL is a UX signal, not a financial one.
Gateway's servers -- POST --> Merchant's own backend endpoint
(https://yourapp.com/api/webhooks/payments)
This is a completely separate channel: the payment gateway's own infrastructure calling the merchant's backend directly, with no browser, no customer, and no client-side code in between. This is the signal a backend should actually act on — mark an order paid, provision access, send a confirmation — because it originates from the party that actually processed the charge.
Purely the customer-facing experience: showing a "Thanks, here's your receipt" page versus a "Something went wrong, try again" page, ideally by querying the merchant's own database for that order's current status (which the webhook already updated) rather than trusting anything in the redirect URL itself. The redirect decides what the customer sees; the webhook decides what actually happened.
Correct division of responsibility:
Redirect -> "which page should I show this browser"
Webhook -> "did this payment actually succeed, and should
anything in my system change because of it"
Never write order-fulfilling logic (granting access, shipping a product, marking an invoice paid) inside a redirect-handling route. That logic belongs in the webhook handler, full stop. The redirect handler's only job is to read the current status from the database and render the right page — it should not be the thing that sets that status in the first place.
Even the webhook isn't instantaneous or perfectly ordered — a gateway can (rarely) deliver events out of order, or a webhook can be delayed by seconds. This is why the customer-facing "success" page usually shows a brief "confirming your payment…" state rather than assuming the webhook has already landed by the time the redirect completes — the two requests race, and the UI should tolerate the webhook winning that race a moment after the redirect does.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. A developer writes: "if the browser reaches /checkout/success, mark the order as paid." What's wrong with this?
2. Where should the logic that actually marks an invoice as paid and grants product access live?
3. A real, successful payment sometimes never triggers the redirect-back at all (closed tab, dropped connection). What does this imply about a design that only fulfills orders via the redirect?
4. Why might a payment confirmation page show a brief loading/confirming state rather than immediately rendering success or failure?
Payments & Financial Systems