E-commerce
WooCommerce audit checklist: stock, tax, webhook races
A founder of a Dutch beauty brand asked us to build a WhatsApp agent on a €1.6M WooCommerce store. We sent back a checklist instead. Here is why.

A founder of a Dutch beauty brand emailed us in March. Subject line: "Can you build us a WhatsApp agent for order status, returns, and 'is this in stock'?" Their WooCommerce store does roughly €1.6M GMV, 22k orders a year, three warehouses, two ERPs.
We did not quote. We sent back a checklist.
The reason: a chat agent talking to a customer is only as honest as the data underneath it. If WooCommerce is lying to itself about stock, taxes, or the order lifecycle, the agent inherits the lie and amplifies it at the speed of WhatsApp. The audit is the part we do before any code gets written, and the last €1.2M+ store we rebuilt taught us which numbers to look at first.
The case for auditing first
A chat agent doesn't replace your support team because it answers faster. It replaces them because it answers correctly more often than a tired human on a Friday at 17:50. The bar is not "average accuracy". It's the worst answer the agent gives, multiplied by the volume of customers who see it before someone catches it.
On stores under €500k GMV we usually skip the audit. The data is small enough that an agent that gets stock wrong twice a week is annoying, not lethal. Above roughly €1.2M GMV you cross into a different regime: enough order volume that a 0.5% error rate costs real money, enough product complexity that "where is my package" sits next to "what's my VAT on this", and almost always at least one integration that was bolted on in a hurry two years ago and never revisited.
So before quoting, we run a half-day audit on the live store. Three categories of problem come back almost every time: stock-sync lag, tax-class drift, and a handful of webhook races. Here is what we look for and the queries we run.
Stock-sync lag
WooCommerce native stock is fine until you add a second source of truth. The moment an ERP (Exact, AFAS, SAP Business One, Odoo, Brightpearl) or a 3PL talks to the store, you have two databases that disagree by some delta, and the delta is the lag.
The chat agent reads stock from WooCommerce. The warehouse reads from the ERP. The customer asks "is the matte cleanser in stock?" The agent looks at _stock on the product meta, says yes, the customer pays, three hours later they get a "sorry, oversold" email. That email is the most expensive email in your stack: it costs the order, the trust, and the support ticket.
We measure lag with a probe script. WP-CLI runs it against the live store from the deployment host:
wp eval-file scripts/stock-lag-probe.php --user=1 \
| tee logs/stock-lag-$(date +%F).logThe probe pulls 200 random SKUs from WooCommerce, hits the ERP read endpoint for each, and writes the absolute difference plus the timestamps of the last sync on both sides. Anything where the lag exceeds 10 minutes goes on the chat agent's "ask, don't tell" list, meaning the agent quotes "should be available, let me confirm at checkout" instead of a hard yes.
What we typically find on stores in this band: median sync lag of 6 to 12 minutes, p95 around 40 minutes. One or two SKUs with permanent drift because a manual override was set in WooCommerce admin and never written back to the ERP. And almost always a cron job that claims to "sync every 5 minutes" but silently dies on PHP timeout above 800 SKUs.
If your ERP sync runs through admin-ajax.php on a wp_cron tick, you don't have a 5-minute sync. You have a sync that fires whenever someone visits the site. Move it to a real system cron before you connect anything to a chat agent.
Tax-class drift
This is the silent one. Nobody complains about it, because customers don't know what their VAT should be, and the finance team only catches it once a quarter when the BTW return doesn't reconcile.
Drift happens when products get added to WooCommerce without an explicit tax class, so they fall into "Standard" by default. On a Dutch store that's 21%. If the product should be in the reduced 9% bracket (books, some food, hairdressing services, certain health products), you've been overcharging customers, and the customer-service inbox slowly fills with refund requests phrased politely. Worse: if the product should be zero-rated (intra-EU B2B with a valid VAT number), you've been charging tax on transactions that should have been clean reverse-charge, and the buyer's accountant is now annoyed at both of you.
The query we run on classic post-meta storage:
SELECT p.ID, p.post_title, pm.meta_value AS tax_class
FROM wp_posts p
LEFT JOIN wp_postmeta pm
ON pm.post_id = p.ID AND pm.meta_key = '_tax_class'
WHERE p.post_type IN ('product', 'product_variation')
AND p.post_status = 'publish'
AND (pm.meta_value IS NULL OR pm.meta_value = '');Any product where _tax_class is empty falls through to Standard. We then cross-check those SKUs against the merchant's actual tax mapping (usually a spreadsheet the bookkeeper maintains). On the last audit, 312 of 4,100 products were on the wrong class. Six of them were responsible for about €18k of mischarged VAT over twelve months.
For a chat agent that's expected to answer "what's my total with VAT" or "can I get a B2B invoice", you fix this first. The agent cannot be a more accurate source of truth than the database it queries.
If you're on the new High-Performance Order Storage (HPOS), the join changes: tax data sits in the new wc_orders and wc_order_addresses tables. The drift problem is the same. Only the SQL moves.
The three webhook races
This is where we lost a week on the last project, and why we now run a webhook trace before quoting.
WooCommerce fires webhooks on order events: created, updated, paid, completed, refunded. The naive mental model is that they fire in that order, exactly once, with the order in a stable state. The real model is none of those three things.
Race 1: created vs updated, fired out of order
When a customer pays via a redirect gateway (Mollie, Adyen, Buckaroo), the order is created in pending state, then transitioned to processing after the gateway callback. WooCommerce can fire order.updated before order.created has been delivered if your queue worker pulls them in parallel, or if the created webhook hit a transient 502 and is sitting in the retry queue.
The chat agent receives updated, looks up the order, finds nothing (because created is still in flight on your side), and replies "we don't see that order yet, can you double-check the number?" The customer just paid. They see panic. They open a ticket. Your agent created the ticket it was supposed to prevent.
Fix: dedupe and reorder on your end. Use id plus a monotonic date_modified_gmt and queue events per-order with a small (2 to 3 second) settle delay before the agent reads.
Race 2: payment_complete fires before the status row commits
The woocommerce_payment_complete action fires inside the gateway callback, before the order row commits in some database configurations (notably MySQL with row-level locks under READ-COMMITTED). If your chat-agent worker reads the order inside that hook, you can get status = 'pending' back, even though logically the payment has cleared.
We learned this on a store running Mollie iDEAL. The agent's "your payment is confirmed" message went out 90 seconds before the order showed as processing in the admin. Customer service got 14 confused emails in one weekend, all from customers who couldn't find their order in their account page yet.
Fix: never trust the status field inside woocommerce_payment_complete. Wait for woocommerce_order_status_processing or woocommerce_order_status_completed, which fire after the row is persisted.
Race 3: stock decrement vs cancel-on-timeout
When an order goes to pending, WooCommerce holds stock for hold_stock_minutes (default 60). If the customer abandons checkout, a cron releases that stock. If the chat agent reads stock during the hold window, it sees a number that's about to change.
Worse: if a customer asks "do you have this in red?", the agent says "yes, 1 left", the customer clicks to checkout, the held stock from another pending order releases mid-flow, and now there are 2. But the agent already told them 1, so they don't add the spare to their basket and you lose the upsell that should have been free.
Fix: query _stock minus pending-order reservations explicitly, or treat anything under 3 units as "limited" in agent copy and force a hard re-check at add-to-cart. The WooCommerce stock management docs explain the hold-stock mechanism, but they don't warn you about the race. That's on you to engineer around.
The checklist itself
Here is the form we run on a €1.2M+ store before quoting a chat-agent build. It takes a senior developer about four hours and surfaces 90% of the work the agent build will otherwise discover the hard way, halfway through.
## Pre-build audit, WooCommerce + chat agent
### Data integrity
- [ ] Stock-sync lag probe over 200 random SKUs, p50 + p95 logged
- [ ] Products with empty _tax_class counted, cross-checked vs bookkeeper
- [ ] Orphan order meta (orders without line items) counted
- [ ] HPOS migration status; if mid-migration, audit both old and new tables
### Webhook hygiene
- [ ] All registered webhooks listed (wp wc webhook list)
- [ ] Delivery log reviewed for 4xx/5xx in the last 7 days
- [ ] Retry policy on consumer side documented
- [ ] Dedupe key chosen (order_id + date_modified_gmt)
### Order lifecycle
- [ ] Gateway list + which hooks fire for each gateway
- [ ] hold_stock_minutes value confirmed
- [ ] Cancel-on-timeout cron verified as system cron, not wp_cron
### Agent surface
- [ ] Intents the agent will handle, mapped to read-only or write
- [ ] For every write intent: idempotency key + rollback path
- [ ] PII boundary: what the agent can quote, what it must maskThe five-minute starter audit
If you run a WooCommerce store above €1.2M GMV and a chat-agent project is in your roadmap, you don't need to hire anyone to begin. Run the _tax_class SQL query above and count the empty rows. Time how long it takes a stock change in your ERP to show up in WooCommerce: edit a single SKU's stock by hand in the ERP, then refresh the WP admin product page every minute until the number updates. Write those two numbers on a sticky note. They are the first two slides of your build brief, and they will tell you more about the project's real shape than a vendor demo will.
When we built the WhatsApp agent for the beauty brand we opened with, the thing that ate our timeline was the third webhook race, the stock-decrement one. We only spotted it because the audit had flagged a hold_stock_minutes value of 240, set during an A/B test two years earlier and never reverted. If you'd like the audit run on your own store before scoping an agent project, that's the work we do under our AI agents practice, and it costs less than the first oversold order an unaudited agent will send out.
Key takeaway
A chat agent is a public API for your store's worst-kept secret: how often your data lies to itself. Fix the data before you quote the agent.
FAQ
When does a WooCommerce store need a pre-build audit?
Above roughly €1.2M GMV the order volume and integration complexity make data quality the real bottleneck. Below that, a small pilot usually surfaces issues faster than a formal audit.
Can the audit run without taking the store offline?
Yes. Every check is read-only against the live database plus a passive review of the webhook delivery log. No production traffic is interrupted and no schema changes are made.
What's the most common surprise in the audit?
Tax-class drift. Almost every store above €1M GMV has at least 50 SKUs on the wrong VAT class, usually silently overcharging customers, occasionally undercharging the tax authority.
How long does the audit take?
Half a day for a senior developer to run the queries and probes, plus a 30-minute walkthrough with the finance lead to validate the tax mappings against the bookkeeper's spreadsheet.