Polling and webhooks without duplicate payment completion

Polling and webhooks may both retrieve a payment status, but the payment transition must be processed only once. Treat both channels as competing messengers of the same provider state. Have them perform the same checks, lock the critical transition and reread the order immediately before changing its status.

This race is normal with terminal payments. The browser asks every few seconds whether the customer has finished, while the payment provider sends a message at almost the same time. Without coordination, both processes may reduce stock, send emails or start downstream integrations. Disabling one channel is not the solution: polling provides immediate feedback, while the webhook covers a closed browser.

Make the order transition idempotent

Define one transition from ‘awaiting payment’ to ‘payment received’. Each processor first checks whether that transition has already been processed. It then briefly claims the right to carry it out, retrieves the current provider payment if necessary and checks the reference and amount. WooCommerce can then set the order to ‘Processing’ when fulfilment is still needed, or ‘Completed’ when the process allows it.

A lock does not need to last for minutes. It must last long enough to keep two simultaneous requests apart and be able to expire automatically if a process stops unexpectedly. Also store the provider reference against the order. A repeated message can then be recognised without relying on timing.

Keep the lock as local as possible: preferably per order or payment attempt, rather than across all transactions. A global lock would unnecessarily make different customers’ payments wait for each other. Use a token so only the owner can release the lock. Remove it in a guaranteed cleanup step, but also rely on an expiry time in case the process terminates abruptly.

Use one decision tree for both channels

Check Polling Webhook
The order exists and can be paid Required Required
Retrieve the current provider status Yes Preferably verify this too
Compare reference and amount Yes Yes
Use a short order lock Yes Yes
Skip an already completed order Yes Yes
Show user feedback Directly in the browser Through the next status display

Do not let the webhook blindly trust the supplied result when the provider offers a reliable way to retrieve it. A message may arrive late, twice or out of sequence. Querying the provider again costs an extra request but provides one consistent source for both paths.

Also define which statuses represent progress that only moves forwards. ‘In progress’ must not reverse an already confirmed payment. A later refund or formal cancellation is a new business event and needs its own route, rather than a simple backwards status change. Preserve the distinction between provider status and WooCommerce order status; they do not always answer the same question.

Fictional example: two confirmations in the same second

Bicycle shop Spaak starts a terminal payment for a collection order. At 14:07:12, the server receives a webhook. A fraction later, the browser tab requests the same status. The webhook process acquires the lock, reads the provider status and processes the payment. The polling process waits briefly, then reads the fresh order and sees that no second payment transition is needed. The bicycle still needs to be prepared operationally.

The customer receives one confirmation email, stock is processed once and the ERP receives one order event. Both requests may write a technical note that they observed the status, provided logs do not cause duplicate business actions. A central integration layer such as Yindle Connect can then route order events and exceptions to other systems in a manageable way.

Spaak then tests a slow provider query. The first request holds the lock longer than expected; the second does not report a false payment outcome but retries after a short pause. If the waiting time is exceeded, the browser accurately continues to report that checking is in progress. Technical uncertainty is not translated into a dangerous ‘pay again’ button.

Test the race before production

  1. Send the same provider message several times with the same reference.
  2. Have polling and the webhook process a successful status at exactly the same time.
  3. Interrupt the first process after it acquires the lock.
  4. Check that an expired lock allows recovery.
  5. Process ‘succeeded’ first, then an older ‘in progress’ message.
  6. Check stock, email, invoicing and ERP exports for duplicate actions.
  7. Repeat the test for cancelled, declined and unknown.
  8. Make the logs show which channel performed the transition.

Monitor errors as well as repeated messages that were correctly ignored. A sudden increase may indicate provider retries, slow responses or an incorrectly configured polling frequency. The same principles apply beyond payments; reliable online store–ERP integrations have the same need for controlled status transitions.

Let Yindle design a payment and order flow that withstands race conditions.

The shared decision path for polling and webhooks belongs with Yindle Terminal Payments, which uses the same authenticated verification for browser status, webhooks and recovery.

Previous insight
Next insight

Start with your question

Where does your team get stuck?

Your first enquiry does not need to be a technical brief. Tell us which systems you use and where work gets stuck. Together we discuss what is possible and which step you can take next.

Discuss my project