Templates / Inventory sync
Keeping stock levels honest
Overselling costs more than a stale count. Frequent syncs keep the storefront within a few minutes of the truth, and cover the webhooks that never arrived.
The recipe
- Request
- POST https://{{domain}}/api/inventory/sync
- Schedule
- Every 30 minutes (*/30 * * * *)
- Headers
- Authorization: Bearer {{secret}}encrypted
- Timeout
- 120s
- Attempts
- 3
- Counts as success
- 2xx
You supply
- Your domain
- example.com
Host only — no https:// and no trailing slash.
- Shared secret
- s3cr3t-valuestored encrypted
Whatever your endpoint checks the caller against.
Why this job exists
Stock usually lives in one system — an ERP, a warehouse platform, a spreadsheet somebody maintains — and is displayed in another. Keeping the two aligned is normally done with webhooks, which are the right primitive and are not reliable enough on their own.
Webhooks are lost for ordinary reasons: the receiver was deploying, the sender retried three times into a five-minute outage and gave up, a payload failed validation after a schema change. Each loss is permanent and silent. The count drifts, and nothing detects it, because there is no signal — the storefront confidently displays a number that stopped being true a week ago.
The cost is asymmetric. Understating stock loses a sale. Overstating it means taking money for something you cannot ship, then apologising, refunding, and losing the customer. A periodic reconciliation costs a few requests an hour and removes the tail.
Setting it up
Sync deltas rather than everything, and report what changed. A sync that reports how many levels it corrected tells you whether your webhooks are working, which is information you cannot otherwise get:
Reconcile and report
const since = await state.get('inventory:lastSync');
const upstream = await erp.stockLevels({ modifiedSince: since });
let corrected = 0;
for (const item of upstream) {
const local = await products.getStock(item.sku);
if (local === item.quantity) continue;
await products.setStock(item.sku, item.quantity);
corrected += 1;
}
await state.set('inventory:lastSync', new Date());
return Response.json({ checked: upstream.length, corrected });Watch the corrected count. If webhooks are healthy it should be near zero, and a persistent non-zero figure is a webhook pipeline that is quietly dropping deliveries — worth fixing at the source rather than papering over here.
Checking it actually works
- Change a level directly in the system of record and confirm it reaches the storefront within one interval.
- Track the corrected count as a health signal for your webhook integration.
- Run a full reconciliation weekly alongside this delta sync — deltas cannot detect a record the upstream stopped reporting entirely.
Where it goes wrong
Overwriting reservations
If the storefront reserves stock at checkout but the ERP only knows about shipped orders, a naive overwrite returns reserved units to the available pool and you oversell. Sync the on-hand figure and subtract local reservations rather than replacing the displayed number.
A full sync every time
Pulling the entire catalogue every half hour is slow, expensive, and will hit the upstream rate limit as you grow. Use a modified-since cursor and full-sync occasionally.
Trusting the last write
When a webhook and a scheduled sync disagree, last-write-wins can apply a stale value on top of a fresh one. Compare timestamps and let the newer observation win, whichever path delivered it.
Questions
Is half an hour often enough?
For most catalogues, yes. For a flash sale or anything with a handful of units, no scheduled sync is fast enough — you need reservation at checkout, with the sync as a backstop.
Should this replace webhooks?
No. Webhooks give you seconds; this gives you a guarantee. Together they are considerably better than either alone.
What about multiple sales channels?
Then the system of record must be authoritative for all of them and each channel syncs from it. Peer-to-peer syncing between channels does not converge.
Running it here
This is a preset in the product, not an illustration. Pick it in the dashboard, fill in the 2 values above, and the job is created, scheduled and enabled — with retries, a record of every run showing status, latency and response, and an alert the first time one fails. Secret values are encrypted at rest and never rendered back.
The free plan runs 3 jobs hourly and needs no card. Where a recipe wants a finer cadence than your plan allows, it is slowed to the fastest schedule you are permitted rather than rejected.