iVendNextDevelopers Request a sandbox

Keep a customer's own system in step with iVendNext — the system-sync recipe

This recipe keeps a retailer's own ERP, order system or warehouse system, running outside our cloud, in step with the shop. Items, prices and stock go out to the shop; sales and returns come in to the ERP; and nothing is lost or doubled. It is for a large retailer's IT team, or a partner working for them. The sample to copy is samples/s7_system_sync/: erp.py is a stand-in ERP and sync.py is the sync (Python, standard library only — any language can do the same). It is built on the event-feed client (tools/feed_client/, recipes/event_feed.md).

When to use it: an outside system owns items, prices or stock, and must also hold every sale and return the shop books.

The picture

 ERP ──(numbered changes)──► sync OUT ──► items · prices · Stock Entries ──► the shop
 ERP ◄── orders · returns ◄── sync IN ◄── the event feed (sale.booked, return.booked, sale.cancelled) ◄── the shop
                              sync keeps: a CURSOR · an IDENTITY MAP · the feed's SEEN-SET        nightly RECONCILE reads both sides

1. Register the sync (the shop does this once)

The shop creates a Registered Application for the sync, following POS Invoice (recipes/event_feed.md §1), and an integration user with its own API key. Give that user this grant:

A shop that wants to grant less must say which call it refuses. ⚠ Today the feed grant cannot be scoped to one application (recipes/event_feed.md §3). Use it where the sync is the shop's one consumer. P-FEED, when it ships, removes the right on the log.

2. OUT — what the ERP changed goes to the shop

The ERP numbers every change it makes that the shop must learn: an item, a price, a stock movement. The sync's cursor is the last number done. The sync takes each change, oldest first:

Change What the sync writes How a retry finds its own work
an item an Item (code = the ERP id behind a prefix; the identity map is the authority, the prefix a convention) — created, renamed, or disabled when the ERP deleted it. Never deleted: a booked sale points at its item the item exists under that code
a price an Item Price in the named price list (create, or change the rate) the row for item + list
a stock movement a Stock Entry the engine books (Material Receipt for goods in, Material Issue for goods out). The sample never sets a quantity a key in the entry's remarks — PP-SYNC <the ERP's own id> mv:<movement number> — looked up before every write. ⚠ The ERP's own id is part of the key. A key of the movement number alone can find an earlier system's entry with the same number, and then no stock is written at all

When your service is down: why a crash is harmless

The cursor moves only after a change is fully written. A crash between the shop write and the cursor leaves the cursor one behind. The restart repeats that change and finds it by its key, so nothing is written twice.

The run stops at the first failure, and the cursor stays. An item comes before its price and its stock. A price or a stock movement for an item the ERP has since deleted is skipped (its shop item is disabled, or was never made). Otherwise an item added and deleted between two passes would stall every change after it.

3. IN — what the shop booked comes to the ERP, once each

The sync runs the feed client's loop: pull the oldest events not yet done, act on them, mark them done. Its handler does this:

Rules that keep it exact:

4. The nightly reconciliation — it reports, it never fixes

SYNC_KEY=… SYNC_SECRET=… python3 samples/s7_system_sync/sync.py reconcile --base … --app … --erp … --state … --warehouse … --company … --since "2026-10-04 00:00:00"

It is read-only on both sides. It lists, by name:

A sale whose event is still in the queue (late, or failed and waiting) is listed apart, not as a difference. A difference in money is a person's decision: the report never "fixes" it. Run it after a pass that ended with backlog 0.

5. Run it

SYNC_KEY=… SYNC_SECRET=… python3 samples/s7_system_sync/sync.py run --base https://shop.example --app "My Sync" --erp erp.sqlite --state sync.sqlite \
    --warehouse "Store - X" --company "My Company" --watch 30

Keys come from the environment, never the command line. No redirect is followed with the key. One run pass pushes OUT, then collects IN.

When the till is offline

Offline sales arrive late. Their events appear when the till drains. At the day's close, till sales are merged into a back-office Sales Invoice. The sync follows POS Invoice only, so it never counts a sale twice.

Limits

Technical notes

The recipe's reference implementation is proven on our test bench.

What was proven where

With the money kit

The sync ran as a service while the money kit rang the shop's baskets, plus two baskets of the sync's own items (dedicated rungs in the kit's set): GREEN, 69 of 69 matched (the 67 shared baskets and the two extra ones), 33 with a discount, no POS day left open. The service itself handled 414 events in 27 passes, from the kit's start until the service was stopped, the final drain found nothing left, and the reconciliation afterwards was clean with 69 sales on each side. Items, prices and stock written by the sync belong to its own synthetic items only — never to a kit item — so the kit's recorded totals are untouched.

Bounds, stated

This page in the kit