Payments
Webhook-verified payments: how to stop paid orders going missing
Short answer: create every order on your server, treat the customer's browser redirect as a hint rather than proof, confirm payment only from the provider's signed webhook, make webhook handlers idempotent, and run a reconciliation job that compares provider records with your database.
The bug every marketplace meets eventually
A customer pays, sees a success screen at the payment provider, and then lands back on your site to find their order still marked unpaid. Or worse: the browser tab closed during the redirect and your system never heard about the payment at all. Support tickets follow, and trust drops.
We designed Wooziee's ticketing checkout — Razorpay in India and Stripe in the UK edition — around preventing exactly this. The pattern below works with any provider that offers server-side orders and signed webhooks.
1. Create the order on your server first
Before the payment screen opens, your back end creates an order with the amount, currency, items and a unique reference, and asks the provider to create a matching payment order or session. The client only receives the provider's order ID — never the authority to decide the amount.
- Amounts and inventory are calculated server-side, so they cannot be changed in the browser.
- Every payment has a matching internal order before any money moves.
- Inventory can be held for a short window while the customer pays.
2. Treat the redirect as a hint, not proof
When the customer returns from the payment page, show a “confirming your payment” state and poll your own API. Do not mark the order paid based on query parameters in the redirect URL — they can be missing, delayed or forged.
3. Confirm with a signed webhook
The provider calls your webhook endpoint server-to-server when a payment is captured. Your handler should:
- Verify the signature using the provider's webhook secret before reading anything else.
- Look up the internal order by the provider's order or session ID.
- Check the amount and currency match what you expected.
- Mark the order paid and issue the tickets, licences or goods — inside one database transaction.
- Return a success status quickly, and push slow work (emails, PDFs, notifications) onto a queue.
4. Make handlers idempotent
Providers retry webhooks, and sometimes deliver events out of order. Store each processed event ID and skip duplicates; guard state transitions so a “paid” order cannot be paid again, and a refunded order cannot be re-fulfilled. Idempotency turns retries from a risk into a safety net.
5. Reconcile on a schedule
Even with webhooks, something will eventually slip: an outage, a misconfigured endpoint, a manual refund in the provider dashboard. A scheduled job that compares the provider's payments with your orders catches these before customers do, and flags anything it cannot resolve automatically for a human.
6. Payouts and refunds need the same care
Marketplaces also pay money out — to organisers, sellers or partners. Treat payouts (for example Razorpay X) and refunds as first-class records with their own states, provider references and reconciliation, rather than side effects of an order update.
A checklist you can use today
- Orders and amounts are created server-side
- Webhook signatures are verified before processing
- Event IDs are stored and duplicates ignored
- Fulfilment happens in the same transaction as the status change
- Slow work runs on a queue with retries
- A reconciliation job runs at least daily and alerts on mismatches
If your payment flow does not tick every box, our API & integration engineering team can review it, or start with a fixed-scope codebase audit.