Three weeks after launching Nventory we had a silent bug.
Inventory was updating correctly everywhere.
Except one seller's WooCommerce store.
Every sync completed successfully. Every log showed green. Every API call returned 200. The seller's stock was quietly wrong for eleven days before anyone noticed.
The bug wasn't in the sync layer. It was in the verification step we hadn't built yet.
javascript
// What we were doing
async function updateInventory(channel, sku, qty) {
const response = await channel.setInventory(sku, qty);
if (response.status === 200) {
await auditLog.record({ sku, qty, channel: channel.id, status: 'success' });
return true; // assumed success
}
}
// The problem
// WooCommerce was returning 200 with a silent validation error
// The inventory wasn't updating
// The log showed green
// Nobody knew for eleven days
We were trusting the API response instead of reading back the actual state.
The fix
javascript
// What we do now
async function updateInventory(channel, sku, qty) {
const response = await channel.setInventory(sku, qty);
if (response.status !== 200) {
throw new ChannelAPIError(channel.id, response);
}
// Read back — verify the write actually took effect
const verification = await channel.getInventory(sku);
if (verification.qty !== qty) {
// The API said success. The state disagrees.
await deadLetterQueue.push({
channel: channel.id,
sku,
expectedQty: qty,
actualQty: verification.qty,
retryAt: Date.now() + backoffMs(0)
});
throw new InventoryMismatchError(sku, qty, verification.qty);
}
await auditLog.record({
sku,
qty,
channel: channel.id,
status: 'verified', // not just 'success'
verifiedAt: Date.now()
});
return true;
}
One word change in the audit log — from success to verified — that represents an entirely different level of confidence in the data.
Why this happens more than you think
Most channel APIs have at least one scenario where they return a success response without actually completing the operation.
WooCommerce returns 200 when a product update hits a validation constraint it doesn't surface in the response body. Amazon SP-API returns success for inventory updates that are silently queued rather than immediately applied. eBay returns Ack: Success with errors buried in a separate Errors array that most implementations never check.
javascript
// eBay — the classic trap
const response = await ebayAPI.reviseInventoryStatus(params);
// Naive check
if (response.Ack === 'Success') return true; // WRONG
// Correct check
if (response.Errors && response.Errors.length > 0) {
throw new EbayAPIError(response.Errors);
}
// Then verify
const verification = await ebayAPI.getItem(itemId);
if (verification.Quantity !== expectedQty) {
throw new InventoryMismatchError();
}
The broader principle
Every external write in a distributed system should be followed by a read that confirms the state changed correctly.
This is especially true for inventory data where the cost of silent failure is immediate and measurable — a seller shows stock that doesn't exist, an oversell happens, a customer gets a cancellation email.
javascript
// The pattern we now apply to every channel integration
class VerifiedInventoryUpdate {
async execute(channel, sku, qty, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Write
await channel.setInventory(sku, qty);
// Verify
const actual = await channel.getInventory(sku);
if (actual.qty === qty) {
await this.auditLog.record({ sku, qty, channel: channel.id, status: 'verified' });
return { success: true, verified: true };
}
// Write succeeded, verification failed — retry
await sleep(backoffMs(attempt));
} catch (error) {
if (attempt === maxRetries - 1) {
// All retries exhausted — dead letter queue
await this.dlq.push({ channel: channel.id, sku, qty, error: error.message });
throw error;
}
await sleep(backoffMs(attempt));
}
}
}
backoffMs(attempt) {
return Math.min(1000 * Math.pow(2, attempt), 30000);
}
}
What changed after this bug
Three specific things:
Every write has a read-back. Not just for WooCommerce. Every channel. Every time. The extra API call is worth it.
Audit logs say "verified" not "success." The distinction matters. Success means the API returned 200. Verified means the state actually changed. We only log verified now.
Silent failures go to DLQ immediately. A mismatch between expected and actual state isn't an error to retry inline — it's a failure to investigate. It goes to the dead letter queue with full context so it can be resolved without data loss.
This is the architecture we've built into every one of the 40+ channel integrations at Nventory — because when you're syncing inventory across Amazon, Shopify, WooCommerce, Flipkart, and eBay simultaneously, a single silent failure can corrupt stock counts across every connected channel before anyone notices.
The eleven days taught us one thing above everything else:
Green logs are not the same as correct data.
Build verification into every write. Assume every API will silently fail you at least once. Because it will and you won't know until eleven days later when a seller asks why their stock is wrong.
Trust nothing. Verify everything.
We're building Nventory — multichanial inventory and order management, free forever. Also available on the Shopify App Store if you're building for multichannel sellers.
Questions welcome in the comments — happy to go deeper on any of the verification patterns.
Top comments (0)