Commerce APIs and corporate ERPs live in noisy networks:
Webhooks can be delivered once, twice, or never.
Cron and queues can retry at awkward times.
Without idempotency, duplicate pushes create ghost invoices, stock drift, and reconciliation nightmares. Your sync layer must guarantee “exactly-once effects” even if the transport is at-least-once.
WooCommerce ──(OrderCreated Webhook)──▶ Sync API (Ingress)
▲ │
│ Validate + Normalize
│ │
└──(Health/Retries)◀── Work Queue ◀─┘
│
Workers
│
SAP B1 Service Layer / DI-API
│
Result & Audit Log
│
Callback / Notes on OrderIngress API: receives Woo events or is called by cron. Adds idempotency key to the message (e.g., wc_order_id + stable salt).
Work queue: decouples reception from processing; supports backoff and DLQ (dead letter queue).
Workers: implement mapping, pre-checks, SAP calls, and idempotent write.
Audit log + metrics: every attempt is recorded with correlation IDs.
Reconciliation jobs: periodic sanity checks to detect drift (e.g., stock/price mismatches).
Define a versioned contract between Woo and SAP. Keep a living spec (v1, v1.1, …). Typical mappings:
Order header
DocDate ← Woo order date (UTC normalized).
CardCode ← Customer code (derived from email/metadata or a “generic web” account with address lines).
Comments ← Human note + machine hints (payment/ref, applied coupon, shipping method).
UDFs ← DNI/NIE, marketing source, consent flags.
Lines
ItemCode ← SKU (strict).
Quantity ← Int/decimal; enforce unit policy.
Price ← Decide: pre-VAT vs after-VAT. Keep one source of truth.
DiscountPercent ← Merge order-level and line-level discounts carefully.
Taxes
Align tax groups (SAP) with Woo tax classes (name + rate). Don’t hardcode 21% if rates can vary per product/region.
When Woo shows prices incl. VAT, ensure you send PriceAfterVAT (or compute net + tax group consistently).
Shipping & fees
Model shipping as a non-inventory item with its own tax group.
Payment fees (if used) should be explicit items for auditability.
Keying
Use a stable, deterministic idempotency key per semantic operation. For order creation, a good default is:
"wc:" + order_id + ":comments:v2"CREATE TABLE sync_ledger (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
idem_key VARCHAR(128) UNIQUE NOT NULL,
source VARCHAR(32) NOT NULL, -- 'wc'
target VARCHAR(32) NOT NULL, -- 'sapb1'
op VARCHAR(32) NOT NULL, -- 'create_invoice', 'patch_comments'
status VARCHAR(16) NOT NULL, -- 'applied','skipped','failed'
target_docnum VARCHAR(64) NULL, -- SAP DocNum/DocEntry
checksum VARCHAR(64) NULL, -- payload hash (optional)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
Apply-once guard (PHP example)
function already_applied($pdo, $key){
$stmt = $pdo->prepare("SELECT status, target_docnum FROM sync_ledger WHERE idem_key=?");
$stmt->execute([$key]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
function mark_applied($pdo, $key, $op, $docnum){
$stmt = $pdo->prepare("
INSERT INTO sync_ledger (idem_key, source, target, op, status, target_docnum)
VALUES (?, 'wc','sapb1', ?, 'applied', ?)
ON DUPLICATE KEY UPDATE status='applied', target_docnum=VALUES(target_docnum)
");
$stmt->execute([$key, $op, $docnum]);
}
function create_invoice_once($pdo, $payload){
$key = "wc:".$payload['order_id'];
if ($row = already_applied($pdo, $key)) {
// Return the previously created doc (idempotent)
return ['status'=>'ok','docnum'=>$row['target_docnum'],'idempotent'=>true];
}
// ... map Woo → SAP body
$sap = sap_create_invoice($payload); // call Service Layer/DI-API
mark_applied($pdo, $key, 'create_invoice', $sap['DocNum']);
return ['status'=>'ok','docnum'=>$sap['DocNum'],'idempotent'=>false];
}
Minimum viable signals:
Correlation ID: include X-Correlation-ID: wc:<order_id> in all logs and SAP calls when possible.
Metrics: processed/min, success rate, median & p95 latency, retries, DLQ size.
Structured logs: JSON lines with stage, order_id, idempotency_key, op, outcome, docnum, error_code.
Example (JSON log):
{"ts":"2025-10-21T07:25:00Z","corr":"wc:58291","stage":"worker",
"op":"create_invoice","idemp":"wc:58291","result":"applied","docnum":"153447"}PII minimization: only store what you need for reconciliation. Mask sensitive fields in logs.
Transport: TLS everywhere; verify certificates; set sane timeouts.
Auth: never embed long-lived SAP creds in frontend systems. Keep secrets in your sync service.
Reconciliation: a periodic job that compares a slice of last N orders between Woo and SAP and emits a report.
Prices
Decide if Woo is incl. VAT and make the mapping deterministic. If SAP holds net prices:
Convert Woo gross → net using the exact tax group.
Or switch to PriceAfterVAT in SAP where appropriate (be consistent).
Ensure coupon discounts don’t double-apply (line-level vs order-level).
Stock
If you maintain multi-warehouse levels, your queue should:
Read current availability from SAP (01, LI, etc.).
Apply business rules (which warehouse feeds the web?).
Push the effective stock back to Woo on a schedule (and after significant changes).
Duplicate webhook deliveries (2–5 times).
SAP API 5xx and slow responses (timeouts).
Price with and without VAT; multiple tax classes.
Shipping with different tax groups.
Partial failures: header accepted, line rejected.
Network partitions (queue builds up).
Schema evolution: add UDF, new field in comments, deprecate a field.
def worker_loop(queue, sap_client, db):
for msg in queue.consume():
order = msg.payload
idem = f"wc:{order['id']}"
prior = db.find_ledger(idem)
if prior and prior.status == 'applied':
ack(msg); continue
try:
body = map_woo_to_sap(order) # normalize/mapping
doc = sap_client.create_invoice(body) # call Service Layer
db.upsert_ledger(idem, op='create_invoice', status='applied', docnum=doc['DocNum'])
ack(msg)
except TransientError as e:
msg.retry(backoff='expo_jitter')
except ValidationError as e:
db.upsert_ledger(idem, op='create_invoice', status='failed', docnum=None)
msg.to_dlq(reason=str(e))No blind deletes. Create compensating actions (credit memo) instead of destructive edits.
Provide a small ops dashboard: search by order ID, see ledger status, re-send safe operations with a single click.
If you operate in regions that need national ID validation (e.g., Spain’s DNI/NIE/CIF), implement:
Client-side hints to reduce friction.
Server-side validation for integrity.
Blocks & Classic compatibility (selector names differ).
Store validated ID as a UDF in SAP so finance can reconcile.
This reduces address/accounting errors before the sync pipeline receives the order.
The best SAP ↔ WooCommerce integrations succeed not because the API calls are clever, but because the system around them—idempotency, retries, observability, and data discipline—is boringly reliable. Start with a strong contract, a small ledger, and a queue; the rest is iteration.
Luis Javier Gil is a WordPress/WooCommerce developer and founder at Replanta LC. He builds production-grade integrations (SAP Business One ↔ WooCommerce), performance tooling, and checkout compliance features.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
| User | Count |
|---|---|
| 12 | |
| 11 | |
| 4 | |
| 2 | |
| 2 | |
| 1 | |
| 1 | |
| 1 | |
| 1 | |
| 1 |