WooCommerce tracks a single stock quantity per SKU. Multi-location retailers need inventory that receives updates from multiple sources simultaneously, handles conflicting writes without data loss, and stays reconciled against an ERP. Native WooCommerce and the marketplace plugins that bolt on location pickers do not solve sync ordering, conflict resolution, or reconciliation against the system that holds the truth. This repo shows the architecture that survives those constraints: webhook ingestion, queue-based processing, configurable conflict resolution, daily reconciliation, and a dead-letter queue for failures that need human review.
For the full technical narrative on the conflict resolution design, the delta-merge dead end, and the reconciliation decisions, see the companion post: https://dvoorhees.com/2026/02/10/woocommerce-multi-location-inventory-sync-architecture-that-survives-concurrent-writes/
This is a reference architecture, not a turnkey plugin. The code is sanitized for public use: no client integrations, no proprietary endpoints, no real ERP credentials. External inventory is represented by a mock adapter you can run locally. The value is in the patterns and the documentation that explains why each piece exists.
- Seven-location inventory model with per-location stock stored as product variation meta
- HMAC-authenticated inbound webhook that validates, persists, and queues without processing inline (returns 202 immediately)
- Idempotent event intake via
event_idwith replay protection, so duplicate POSTs from network retries never schedule duplicate jobs - Action Scheduler workers with per-product locking (transient-based) and exponential backoff on failure
- Dead-letter queue surfaced in WordPress admin with manual replay capability
- Two conflict resolution strategies shipped side by side: last-writer-wins (timestamp ordering, simple and auditable when the ERP is authoritative) and delta merge (applies relative increments from each source, required when both the ERP and online sales write concurrently)
- Outbound order-completion sync to ERP with correlation IDs and retry handling
- Partial-fulfillment support via per-line-item location meta, so a single order can ship from multiple locations
- Daily reconciliation job that compares WooCommerce stock against ERP state, with a filterable admin report and Markdown export
- Mock ERP server with intentional latency (up to 3 seconds) and random failures (5% rate) for resilience testing
- Docker Compose environment that boots WordPress, MySQL, and the mock ERP together
flowchart LR
subgraph inbound [Inbound Path]
ERP1[External System] -->|POST webhook| API[REST Endpoint]
API -->|persist| EVT[(Inbound Events)]
API -->|enqueue| Q[Action Scheduler]
Q --> PROC[InboundProcessor]
PROC --> STOCK[(Per-Location Meta)]
end
subgraph outbound [Outbound Path]
ORDER[Woo Order Completed] --> OUT[OutboundProcessor]
OUT --> ERP2[Mock ERP]
OUT --> STOCK
end
subgraph reconcile [Reconciliation]
CRON[Daily Job] --> COMP[Compare Woo vs ERP]
COMP --> DISC[(Discrepancies)]
ADMIN[Admin UI] --> DISC
end
Deeper diagrams and sequence charts: docs/architecture-overview.md
woo-multilocation-inventory-reference/
├── woo-multilocation-inventory-reference.php Plugin bootstrap
├── src/
│ ├── Admin/ DeadLetterPage, ReconciliationPage
│ ├── Exceptions/ Conflict, Signature, Timeout, Validation
│ ├── Locations/ LocationRegistry, StockRepository
│ ├── Outbound/ ErpClient, OrderCompletionHandler
│ ├── Queue/ DeadLetterStore, InboundProcessor, JobLocker, OutboundProcessor
│ ├── Reconciliation/ DailyReconciler, DiscrepancyStore, Reporter
│ ├── Resolution/ DeltaMergeResolver, LastWriterWinsResolver, ResolverInterface
│ ├── Support/ Logger, Migrations
│ ├── Webhooks/ InboundController, PayloadValidator, SignatureVerifier
│ └── Plugin.php
├── docker/wordpress/ Docker configuration for the local dev environment
├── mock-erp/ Express server simulating an external inventory system
├── tests/ PHPUnit: resolvers, signature verification, job locking, reconciliation
├── docs/ Architecture, conflict resolution, operations, deployment, webhook protocol
├── examples/ Sample webhook payloads and signing helper
└── bin/setup.sh Automated local environment setup
git clone https://github.com/d-voorhees/woo-multilocation-inventory-reference.git
cd woo-multilocation-inventory-reference
cp .env.example .env
chmod +x bin/setup.sh
./bin/setup.shOr manually:
docker compose up -d --build
composer install
docker compose run --rm --entrypoint sh wpcli -c '...' # see bin/setup.shAdmin login: http://localhost:8081/wp-admin (user: admin, password: admin)
1. Send a signed stock update
php examples/signing-helper.php http://localhost:8081
# copy and run the printed curl command2. Process the queue
docker compose run --rm --entrypoint wp wpcli action-scheduler run --due-now3. Verify stock in the product editor or via WP-CLI:
docker compose run --rm --entrypoint wp wpcli post meta get <product_id> _mm_inv_stock_LOC-0014. Trigger a conflict (in last-writer-wins mode):
# Simulate a register override at the ERP
curl -X POST http://localhost:8080/admin/conflict \
-H 'Content-Type: application/json' \
-d '{"sku":"DEMO-SKU-001","location_id":"LOC-001","quantity":3}'
# Then send a webhook with an older source_timestamp
# (see examples/sample-webhook-payloads/)5. Run reconciliation: WooCommerce, then Inventory Sync, then Run Reconciliation Now
6. Export report: same page, Export Markdown
| Component | Doc |
|---|---|
| Webhook intake and HMAC auth | docs/webhook-protocol.md |
| Conflict resolution strategies | docs/conflict-resolution-strategies.md |
| Reconciliation | docs/reconciliation.md |
| Operations runbook | docs/operational-playbook.md |
| Production deployment notes | docs/deployment-considerations.md |
| Mock ERP API | mock-erp/README.md |
Action Scheduler over custom tables or Redis queues. WooCommerce ships Action Scheduler. It provides retries, admin visibility, and grouping without operating a second queue infrastructure. The tradeoff is throughput: very high-volume catalogs need a dedicated runner and possibly the Action Scheduler high-volume plugin. For seven retail locations and SKU counts in the low tens of thousands, Action Scheduler is the right default.
Last-writer-wins as default, delta merge as the bidirectional option. When the ERP is authoritative and WooCommerce is read-mostly for stock display, absolute sets with timestamp ordering are simple and auditable. When registers and online both write, absolute overwrites lose data. Delta merge applies increments from each source. Both strategies are included because the correct choice depends on the write direction of the integration, and that varies by retailer.
Idempotency at the webhook layer via event_id. Network retries are guaranteed to happen. Storing the event before enqueueing means a duplicate POST never schedules two jobs. The tradeoff is that the event table grows and needs a retention policy in production. The alternative (making workers idempotent without deduplicating at intake) still wastes compute and can race before the lock is acquired.
Per-product locking via transients. Cheap, works in single-node WordPress, prevents two workers from writing the same SKU concurrently. The tradeoff is that multi-node WordPress deployments require a shared object cache. Production should use Redis-backed transients.
Async webhook response (202). ERP timeouts and ERP slowness should not block the caller. The mock ERP injects up to 3 seconds of latency and 5% failures to prove the point. Inline processing would pass that cost to the webhook sender.
Reconciliation is compare-only. The reconciliation job records discrepancies; it does not auto-heal. Auto-healing without human review causes silent data loss when the bug is in your own sync logic. Operations gets a Markdown export and decides.
This repo is not a packaged plugin for WooCommerce.com distribution. It is not connected to any specific ERP, POS, or WMS vendor; the mock ERP demonstrates the integration pattern without coupling to a proprietary API. It does not handle SKU mapping, bundle decomposition, returns, or transfers between locations, all of which require discovery on the actual integration's constraints. It does not include production-hardened authentication, monitoring, or secret management.
composer install
composer testPHPUnit covers resolvers, signature verification, job locking, and reconciliation reporting. CI runs on push via GitHub Actions.
Almost no contract WooCommerce developer can point to public, credible multi-location inventory work. This repository is the proof point behind that capability: patterns extracted from real integrations across seven physical retail locations, rebuilt as a reviewable reference by Medium & Message.
GPL-2.0-or-later. See LICENSE.