Order and Payment Status on Your Online Store: Keeping Webhooks, Orders and Refunds in Sync
Your order records and your payment gateway stop agreeing for one structural reason: the customer's browser, the gateway's servers and your store's database are three separate systems. The fix is a set of things a developer builds on purpose: a signed webhook endpoint that answers quickly, an idempotent handler that can safely see the same event twice, an explicit order and payment state machine, a log of every event received, an admin screen that shows disagreements, and a scheduled job that asks the gateway what really happened. With those in place, a paid order shows as paid and a refund is never recorded twice. Without them, you find out through customer complaints.
This article is for store owners who must brief or evaluate a developer, and for developers who want a checklist. It replaces an earlier version of this page that approached the subject as an accountant's settlement exercise. That subject is real, but it belongs to your accountant; this site is about building websites. What follows covers only what a developer controls. It does not rank gateways or discuss charges, tax or accounting.
Why Storefront Order Status and Gateway Status Drift Apart
The shopper clicks Pay, enters their details on the gateway's page, approves, and lands back on your thank-you page. The money moves inside the gateway's systems, and your store learns about it by one of two routes: the browser coming back, or a server-to-server message from the gateway, the webhook. Drift comes from relying on the wrong route, or from the right one misbehaving.
The browser closes after payment. The tab closes, the signal drops or a call arrives. If your store marks the order paid only when the thank-you page loads, the gateway has the money and your store shows an unpaid order.
Redirect-only confirmation. A thank-you page that trusts "payment=success" in the address bar can be reached by editing the link, and reached twice by refreshing. The redirect should show the customer a result; the order state should change only on verified information from the gateway.
Duplicate callbacks. Senders deliver at least once, not exactly once. If your endpoint is slow, or errors after doing the work, the sender retries. A handler that adds a payment record each time produces double entries, double emails and possibly two shipments.
Delayed or failed webhooks. The server was being deployed, the endpoint timed out, a firewall blocked the sender or a plugin returned an error page. The sender retries for a while and gives up, and the order stays pending for ever.
Out-of-order events. A "refund created" message can arrive before the "payment captured" message. Shopify's documentation says it does not guarantee ordering within a topic or across topics for the same resource, and that delivery is not always guaranteed. Design for that as normal behaviour on any platform.
Records will drift; the question is whether the system notices and repairs it.
What Your Developer Must Build
Six parts: a signed webhook endpoint that answers quickly; an idempotent processor; an explicit order and payment state machine; an event log; a reconciliation view in the admin; and a scheduled sync job. These should appear in the scope of any build. Our payment gateway integration service treats them as part of the integration, because a gateway that takes money without reliably updating the order is only half connected.
Webhook Design: The Details That Matter
Verify the signature
A webhook endpoint is a public URL, so anyone can send it "order 1042 is paid". The defence is a signature: the sender computes a keyed hash of the raw body with a shared secret and puts it in a header, and you recompute and compare. Shopify documents the X-Shopify-Hmac-SHA256 header, and WooCommerce lets you set a secret on a webhook that generates a signature in the request headers. Gateways document their own header names and algorithms.
Hash the raw bytes before any framework re-serialises them, compare in constant time, reject and log bad signatures, and keep the secret in server configuration, not in the repository or front-end code.
Acknowledge fast, process later
Senders expect a quick answer. Shopify's documentation describes a one-second connection timeout and a five-second limit for the whole request, and treats any non-2xx response as an error. Check the current limits for your sender.
So the endpoint verifies the signature, saves the raw event to a table and returns success. A background worker then does the real work: updating the order, sending email, creating the shipment, adjusting stock. Doing it all inside the request causes timeouts, timeouts cause retries and retries cause duplicates. Return a failure only if you genuinely did not save the event; a downstream outage belongs to your own queue.
Make every handler idempotent
Idempotent means doing the same thing twice has the same effect as once. There are two layers.
Event-level de-duplication. Every delivery carries an identifier. Shopify documents the X-Shopify-Webhook-Id header for detecting duplicates against a persistent store, and gateways put an event id in the payload or headers. Put a unique constraint on it in your event table; if the insert fails, acknowledge and skip.
Effect-level safety. "Set order 1042 to paid if it is pending or authorised" is safe to run twice. "Add the payment amount to the amount paid" is not. Prefer setting a state over incrementing a number, and record payments against the gateway's payment id with a unique constraint.
The same applies outbound. If a customer double-clicks Pay or your server retries a timed-out create call, an idempotency key or your own unique order reference lets the gateway return the existing payment. Read your gateway's current documentation for its mechanism.
Plan for ordering
Do not let a late, old event overwrite a newer state. Use the event's timestamp where provided; Shopify recommends the X-Shopify-Triggered-At header or the payload's updated_at. Enforce the state machine below, so an impossible move is parked and retried later, not applied blindly. Where possible treat the webhook as a nudge: read the event, fetch the current payment from the gateway's API and update from that. One extra call removes most ordering problems.
Retries, replay and the dead-letter queue
Your own worker needs retries too: increasing delays, a limited number of attempts, then a dead-letter table of events that need a person. The admin should list it with a Replay button, otherwise a bug fix on Tuesday does nothing for events that failed on Monday.
Remember what the sender does when you fail. Shopify's documentation says it retries eight times over four hours, after which subscriptions created through the Admin API are removed. WooCommerce's says a webhook is disabled after more than five consecutive delivery failures. A bad week can silently switch notifications off, so the sync job and alerts below are not optional.
The Order and Payment State Machine
A state machine is a written list of the states an order can be in and the moves allowed between them. Most stores have one by accident, scattered across plugins. Write it down, keep it in one place and route every update through it. Keep two things apart: the order status, which describes your workflow, and the payment status, which describes what the gateway did with the money. Mixing them in one field is how "cancelled but captured" orders appear.
| State | Meaning | Allowed next states |
|---|---|---|
| Pending | Order created, no payment outcome yet | Authorised, Captured, Failed, Cancelled |
| Authorised | Funds reserved, not yet taken | Captured, Cancelled (void), Failed |
| Captured | Payment completed | Partially refunded, Refunded |
| Failed | Payment declined or errored | Pending (new attempt), Cancelled |
| Partially refunded | Part of the captured amount returned | Partially refunded again, Refunded |
| Refunded | Whole captured amount returned | None (final) |
| Cancelled | Abandoned or voided before completion | None (final) |
Many checkouts capture immediately, so Authorised is brief or absent, and cash-on-delivery orders have their own path. What matters is that "can it move from A to B?" has one answer in code, with three rules: only the state machine changes status, whether the request comes from a webhook, the thank-you page, an admin button or the sync job; impossible moves are parked with a reason, not applied or dropped; and every transition records the old state, new state, cause, gateway reference and time.
Where the browser fits
The thank-you page should show the best available information, not decide the outcome. When the shopper returns, show "We are confirming your payment" and ask your server for the order's current state. If the webhook has landed, show success; if not, query the gateway once and poll briefly. If still unresolved, say honestly that an email will follow, and let the webhook or sync finish. Our guide to checkout optimisation for Indian stores covers the wider checkout experience.
What to Log, and What Not to Keep
For every incoming event store the gateway's event id and type, your order id, the gateway's payment or refund id, time received and processed, the result (processed, duplicate, ignored, failed, with error), the signature check result and the state before and after. For every outgoing call store the idempotency key, response status and gateway id.
Raw payloads and personal data
Raw payloads are good evidence but often hold names, emails, phones and addresses. Under India's Digital Personal Data Protection Act, 2023, personal data should be kept only as long as the purpose needs, and secured. Our guide to DPDP compliance for websites covers this; take legal advice for your case.
Keep the structured fields indefinitely, since they describe transactions, not people. Keep raw payloads for a short defined window, long enough to debug and replay, then delete or strip personal fields. Never log card details or secrets, restrict who can read the event table, and set retention with your accountant and legal adviser, since other laws may require some records.
The Reconciliation View Your Admin Should Have
Here "reconciliation" means only: does the store's record match the gateway's, order by order. It is an operations screen, not an accounting report. Ask for a permission-controlled admin page listing:
- Stuck pending orders older than a set period, with the gateway's latest status beside them.
- Status mismatches, such as paid at the gateway and pending in the store.
- Refund mismatches in either direction.
- Failed events from the dead-letter list, with errors and a Replay button.
- Duplicate payments against one order.
- Webhook health: time of the last event received and failures in the last day.
Each row should link to the order, its event history and the gateway record, and offer safe actions: refresh from gateway, replay, mark for review. Anything that moves money needs a permission level and an audit entry. The aim: support resolves "I paid but my order says pending" without a developer.
The Scheduled Sync Job as a Safety Net
Webhooks are fast but not guaranteed; a scheduled job is slow but dependable. Shopify's documentation recommends a reconciliation job that periodically retrieves data you may have missed. Design it simply:
- Every few minutes, check orders that are Pending or Authorised, older than a few minutes, against the gateway.
- Nightly, fetch recent payments and refunds for a window such as two days and compare.
- Apply differences through the same state machine and idempotent handlers, never a separate path.
- Log changes, and alert when it fixes many orders, since that means webhooks are failing.
Give pending orders a final fate: after a defined period, cancel, release stock, and route any late payment to manual review.
Platform Specifics: Shopify and WooCommerce
Platform behaviour changes; check current documentation before building.
Shopify
Shopify runs the checkout, order and transactions, so with a supported gateway much status handling is done for you. Developer work arises when your own app or external systems depend on Shopify events. From its webhook documentation: verify the HMAC signature before processing; respond quickly with 2xx; failed deliveries are retried and subscriptions can be removed after repeated failure, so monitor that they still exist; de-duplicate on the webhook id; expect no guarantee of delivery or order; and run a periodic reconciliation job.
Its transaction model distinguishes authorization, sale, capture, void and refund, so payment history is a sequence of records, not one flag. Its documentation also describes REST as legacy and expects new public apps to use GraphQL, so ask an agency which API it builds on. Our Shopify payment gateway integration guide separates configuration from custom work.
WooCommerce
WooCommerce documents these statuses: Pending payment (received, no payment made), Processing (paid, stock reduced, awaiting fulfilment), On hold (awaiting payment confirmation), Completed, Failed, Cancelled and Refunded (fully refunded by an admin), plus a Draft status used by block checkout before submission. The normal path is Pending payment, Processing, Completed.
- A gateway plugin moves an order out of Pending payment, usually through its own callback URL on your site. Confirm the URL is reachable and not blocked by a security plugin, firewall, caching or login wall.
- WooCommerce's own webhooks are disabled after more than five consecutive failures, and deliveries are logged under Status, Logs. Check after any outage.
- Action Scheduler, the bundled job queue, runs on WP-Cron and loopback requests per its documentation, so on a quiet site a sync can run late. Ask about a real server cron.
- Test that a refund made in the order screen reaches the gateway, and one made at the gateway returns to the order.
Generic plugins rarely cover custom statuses, cash-on-delivery verification or courier and ERP links. That is where our ecommerce development service and checkout system service apply.
A Testing Plan Before Launch
These failures do not appear in a smooth demo. Run them in the gateway's sandbox on staging; see our guide to staging before launch.
- Happy path: one paid order, one email, one stock deduction.
- Close the browser before the redirect; the webhook must still mark it paid.
- Back button and refresh on the thank-you page; nothing duplicates.
- Double click Pay, and open checkout in two tabs; one order, one payment.
- Duplicate webhook: replay one event three times; nothing changes after the first.
- Delayed webhook: block the endpoint and confirm the sync fixes the order.
- Out-of-order events: send refund before capture; it is parked or ordered.
- Failed then successful retry; history stays clear.
- Bad signature: rejected and logged.
- Partial and full refunds from admin and gateway; both end in the same state, and over-refunding is refused.
- Email or courier calls failing: the webhook is still acknowledged and the job retried.
- Endpoint down briefly: sender retries and the sync recover the events.
Repeat a short version live with a small real transaction, and after any checkout, plugin or hosting change.
Alerting: Know Before the Customer Writes
Alert a named person, by email plus a team chat message, on: no webhook for an unusual period; rising signature or processing errors; dead-letter events; orders pending beyond the threshold; the sync failing or finding many differences; a disabled or removed webhook; and endpoint certificate expiry. An alert nobody reads equals no alert. See also our website uptime monitoring guide.
Pre-Launch Checklist
- Order and payment status stored separately, with documented transitions and one function allowed to change them.
- Thank-you page displays state but does not set it.
- Signatures verified on the raw body; events saved and acknowledged fast; work done in a queue.
- Unique event ids; outbound calls carry idempotency keys.
- Retries with limits, a dead-letter list and replay.
- Scheduled sync, pending-order timeout and a rule for late payments.
- Reconciliation view and permission-controlled refunds.
- Payload retention limit; no card data or secrets in logs.
- Alerts tested and owned; secrets in server configuration with a rotation process.
- The twelve tests passed on staging, and a live smoke test passed.
What to Ask a Developer Before You Hire or Sign Off
- How does the store learn of a payment if the shopper closes the browser?
- What happens if the same webhook arrives three times? Show me the unique constraint.
- Where is the state machine, and what code may change an order's status?
- How are out-of-order events, failed events and replays handled?
- How often does the sync run, and what alerts exist?
- What personal data is logged, for how long, and who can read it?
Vague answers to the first three suggest only the happy path was tested.
Working with Govindani Infotech
Govindani Infotech is a website and ecommerce development company in Pune. If your store already shows paid orders as pending, or you are about to launch a checkout and want these six parts built in, our payment gateway integration service covers webhook handling, order states and admin tooling, and our ecommerce development service covers the wider store build on Shopify, WooCommerce or custom code. If you are replacing a whole checkout, see our checkout system service. Refunds also connect to returns; see building a returns page and return-request flow.
You can message us on WhatsApp or use the contact page with your platform, gateway and a description of what goes wrong. We will tell you what is a configuration change and what needs development, and confirm scope and pricing against your requirements.
Frequently Asked Questions
Why does my store show an order as pending when the customer says they paid?
Usually the store never received, or never processed, the confirmation from the gateway. Common causes are a closed browser, a down or blocked endpoint, a failing signature check or a handler that errored part-way. A scheduled sync that asks the gateway for pending orders, plus a reconciliation view in the admin, is how you find and fix these without waiting for a complaint.
Is the thank-you page enough to confirm a payment?
No. The page can be missed if the shopper closes the browser, and a redirect with a success parameter can be forged or reloaded. Use it to show the customer a result, but base the order state on a verified webhook or a direct query to the gateway's API.
What is an idempotency key and why does a store need one?
It is a unique value sent with a request, or stored with an event, so that repeating the same request has the same effect as sending it once. It stops double charges from double clicks, duplicate orders from retries and duplicate processing of repeated webhooks. Check your gateway's current documentation for how its idempotency support works.
Do I need all of this on a small Shopify or WooCommerce store?
You need the principles, not necessarily custom code. A standard Shopify checkout handles most of this on the platform side. On WooCommerce, choose a well-maintained gateway plugin, confirm that its webhook URL works, run the tests above and check the pending and on-hold orders regularly. Custom work pays off once you add custom statuses, cash-on-delivery verification or external systems.
How long should I keep raw webhook payloads?
Keep structured transaction fields as long as operations need them, and raw payloads only for a short window long enough to debug and replay. Payloads can contain personal data, so set the period with your legal and accounting advisers, considering the DPDP Act and any record-keeping rules.