iVendNextDevelopers Request a sandbox

5. Outside apps and the event feed

A sale at the till, then the warranty service's comment on that sale in Desk

A cashier sells a covered phone; seconds later a partner's own warranty service, running on its own server, has written a comment on the sale in Desk (sample S1).

Your service runs on your own server, in any language. It reads a customer's iVendNext, writes to it and reacts to what happens there. This chapter is for a developer building that kind of connected app.

Reference implementation: Building a connected app · Making the integration user · Reading iVendNext's events reliably with the client ../tools/feed_client/ · Receiving webhooks safely (the nudge only) · the samples ../samples/s1_warranty/service/ and ../samples/s2_bins/service/. Every call the samples make is in ../api/collection/.

When to use it

Rung 2 of the ladder (chapter 3), after configuration (chapter 4): anything that can happen before a sale (prepare data) or after it (react to a sale, sync a system, set a status). If the cashier must see an answer during the sale, add a till key (chapter 6). If you need your own record types inside the shop, ship them in a manifest (chapter 7).

Steps

  1. Get a key. The shop makes one integration user for your app, holding one narrow role (chapter 2; the recipe lists the role the bin sample needs). One app, one user. ⚠ On a product before 2026-09-30 that key can do what an administrator can: there every desk user also holds the Workspace Manager role, and with it a key could put content on a shared workspace that runs in the browser of whoever opens it. Since 2026-09-30 only a System Manager gets that role, and the manifest installer refuses an integration user that holds it (chapter 7). Keep the key in a secret store, never in code or chat, and rotate it at any doubt: Generate Keys again, and the old secret is refused at once (401).

  2. Make the first calls from the collection, not from a library. Every call is a plain HTTP file; run.sh sends them all with curl and jq:

    BASE=http://<shop>:<port> KEY=<api key> SECRET=<api secret> bash api/collection/run.sh
    
  3. Read and write records. List with fields and filters; a list returns only name unless you ask for more. PUT changes only the fields you send. ⚠ A child table sent in a PUT replaces the record's rows: to add one row, send the existing rows plus the new one. Read a stock balance (Bin.actual_qty) from the shop; never keep your own copy.

  4. Book a stock movement by asking the shop to book it. One call creates and books it. Send purpose as well as stock_entry_type: over the API the purpose is not filled in for you.

    POST /api/resource/Stock Entry
    {"stock_entry_type": "Material Transfer", "purpose": "Material Transfer", "company": "<company>", "docstatus": 1,
     "items": [{"item_code": "<item>", "qty": 4, "s_warehouse": "<back room>", "t_warehouse": "<till's warehouse>"}]}
    

    Book the movement first, then update your own records with its name as the reason. ⚠ A booking call that times out may still have booked: look for it (a reference in its remarks, say) before you send it again.

  5. Learn what happened from the event feed, not from webhooks. The shop's administrator registers your application: its name, the user your service reads as, and the record types you follow. From then on every change to a followed record adds one row to your queue, after the change commits. Your own writes are not sent back to you. Nothing is pushed, so nothing is lost while you are down: the rows wait. Your loop, in this order:

    • ask for the oldest events not yet done;
    • for each, skip it if you have seen it; otherwise, in one transaction of your own, act on it and record its id in your seen-set;
    • mark the page done.

    An event fetched and not marked done comes back. So a crash between acting and marking does no harm: the seen-set skips it. A write you make somewhere else (back into the shop, or to a third system) cannot share your transaction. Make that write idempotent too: key it on the record the event names and treat "already exists" as done.

    The deviations a partner's service recorded after reading one sale's event

    After a sale, a partner's service read the sale's event, read the sale over the API, and recorded the one line sold more than 10% below list (sample S3, reading our own stand-in for the feed; the product's feed is not yet tried).

  6. Know what an event is. It names which record changed and how (sale.booked, return.booked, customer.created, and so on, a published table of labels). It carries no content, so read the record over the API when you need it.

    • Expect several events per sale (six measured for one till sale): act on the label you need and mark the rest done.
    • Events can lag the sale (58 seconds measured with the shop's one background worker busy).
    • A sale rung offline produces its events when the till syncs, possibly hours later.
    • The day's close merges till sales into a back-office invoice with is_consolidated = 1: skip it if you follow both, or you count every sale twice.
    • Mark every row done. The shop clears only done rows (after 30 days), so an application that never marks done grows its queue for ever.
    • An application you stop using keeps its rows waiting: disable or delete its registration.
  7. Run the reference client instead of writing the loop. It is one file, Python standard library only. Copy it and replace sample_handler with your own, which receives the event and an open transaction of yours. Today, run it on the shop's own sync log (--transport engine):

    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
    

    The key comes from the environment, never the command line, and the client follows no redirect. Two warnings for this transport:

    • ⚠ It cannot be scoped to one application. Its key needs read and write on the shop's Integration Sync Log, so one application's key can read another's rows and mark them done. The client filters to its own rows, but that is a convention, not a control: use it where your service is the shop's one consumer.
    • Give the key read on the whole record type you follow. On this transport a POS Invoice the key cannot read is labelled pos_invoice.deleted, not sale.booked, and a handler that acts only on sale.booked would skip it.

    The scoped feed (--transport door, the client's default) has shipped as the event feed extension point (chapter 9), but the kit's client has not been run against it: until it has, do not rely on it.

  8. Reconcile every night, and after any long outage. The reconciliation lists the records created in a window that your service never saw an event for, and keeps no state:

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

    Run it after a pass that ended with nothing left in your queue. Act on what it names through a handler keyed on the record, so acting twice changes nothing.

  9. A webhook is only a doorbell (optional). If you want to pull now rather than on your next poll, the shop may send a thin signed nudge. Check the signature over the raw body, before parsing, comparing in constant time. Refuse a bad one with 401. Keep a seen-set so a replayed call does nothing. Answer quickly. Never treat the nudge as the event: it can be lost, delayed or repeated.

  10. Handle errors by their status:

    • 401: a wrong or rotated key (alert, do not loop);
    • 403: outside your role;
    • 404: absent;
    • 417: the shop refused the record (read the message, do not retry unchanged);
    • a timeout or 5xx: retry later, idempotently.

The same shape (a cursor, a map of record ids, an idempotency key, a reconciliation that reports and never fixes) carries the large-scale recipes in chapter 10.

What is refused, and why

When your service is down, and when the till is offline

Technical notes

What the kit has proven

What it has not

The pictures

See also

2. Getting started · 3. The ladder and the four rules · 6. Till keys · 7. Manifests · 10. Integrations · 16. Possible, not possible, limits

This page in the kit