Node Webhooks: Push Events to Your POS
Point a node at a URL and it POSTs every event, signed with HMAC-SHA256. Delivery is deliberately best-effort — this page says exactly how weak it is.
Published 2026-09-09 · All documentation →
A POS that wants live events from a node does not have to hold a stream open. Set one URL on the node and it POSTs each event to you, signed. This page is the contract — and the limits, which are real and are on this page rather than in a support ticket six months from now.
Why this exists
The pull side of the API has one push channel, GET /events, and it has four slots. Four
browser tabs on a node’s own page take every one of them from the till that needed one. A
POS that wanted to know a barcode had been scanned had to hold a socket open forever and hope
nobody opened the page.
Setting one up
There is no new route. The webhook is a field on the node’s config, beside every other setting:
curl -X PUT http://192.168.86.34/config \
-H 'Content-Type: application/json' \
-d '{
"webhook": {
"url": "http://192.168.86.20:8099/pos/hook",
"secret": "whatever-your-receiver-checks-against",
"events": ["scan_read", "printer_status"]
}
}'The node does not know or care where that URL points. A till on the shop LAN and a server you
run are the same thing from here, which is what keeps the local case free of anything hosted. Plain
http:// is allowed on purpose: a till on the same network has no certificate, and refusing it
would mean the feature only worked for the deployments this product exists not to require.
An unknown event name is refused with a 422 rather than dropped silently. This is the only place
that decision can be made — the mask the subscription is stored in has no room for a name nobody
recognised — and a config the node accepts and then does not honour is worse than a refusal.
What arrives
POST /pos/hook HTTP/1.1
Content-Type: application/json
X-PN-Node: pn-d938
X-PN-Seq: 42
X-PN-Signature: sha256=<hex>
{"node":"pn-d938","seq":42,"uptimeMs":1157,"event":{"type":"scan_read"}}The URL is used verbatim — the node appends no path, because the path belongs to you. The
event object is exactly what the event stream carries under the same type, so software
filtering webhooks and software filtering the stream use one vocabulary.
X-PN-Signature is HMAC-SHA256 over the exact request body, keyed on the secret you set. The node
never returns that secret; a read of the config reports only that one is stored.
There is no timestamp header, and the signature is not replay-proof
Its absence is a decision. There is no clock in this firmware — every notion of time a node has is
monotonic uptime since boot — so a date here would be a fabrication, and a fabricated timestamp
inside a replay window is worse than no timestamp, because it looks like protection. Replay defence
is yours, keyed on X-PN-Seq, which is monotonic within a boot. uptimeMs going backwards is how
you learn the node restarted and the sequence began again.
The delivery guarantees, stated plainly
At-most-once. One retry. A queue eight deep, oldest dropped when it is full. No retry at all on a 4xx — the far end understood and refused, identical bytes will not change that, and hammering an auth failure is how a node gets itself blocked.
That is not a hedge, it is a consequence. A durable queue here would mean either flash — where every write suspends a USB interrupt service routine that is not safe to suspend, which is how a 481 ms printer bind once became 17 seconds — or unbounded RAM, where a receiver that is down for an hour eats the heap the printer needs. The printer is what must not fail.
What makes that usable rather than lossy
The node already holds the record. Every scan carries a monotonic sequence number on both the event and every row of the scan log. So:
a receiver that sees the sequence go 41 → 43 fetches the scan log, finds 42, and knows exactly what it missed — including whether it was a read a rule refused rather than one that was lost.
Treat the push as a latency optimisation over a durable ring, and the scan log as the authority. That is the integration pattern, not a workaround for it.
Delivery health
The node reports what came of the URL you set — how many were delivered, how many dropped, how many are queued, the current sequence number, the last HTTP status it saw and how long ago it tried. Config is what you set; status is what came of it.
queued is what separates a receiver that is slow from one that is gone. A slow receiver has a
queue and a climbing age. A gone receiver has a last status of -1 and drops.
Three rules the secret inherits
Each of these was bought with a bug elsewhere on this API, and they apply to the webhook secret exactly as they do to a device token:
- An empty secret means clear it. Presence, not truthiness, decides whether a field applies.
- Over-length is refused, never truncated. A truncated credential is indistinguishable from a working one until the far end starts rejecting things.
- A URL that actually changes clears the stored secret unless a new one arrives with it, so a credential cannot follow a URL to a host it was not issued for.
No relay, and that is on the record
There is deliberately no hosted relay for POS software that cannot receive a POST on a local network. That is a decision, not a gap — the node calling a URL you control is what keeps the print path free of anything hosted. If your software cannot listen on the shop LAN, the pull side of the API is the integration: poll the scan log, which is the authority anyway.
Something to point a node at
Before you write a handler, point the node at anything that logs a request body and a header — the signature is the only part that needs code, and the section above is the whole of it. The digital twin speaks the same protocol on your laptop, so the loop runs with no hardware at all.
Frequently asked questions
- Can I have more than one webhook URL per node?
- No — one URL per node. Fan out on your side if you need several consumers; a node has neither the heap nor the sockets to be a message broker.
- What happens to events while my receiver is down?
- Up to eight queue, then the oldest are dropped, and each delivery gets one retry. Nothing is stored to flash. When your receiver comes back, reconcile from the scan log using the sequence numbers rather than expecting a backfill.
- How do I verify the signature?
- Compute HMAC-SHA256 over the exact bytes of the request body using your shared secret, hex-encode it, and compare against the part of the header after `sha256=`. Compare in constant time, and compute over the raw body rather than a re-serialised parse of it.
- Should I use webhooks or the event stream?
- Use webhooks when your software can listen on the shop network and you do not want to hold a connection. Use the stream when you are already holding one — a dashboard, an installer tool — and remember it has four slots that browser tabs also consume.
- Does the node need the internet for this?
- No. The node POSTs to the address you gave it, which on a normal install is another box on the same LAN. Nothing hosted is in the path.
Related reading
- The Node HTTP API: Scope, Auth, and LimitsWhat a node's local HTTP/JSON API covers, which tier gates each route, and the limits worth knowing before you integrate. The route table is linked, not copied.
- Node API Error Codes and Retry AdviceEvery error string the node API can answer with, the HTTP status it arrives on, the routes that emit it, and whether retrying the same request can ever work.
- Quickstart: From Box to First ReceiptSet a node up end to end — power, the setup Wi-Fi, joining your network, confirming the peripheral bound, printing — including when no printer is attached.