iVendNextDevelopers Request a sandbox

Reading iVendNext's events reliably — the event-feed recipe

A sale with two offers, and the line the outside service recorded

A sale with two offers at the till; the outside service reads it from the feed and records the one price that broke its rule.

Your service learns what happened in a shop — a sale booked, a return, a customer created, goods received — without ever missing one while it was down. This recipe is for a partner's service, a customer's own system, a BI feed or an AI agent.

Reference client: tools/feed_client/feed_client.py (Python, standard library only; any language can do the same).

A shop keeps a queue of events per registered application. Your service pulls its own queue, acts on each event, and marks it done. Nothing is pushed at you, so nothing is lost when you are down: the events wait. The reliable feed is the shop's existing pull feed, not webhooks. A webhook may still nudge you (appendix A).

When to use it. Whenever your service must act on what happens in a shop and cannot afford to miss an event.

1. Register your application (the shop does this)

The shop's administrator creates a Registered Application for you: its name, the user your service reads as (an integration user with its own API key), and the record types you follow (POS Invoice, Sales Invoice, Customer, Item, Stock Entry …), each ticked on. A manifest can install the registration switched off (registered_applications, manifest v2); the shop picks the user and ticks it on. From then on, every change to a followed record adds one row to your queue — after the change commits, so a refused save sends nothing.

Changes made by your own integration user are not sent to you (no echo of your own writes).

2. Pull, act, mark done — in this order

loop:
  page = events(limit=100)                 # the OLDEST not-done events, oldest first
  for each event in page:
      if event.id in my seen-set: skip     # a repeat: already acted on
      in ONE transaction of my own:        # my business write + the seen-set entry, together
          act on the event                 #   (read the record over the API if I need its content)
          add event.id to my seen-set
      on failure: failed(event.id, note)   # it stays not-done, with the note, for the next pass
  done(ids of the page)                    # one call

3. Two ways to read the queue, one client

The client reads the queue in one of two ways (its --transport). Your handler is the same for both.

The feed every shop has today (--transport engine) The new feed extension point, P-FEED (--transport door)
Calls the record API on Integration Sync Log (list your rows, mark them done with flag = 1, a note in error) events · done · failed (the P-FEED extension point's published contract)
The key needs read + write on Integration Sync Log and read on the record types you follow — the narrowest grant that works read on the record types you follow — nothing on the log
Scope ⚠ none on the server. The key can read and mark done every application's rows, not only yours. The client filters to its own rows, but that is a convention, not a control. Use it only where your service is the shop's one consumer the server's: only the caller's own application's rows. Another application's event is refused
Labels (sale.booked …) applied by the client, from the same published table decided by the server
Status ✅ works on every shop today ⏳ not shipped yet; tested only against our stand-in for it

Write your handler against the labels and both ways run it unchanged. Switch to the P-FEED extension point when it ships (--door-prefix ivendnext_mpos.api.feed, the default).

4. The published labels

Record and event event
POS Invoice submitted, not a return / a return sale.booked / return.booked
POS Invoice cancelled sale.cancelled
Sales Invoice submitted / cancelled invoice.booked / invoice.cancelled
Sales Order submitted order.booked
Customer inserted / updated customer.created / customer.updated
Item inserted / updated item.created / item.updated
Stock Entry / Purchase Receipt / Stock Reconciliation submitted stock.moved / goods.received / stock.counted
anything else <record type>.<raw event> (lower case, spaces as _); a record deleted since reads <record type>.deleted

⚠ With --transport engine the client decides the label by reading the record with your key: a POS Invoice your key cannot read (a user permission, say) reads as pos_invoice.deleted, where the P-FEED extension point — deciding on the server — says sale.booked. Give the key read on the whole record type you follow.

Expect several events per sale. A till sale followed as POS Invoice produces several rows (inserted, updated, submitted, and more changes). Act on the label you need (sale.booked) and mark the rest done.

Events can lag the sale. Rows are written by the shop's background worker after the sale commits. When the worker is busy, a sale's events can take about a minute to appear. Never assume an event exists the moment a sale does; the queue catches up.

Offline sales arrive late. A sale rung offline produces its events when the till's queue drains — the at time can be hours after the customer left. The day's close merges till sales into a back-office Sales Invoice: if you follow both, skip the merged roll-up (is_consolidated = 1) or you count every sale twice.

5. The nightly reconciliation

Once a night (and after any long outage), list the records booked in a window over the API and compare them with what your seen-set holds:

FEED_KEY=… FEED_SECRET=… python3 tools/feed_client/feed_client.py reconcile --base … --transport engine --app "<your registered application>" --since "2026-10-04 00:00:00" --doctype "POS Invoice"

(--transport engine --app … is what works on every shop today; the client's default is door, which is not shipped, so a command without --transport engine fails on a real shop.) It lists every record created in the window that your service never saw a sale.booked / return.booked for — and, apart from those, the ones whose event is still in your queue (late, or failed and waiting), which are not missing. Run it right after a pass that ended with backlog 0. Act on what is missing through a handler keyed on the record (your seen-set knows events, not records), so acting twice changes nothing. It keeps no state: a record you recover is listed again next night while it stays in the window, harmlessly.

6. Run the reference client

FEED_KEY=… FEED_SECRET=… python3 tools/feed_client/feed_client.py run --base https://shop.example --transport engine --app "<your registered application>" --state state/feed.sqlite --watch 30

This is --transport engine, the one that works on every shop today. --transport door is for the P-FEED extension point once it ships (add --door-prefix … if the prefix differs) and today runs only against our stand-in. The key and secret come from the environment, never the command line. The client follows no redirect (your key never travels to another host). dump prints what the sample handler recorded; backlog prints how far behind you are. Replace sample_handler with your own: it receives the event and an open database transaction of yours.

A handler that writes back, end to end (S3, samples/s3_price_audit/outside/). A cashier sells a case, a charger and a cable, two of them on offer:

a sale with two offers

The outside service reads the sale from the feed and records the one line priced too far below list:

the deviations its service recorded from the feed

one deviation

Appendix A — the nudge (was the webhook recipe)

A webhook is a doorbell, not a parcel: optional, for when you want to pull now instead of on your next poll. The shop may create a Webhook on Integration Sync Log, after insert, conditioned on your application's name, sending no content — {"app": "<your application>"} — signed with a secret (recipes/webhook_receiver.md §2 for the signature check). On a valid nudge, run one poll. Never treat the nudge as the event: it can be lost, delayed or repeated, and the queue is the record. We have not tested the nudge yet; it is configuration only.

Technical notes

What was proven where

Bounds, stated

This page in the kit