The most common complaint in online payments, "I paid but my order says unpaid", is usually a webhook problem. The payment went through. The customer's browser never made it back to the store, and the server-to-server message that should have marked the order paid was missed, rejected or processed wrongly. Nothing was lost except trust.
Webhooks look simple: your payment provider sends an HTTP request to a URL on your server when something happens. Getting them right means handling the things that make them hard: they can be forged, retried, duplicated, delayed and delivered out of order. This guide covers each, for developers building an integration and for product owners who want to know what to ask for.
Return URL, webhook and status API
Three mechanisms tell your system about a payment, and they're not equally trustworthy.
| Compared on | Return (redirect) URL | Webhook | Status API |
|---|---|---|---|
| Who sends it | The customer's browser or app | The provider's servers, to yours | Your server asks the provider |
| When | When the customer comes back, if they do | When the event happens, with retries | Whenever you ask |
| Can it be lost? | Often: closed tabs, lost signal, app switches | Rarely, if you acknowledge it and the provider retries | No, but you must remember to ask |
| Can it be faked? | Easily, by the customer | Not if you verify its signature | No |
| Use it for | Showing the customer a page | Updating orders and triggering fulfilment | Checking stuck orders and answering support queries |
Providers name these differently. Some call the webhook a "callback" or "notification URL"; some use "callback URL" for the browser return. What matters is who sends it: anything that travels through the customer's browser is a display hint, never proof of payment.
What a webhook delivers
Event names and formats vary by provider, but most send events in these families:
- Payments: authorised, captured or succeeded, failed, pending.
- Refunds: created, processed, failed.
- Disputes: a chargeback or dispute raised against a payment, and its outcome.
- Settlements: a settlement paid to your bank, with its reference.
- Payouts: queued, processed, failed or reversed, with the bank reference when it succeeds.
- Mandates: a UPI AutoPay or card mandate created, paused, revoked or debited.
Each event normally carries an event ID, an event type, a timestamp, and the object it's about: the payment, refund or payout, including your own order reference if you passed one. That reference is what lets you find the right order without guessing.
Rule 1: verify every webhook
Your webhook URL is reachable from the internet, so anyone can send it a request that looks like "payment successful". If your server believes it, you've given away goods. Providers prevent this by signing each webhook, typically by computing a keyed hash (an HMAC) of the request body with a secret shared only between you and the provider, and sending the result in a header.
- Read the raw request body exactly as received, before any JSON parsing or reformatting. Signatures are computed over the exact bytes.
- Compute the signature the same way the provider's documentation describes, using your webhook secret for that environment.
- Compare it to the signature header using a constant-time comparison, and reject the request if they differ.
- If the provider includes a timestamp in what's signed, reject events older than a few minutes, so a captured webhook can't be replayed later.
- Only then parse the body and act on it.
Rule 2: acknowledge fast, process after
Providers expect a quick success response (an HTTP 2xx). If your endpoint is slow, times out or returns an error, the provider assumes delivery failed and retries later, usually several times with growing gaps. So the endpoint should do as little as possible before replying:
- 1ReceiveRaw body and headers
- 2VerifySignature and timestamp
- 3StoreSave the event with its ID
- 4AcknowledgeReturn 2xx immediately
- 5ProcessA background job updates orders, sends emails, ships
Return an error only when you genuinely want a retry, for example when your database is unavailable. Returning success for a webhook you failed to store means the provider won't send it again.
Rule 3: expect duplicates
Retries mean the same event can arrive twice, or five times, especially when your acknowledgement was slow. Your processing must be idempotent: handling the same event again must have no further effect.
- Record each event ID when you store it, with a uniqueness rule in your database, and skip events you've already processed.
- Make actions conditional on state, not on the event: "mark paid if not already paid", "ship if not already shipped".
- Send customer emails and messages from your state changes, not directly from the webhook, so duplicates don't become duplicate emails.
Rule 4: expect events out of order
Events aren't guaranteed to arrive in the order they happened. A "payment pending" webhook delayed by a retry can arrive after "payment succeeded". If you simply apply each event as it comes, a paid order can slip back to pending.
The fix is to treat payment status as a state machine with allowed transitions. Pending can move to succeeded or failed; succeeded can move to refunded or disputed; nothing moves a succeeded payment back to pending. When an event would make an illegal move, log it and ignore it. If in doubt, fetch the current status from the provider and use that.
| Current state | Event arrives | What to do |
|---|---|---|
| Pending | Succeeded | Mark paid and fulfil |
| Succeeded | Pending (delayed) | Ignore; a succeeded payment doesn't go back |
| Failed | Succeeded | Check the status API; late successes do happen, so act on the provider's current status |
| Succeeded | Refund processed | Record the refund against the payment |
Rule 5: don't rely on webhooks alone
Webhooks are reliable, not infallible. An endpoint outage longer than the provider's retry window, a misconfigured URL after a deploy, or a bug that acknowledged events without storing them can all leave gaps. Two backstops catch them:
- A scheduled job that checks, through the status API, every order still pending after a set time, such as 15 or 30 minutes. This is what resolves UPI payments that stayed pending, which our guide to UPI payment pending explains from the customer's side.
- Daily reconciliation that matches your orders against the provider's transaction and settlement reports, so any payment your webhooks missed shows up as an exception. Our payment reconciliation guide sets out the routine.
Rule 6: keep the endpoint and secret safe
The webhook endpoint is part of your payment security, and deserves the same care:
- Serve it only over HTTPS.
- Keep the webhook secret in server-side configuration, separate for test and live, and rotate it if it might have leaked.
- Don't put the secret, or anything that bypasses signature checks, in a query string or log file.
- Return generic errors; don't echo internal details back to the caller.
- If your provider publishes the addresses it sends from, an allowlist adds a layer, but never replaces signature verification.
The rest of the picture is in our payment gateway security guide.
Rule 7: log and monitor
When a customer says they paid and you can't see it, the webhook log is the first place to look. Store each event's raw body, headers, receipt time, verification result and processing outcome. Then watch a few numbers: failed verifications (a spike can mean a wrong secret after a deploy, or an attack), processing failures, the age of the oldest unprocessed event, and orders pending longer than normal. Alert on changes, not just thresholds.
Testing webhooks before you go live
Most providers can send test events from their sandbox, and many let you resend a past event. Use that to test the hard cases, not just the happy path:
| Test | Expected result |
|---|---|
| Valid payment-succeeded event | Order marked paid once; fulfilment starts |
| Same event sent twice | Second one recognised and skipped |
| Event with a tampered body or wrong signature | Rejected with an error; nothing changes |
| Old event replayed (if timestamps are signed) | Rejected |
| Pending event arriving after succeeded | Ignored; order stays paid |
| Endpoint down for a few minutes | Events retried and processed after recovery |
| Refund and dispute events | Recorded against the right payment |
Our guide to payment gateway integration has a wider test plan covering the checkout itself.
With more than one provider
Each provider has its own event names, payload shapes, signature scheme and retry behaviour. With two or three providers, the cleanest approach is to verify each provider's webhooks with its own method at the edge, then translate them into one internal event format before any business logic sees them. Orchestration platforms do this for you: LILPE's unified webhooks page describes how provider callbacks are verified, normalised into one signed event format and delivered to your endpoint.
Frequently asked questions
What is a webhook in a payment gateway?
An HTTP request your payment provider's servers send to a URL on your server when something happens, such as a payment succeeding, a refund being processed or a payout failing. It's the reliable way for your system to learn a payment's result.
What is the difference between a webhook and a callback URL?
Providers use the terms differently. What matters is who sends the request: a return or redirect through the customer's browser can be lost or faked and is only for showing a page, while a server-to-server webhook, verified by its signature, is what you should use to update orders.
How do I verify a payment webhook?
Compute the signature over the raw request body using your webhook secret, exactly as your provider documents, and compare it with the signature header using a constant-time comparison. Reject mismatches and, if timestamps are signed, events that are too old.
Why did I receive the same webhook twice?
Providers retry when they don't get a quick success response, and sometimes even when they did. Duplicates are normal, so record event IDs and make your processing idempotent.
What if my server was down and missed webhooks?
Providers usually retry for a period. For anything beyond that, a scheduled status check on pending orders and daily reconciliation against the provider's reports will catch what was missed. Many providers also let you view and resend past events from their dashboard.




