API & Webhook Reliability
WooCommerce Webhook Fires More Than Once: Prevent Duplicate Processing Without Dropping Real Events
When a WooCommerce webhook appears to fire twice, first prove what actually repeated. Then protect the downstream business operation with atomic idempotency, durable state and reconciliation.
A WooCommerce webhook reaches your application.
The receiver creates a shipment, inserts a CRM record, sends a customer message or starts fulfilment. A short time later, something that looks like the same event reaches the system again.
Now there are two shipments, two records or two executions of an operation that should have happened once.
It is easy to describe that as a duplicate webhook. That description may be correct, but it is not yet a diagnosis.
The order may have changed twice. Two active webhook configurations may point to the same endpoint. Another layer may have replayed a request. Two workers may have raced to claim the same operation. The receiver may also have accepted one webhook correctly and duplicated the work later in its own queue.
Before suppressing anything, identify the boundary where one intended operation became two.
The short answer
Do not deduplicate WooCommerce webhook processing by order ID alone. An order can have many legitimate updates.
X-WC-Webhook-Delivery-ID is useful for tracing a delivery, but it does not automatically identify the business operation your integration must protect.
A safer receiver does four things well:
- authenticates the incoming webhook
- records enough evidence to correlate deliveries
- derives an idempotency key from the business operation
- claims and tracks that operation durably before irreversible work is repeated
If the external side effect has an uncertain outcome, reconcile it before retrying.
The practical target is simple:
Repeated delivery must not repeat an irreversible business operation, while legitimate future changes still reach the downstream system.
First prove what actually repeated
Consider an order.updated webhook for order 18452.
Another order.updated request arrives for the same order a minute later.
That does not prove WooCommerce duplicated a delivery. The order may have changed twice.
The second update could reflect:
- a payment-state change
- a shipping-address edit
- fulfilment metadata
- a tracking update
- a status transition
- custom integration metadata
- another extension writing to the order
The resource ID tells you which order changed. It does not tell you whether the same business operation is being requested again.
Treat duplicate processing as a correlation problem first.
WooCommerce has narrow duplicate suppression in core
Current WooCommerce core already avoids some repeated scheduling.
In WooCommerce 11.0.1, each WC_Webhook instance keeps a list of resource arguments it has processed during the current PHP request. If multiple hooks for the same webhook/resource fire inside that request, the same resource argument is not processed again by that webhook instance.
The asynchronous queue path has another narrow guard. Before adding woocommerce_deliver_webhook_async, WooCommerce checks for an existing scheduled action with the same webhook ID and resource argument inside its compatibility window.
These safeguards solve specific WooCommerce scheduling cases.
They do not establish one immutable event identity across every request, worker and downstream system.
They also do not answer the most important integration question:
Which business operation must be protected from running twice?
Do not assume every repeated request is a core WooCommerce retry
WooCommerce’s public webhook guide currently describes a webhook being disabled after repeated unsuccessful deliveries.
The released core code is more useful when diagnosing what that means. The delivery result updates a consecutive failure count and can disable the webhook when the configured threshold is exceeded. That failure path does not itself show a reschedule of the same failed HTTP delivery.
WooCommerce engineering discussion has also described automatic retry and replay as reliability functionality that core lacked and proposed adding it.
So when two similar requests reach your receiver, do not label the second one “WooCommerce retry” without evidence.
A retry may come from:
- custom WooCommerce code
- a receiving queue
- SaaS webhook infrastructure
- a workflow platform
- a manual replay
- another integration layer
First establish who created the second attempt.
Build a webhook fingerprint
Capture at least two examples involved in the incident.
A useful fingerprint is:
| Evidence | Why it matters |
|---|---|
| Exact receive timestamp | Correlates WooCommerce, receiver and downstream logs |
| X-WC-Webhook-ID | Identifies the WooCommerce webhook configuration |
| X-WC-Webhook-Delivery-ID | Correlates a specific delivery attempt |
| Topic and event | Shows the class of WooCommerce change |
| Resource ID | Identifies the order, product or customer |
| Payload hash | Shows whether raw bodies are byte-identical |
| Relevant business state | Explains what the resource represented at that time |
| Receiver request ID | Traces the request inside your application |
| Operation key | Identifies the business action being protected |
| Downstream ID | Links the operation to a shipment, payment or external record |
| HTTP response | Shows what WooCommerce received from the endpoint |
| Processing duration | Helps reveal timeout and concurrency patterns |
Do not log customer data that is irrelevant to the investigation.
The objective is correlation, not a larger data-retention problem.
Idempotent webhook processing map
Protect the operation behind the delivery
Authenticate and correlate the transport first. Then derive and atomically claim the business operation before any irreversible side effect can repeat.
Layer 1
Transport identity
-
01
WooCommerce resource change
Order, product or customer state changes
Legitimate repeated resource update -
02
Webhook scheduled and delivered
A configured endpoint receives the request
Duplicate webhook configuration -
03
Verify signature
Authenticate the raw request body -
04
Correlate transport evidence
Webhook ID, delivery ID, resource and payload
Replay or receiver concurrency
Layer 2
Business-operation identity
-
05
Identify business operation
Define the side effect that must not repeat -
06
Atomic operation claim
One durable key, one processing owner
Layer 3 and 4
Claim result and safe outcome
-
Already succeeded
Load the recorded result- Return safely
- Do not repeat the side effect
-
New operation
This worker owns the key- Persist and process
- External side effect
- Record external ID
- Mark succeeded
Downstream worker or crash-recovery duplication
-
Failed or uncertain
The external outcome is not safely known- Reconcile first
- Retry only when safe
Protect the business operation, not merely the HTTP request.
Duplication class 1: two webhook configurations send to the same receiver
A store can accumulate old webhook registrations.
An old integration remains active. A replacement webhook is added later. Both use order.updated, and both point to the same receiver.
The downstream system sees two requests for one order change.
Compare X-WC-Webhook-ID.
If the requests come from different webhook IDs, inspect WooCommerce configuration before changing receiver logic. The source problem may be two active registrations.
The receiver should still protect irreversible operations, but an obsolete webhook should not remain active simply because downstream deduplication can hide it.
Duplication class 2: the resource legitimately changed again
This is where order-ID deduplication causes damage.
Imagine a receiver that stores:
processed_order = 18452
Every future webhook for that order is now ignored.
The first event may have represented a paid order. The next may contain a shipping change. A later update may contain tracking or cancellation information.
The same resource can participate in several legitimate business operations.
An order ID is usually a resource identity.
It is rarely a complete idempotency identity.
Duplication class 3: the same business operation is attempted again
Suppose an integration creates one shipment when an order becomes ready for fulfilment.
The receiver creates shipment SHIP-987. Before the local application records that outcome, the process fails.
Later, the fulfilment operation is attempted again.
If the code only knows that the webhook is being processed again, it may call the shipping API a second time.
The important identity is not the webhook request.
It is the business operation:
create shipment for this fulfilment scope
That operation needs a stable key that survives retries, process restarts and concurrent workers.
For example:
shipment:create:{order_id}:{fulfilment_scope}
If an order supports more than one legitimate shipment, the fulfilment scope must distinguish them.
For production patterns where shipment synchronization and duplicate protection matter, the same principles appear in DevFluxr’s WooCommerce shipping integration case study.
Duplication class 4: two workers race to claim the operation
A receiver can check for duplicates and still create them.
Consider two requests arriving almost together:
Request A: operation exists? no
Request B: operation exists? no
Request A: create shipment
Request B: create shipment
The weakness is the gap between checking and claiming.
A stronger design lets the database enforce one owner for the operation key.
Conceptually:
CREATE TABLE integration_operations (
operation_key varchar(191) NOT NULL,
status varchar(32) NOT NULL,
external_id varchar(191) NULL,
created_at datetime NOT NULL,
updated_at datetime NOT NULL,
PRIMARY KEY (operation_key)
);
Then processing behaves like:
attempt to insert operation_key
insert succeeds:
this worker owns the operation
duplicate-key result:
load existing operation state
do not start a second irreversible operation
The important property is the PRIMARY KEY or UNIQUE constraint. Two concurrent workers cannot both insert the same operation key successfully.
That is stronger than a cache flag or a separate “check first” query.
An atomic claim does not remove the external crash window
This is the part many idempotency examples leave out.
Suppose the local operation row is claimed successfully.
The worker calls the shipping API.
The shipping API creates SHIP-987.
Then PHP dies before the application records SHIP-987 locally.
The database claim prevented two workers from running concurrently, but the system is still uncertain about the external outcome.
A later worker cannot safely assume either:
nothing happened
or:
the operation completed
The recovery depends on the downstream system.
If the external API accepts its own idempotency key, reuse the stable operation key there.
If it exposes a reliable lookup or reconciliation endpoint, check whether the shipment already exists before creating another one.
If neither is available, the operation may need a terminal “uncertain” state for human or controlled reconciliation.
This is why idempotency and reconciliation belong in the same integration design.
For broader implementation work at this boundary, see DevFluxr’s WooCommerce API and webhook integration engineering.
Delivery identity and business identity answer different questions
WooCommerce includes headers such as:
- X-WC-Webhook-ID
- X-WC-Webhook-Delivery-ID
- X-WC-Webhook-Topic
- X-WC-Webhook-Resource
- X-WC-Webhook-Event
- X-WC-Webhook-Signature
They are useful, but they do different jobs.
Webhook ID
Which WooCommerce webhook configuration sent this request?
Useful for finding duplicate or stale registrations.
Delivery ID
Which delivery attempt is this?
Useful for correlation across WooCommerce and receiver logs.
Resource ID
Which order, product or customer is involved?
Useful for loading the relevant business state.
Operation key
Which business action must not run twice?
This usually belongs to your integration domain rather than WooCommerce transport metadata.
That distinction is central to a safe design.
Build the idempotency key around the side effect
There is no universal WooCommerce idempotency key.
Choose the identity according to the operation you are protecting.
Shipment creation
shipment:create:{order_id}:{fulfilment_scope}
Applying a payment transaction
Use a stable payment-provider transaction identity where that provider’s semantics make it appropriate.
Importing an ERP event
A stable external event/version ID may be the correct key.
Applying a specific transition
A useful identity may combine:
resource + intended transition + external operation identity
The key should describe the side effect closely enough that a legitimate future operation receives a different identity.
Payload hashes are evidence, not a universal idempotency key
Hashing the raw webhook body is useful when comparing suspicious deliveries.
If two hashes match, the request bodies were identical.
That still does not prove the business operation should be suppressed forever.
Two payloads for the same intended operation may also differ because:
- unrelated metadata changed
- a timestamp changed
- field order or serialization changed
- another extension added data
- WooCommerce built the payload from a later resource state
Use a payload hash as correlation evidence unless your integration semantics make it a trustworthy operation identity.
Verify the signature before trusting the event
Idempotency is not authentication.
WooCommerce signs webhook payloads using the configured secret. By default, current core generates a Base64-encoded HMAC-SHA256 signature over the encoded request body and sends it in X-WC-Webhook-Signature.
Validate against the raw request body.
Conceptually:
$expected = base64_encode(
hash_hmac( 'sha256', $raw_body, $secret, true )
);
if ( ! hash_equals( $expected, $provided_signature ) ) {
// Reject the request.
}
Do not decode JSON and then re-encode it for verification. A different byte representation can produce a different signature.
Signature verification answers whether the request can be trusted.
Idempotency answers whether trusted processing can repeat a business operation unsafely.
Both are required.
Separate durable acceptance from heavy processing
A webhook endpoint often becomes safer when it does less synchronous work.
A useful receiver flow is:
receive
→ verify signature
→ validate
→ derive operation identity
→ durably record/claim
→ enqueue controlled work
→ acknowledge
The worker then performs the expensive integration operation and records the result.
Do not acknowledge a request as safely accepted if nothing durable exists and losing the process would lose the operation.
The opposite design has its own failure mode. If the endpoint waits for a slow external API, the external side effect may succeed while the HTTP request times out or the process dies before the result is recorded.
When your receiver uses WooCommerce/WordPress background processing, keep the queue itself observable. If those jobs accumulate, the diagnostic model in WooCommerce scheduled-action backlog diagnosis applies to that internal processing boundary.
Track operation state, not a single “seen” flag
A durable operation record should explain what happened after the request was accepted.
Useful states can include:
- received
- The request was authenticated and durably accepted.
- processing
- A worker currently owns the operation.
- succeeded
- The intended side effect completed and the external identity was recorded.
- retryable_failed
- The operation failed in a way the recovery policy permits another attempt.
- terminal_failed
- Automatic processing stopped and reconciliation or human intervention is required.
An operation may also need an explicit uncertain state when the downstream system may have succeeded but the local result was not safely persisted.
That state is safer than blindly repeating an irreversible call.
Store downstream identifiers
After a successful external call, store the identity the downstream system returned.
For a shipment, that could include:
- operation key
- WooCommerce order ID
- fulfilment scope
- external shipment ID
- completion time
- integration status
A later request can now answer:
SHIP-987 already represents this fulfilment operation
That is stronger evidence than:
we saw a webhook for order 18452 earlier
HTTP response and business outcome are separate states
WooCommerce records the receiver’s HTTP response as delivery evidence.
A successful response means the endpoint accepted the HTTP interaction according to its contract. It does not prove an ERP row, shipment, CRM update or inventory reconciliation completed later.
Likewise, an external operation can succeed even if the receiver ultimately returns an error.
Keep these states separate:
delivery accepted
business operation succeeded
That separation makes incident recovery possible.
Do not return success for an invalid signature, malformed request or request that your architecture has not durably accepted.
Do not return an error after an irreversible side effect has already succeeded unless your recovery model can identify that state safely.
Inspect WooCommerce evidence before changing receiver logic
Current WooCommerce 11.x webhook delivery code exposes useful request metadata, response metadata, delivery duration and a delivery ID through the WooCommerce logging system.
Compare suspicious requests side by side.
A focused investigation asks:
- Did they come from the same webhook ID?
- Did they have the same delivery ID?
- Did they have the same topic and event?
- Did they involve the same resource?
- Were the raw payloads identical?
- Did the receiver assign the same operation key?
- Did both workers reach the downstream API?
- Did the downstream system create one result or two?
- What response did WooCommerce receive?
That evidence tells you where the duplication actually began.
Avoid temporary “ignore duplicates for ten minutes” locks unless the business operation itself has a ten-minute identity. A time window can hide a symptom while still dropping a legitimate update.
The duplicate may be downstream of WooCommerce
Sometimes WooCommerce sends one request and your endpoint receives one request, but two side effects still occur.
For example:
one webhook
→ internal job
→ worker A
→ retry worker B
→ both call fulfilment
Or:
one webhook
→ create shipment succeeds
→ local persistence fails
→ recovery repeats shipment creation
If you only inspect WooCommerce’s webhook configuration, you will never find that boundary.
Trace the complete chain:
WooCommerce trigger
→ webhook scheduling
→ HTTP delivery
→ receiver
→ internal queue
→ worker
→ downstream API
→ recorded result
Find the first point where one intended business operation becomes two.
Do not deduplicate legitimate state changes away
Overaggressive deduplication creates a quieter but more dangerous failure.
Suppose an ERP processes only the first webhook it ever receives for an order.
Later, the shipping address changes. The order is refunded. Tracking is added.
Those changes are silently ignored because the resource ID was marked “processed”.
For state synchronization, a different model can be safer:
receive notice that the resource changed
enqueue synchronization
fetch the current authoritative state
reconcile the other system toward that state
That approach does not require every intermediate order.updated notification to represent an irreversible command.
Event commands and state synchronization need different guarantees
Consider two integrations.
Command-like operation
Create one shipment for this fulfilment unit.
Repeating the external side effect is dangerous. Business-level idempotency is central.
State synchronization
Keep ERP inventory aligned with WooCommerce.
The important outcome may be convergence toward the current value rather than replaying every historical update.
Reliable integrations often combine both ideas:
idempotent commands
+
state reconciliation
Webhooks provide fast notification.
Reconciliation gives the system a recovery path when delivery or processing history becomes uncertain.
Add retries only after repeated execution is safe
Many real integrations have retry behavior even when core WooCommerce does not automatically replay the same failed delivery.
The retry may live in:
- a custom extension
- Action Scheduler
- the receiver’s queue
- a SaaS webhook platform
- a workflow engine
- a manual recovery tool
Before introducing or increasing retries, define:
- retryable errors
- retry delay
- maximum attempts
- maximum operation age
- idempotency key
- downstream idempotency behavior
- reconciliation path
- terminal failure state
A remote 429 may justify a delayed retry.
An invalid signature does not.
An operation with an uncertain external outcome should be reconciled before a retry that could create a duplicate side effect.
Test concurrency and crash recovery deliberately
A receiver that works in one Postman request has not proven idempotency.
Test at least:
- two concurrent requests with the same operation key
- a repeat after successful completion
- process failure after claim but before the external call
- process failure after external success but before local persistence
- a legitimate later update for the same WooCommerce order
- invalid signature
- delayed downstream API
- remote 429
- remote 5xx
The key verification question is:
Does the system produce the correct business result when delivery is repeated, delayed or concurrent?
Verification needs safety and liveness
Idempotency can fail in two directions.
Safety failure
The same business operation creates duplicate side effects.
Liveness failure
A new legitimate operation is rejected because the receiver thinks the resource has already been handled.
Verify both.
Repeated processing of the same operation key should not create another irreversible result.
A genuinely new operation for the same order should still process normally.
That is why “I sent the same webhook twice and got one row” is incomplete verification.
A practical investigation sequence
For duplicate WooCommerce webhook processing, use this order.
-
Preserve two or more examples
Record timestamps, webhook IDs, delivery IDs, topic/event, resource ID, payload evidence, responses and downstream results.
-
Identify the duplication boundary
Classify what happened:
- duplicate webhook configuration
- legitimate repeated resource update
- repeated delivery or replay
- receiver concurrency
- downstream duplicate processing
-
Define the protected business operation
Write the operation in business terms.
For example:
Create one fulfilment shipment for this fulfilment unit.
The operation definition determines its idempotency key.
-
Claim the operation atomically
Use a storage constraint or transactional boundary that prevents two workers from owning the same key concurrently.
-
Persist processing state
Track ownership, completion, retryable failure, terminal failure and uncertain outcomes where required.
-
Separate acceptance from expensive work
Durably accept the operation before returning success, then move expensive processing to controlled background work where appropriate.
-
Recover with bounded retries
Retry only operations whose failure class and downstream semantics make another attempt safe.
-
Reconcile the downstream state
When the external outcome is uncertain, check the downstream system before repeating the side effect.
-
Verify the next legitimate update
Confirm that the idempotency boundary does not suppress a new business operation on the same WooCommerce resource.
The success criterion
A reliable webhook integration does not depend on every HTTP request arriving exactly once.
It should preserve this business behavior:
Repeated or concurrent delivery cannot repeat an irreversible operation unintentionally. Legitimate future operations still run, and uncertain failures remain visible until they are reconciled.
That is the boundary worth protecting.
Preventing the next webhook incident
For important WooCommerce integrations, define these before launch:
- Authentication
- How does the receiver verify WooCommerce?
- Transport identity
- How is one delivery correlated across logs?
- Business-operation identity
- What action must not happen twice?
- Atomic claim
- What prevents two workers from owning it simultaneously?
- Processing state
- How does the system distinguish accepted, processing, succeeded, failed and uncertain work?
- Downstream idempotency
- Can the external API accept a stable idempotency key?
- Retry policy
- Which failures can safely run again?
- Reconciliation
- How do the systems recover when the outcome is uncertain?
- Observability
- Can one WooCommerce change be traced to one downstream result?
- Retention
- How long is the evidence needed for diagnosis kept?
A webhook URL is one transport boundary. The reliability of the integration depends on everything that happens around it.
When to escalate
A specialist investigation is justified when:
- one WooCommerce order creates multiple shipments or external records
- order.updated appears to trigger far more often than expected
- old and new webhook registrations coexist
- duplicate work appears only under production concurrency
- retry jobs repeat external side effects
- WooCommerce and downstream logs disagree
- ERP, CRM, inventory or fulfilment state drifts from WooCommerce
- a payment, inventory or shipping operation must never run twice
- custom receiver code deduplicates only by order ID or a short-lived cache
- nobody can trace one webhook delivery to one final downstream outcome
The investigation should identify the duplication boundary, define the protected business operation, make ownership safe under concurrency and add reconciliation for uncertain outcomes.
Technical references
- WooCommerce Developer Documentation — Working with WebhooksWebhook configuration, topics, signatures, delivery status and troubleshooting/logging entry points.
- WooCommerce Developer Documentation — REST API WebhooksWebhook topics and transport headers including source, topic, event, signature, webhook ID and delivery ID.
- WooCommerce 11.0.1 Core — WC_WebhookRequest-level processed-resource suppression, delivery ID generation, signature generation, delivery logging and failure tracking in released core.
- WooCommerce 11.0.1 Core — Webhook FunctionsShutdown-time queueing, async woocommerce_deliver_webhook_async scheduling and the narrow same-webhook/resource scheduling guard.
- WooCommerce GitHub Discussion #44199 — Gathering feedback about the Web APIProduction-scale webhook reliability discussion, including the absence of automatic/manual retry functionality in the discussed core model and proposals for retries, replay and better observability.
- WordPress Plugin Handbook — Creating Tables with PluginsWordPress guidance for creating plugin-owned MySQL/MariaDB tables and maintaining their schema.
- MySQL 8.4 Reference Manual — PRIMARY KEY and UNIQUE Index ConstraintsDatabase-level uniqueness behavior used to enforce one operation key under concurrent inserts.
