The Node HTTP API: Scope, Auth, and Limits

What 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.

Published 2026-09-09 · All documentation

A node serves one HTTP/JSON API on port 80 of your own network, and raw ESC/POS on port 9100 for software you cannot change. This page is what the surface covers, what gates it, and the limits worth knowing before you write against it.

What it covers

Printing and raw byte pass-through. Live hardware state — paper, cover, drawer — read over DLE EOT. A server-sent event stream. Configuration. The cash-drawer pulse. A scan log with a verdict on every read. Weight from a USB scale. Camera frames and a stream on the SKU that has a sensor. A diagnostic capture ring. Peer discovery. Network settings, including a fixed address. The cloud link. Composed print jobs. Firmware state and replacement. Boot history and the crash record.

Every response is JSON unless a route says otherwise, and one contract package — shared by the firmware, the digital twin, the tooling and this site — types the shapes.

Why there is no route table on this page

There is one, and it is not here. A node registers more handlers than any marketing page keeps up with, and the authoritative list is the firmware’s own route table — the internal reference that mirrors it opens by warning that its own counts go stale, and it was right: a row in it described a route that had been removed months earlier, and nothing arithmetic could catch that because the counts stayed consistent the whole time.

A partial list of an API is worse than no list, because it reads as complete. So these pages publish the parts that do not move — the auth model, the error vocabulary, the limits below, and the webhook contract — and describe the surface rather than tabulating it. Ask a node: GET /status on a device or on the digital twin is the current answer, always.

Print

One call in, hardware bytes out.

curl http://proxynodes-7f3a.local/print \
  -X POST -H 'Content-Type: application/json' \
  -d '{
    "lines": [
      { "type": "banner", "value": "ORDER", "align": "center" },
      { "type": "text", "value": "Table 4", "bold": true, "size": "2x" },
      { "type": "barcode", "symbology": "qr", "value": "ORDER-1042" },
      { "type": "cut" }
    ]
  }'

Seven line types: text with weight, alignment and double width or height; banner lettering the node draws itself; rules; feeds; cuts; barcodes; and 1-bit rasters. The node wraps text at the paper’s real column count — a measured 48 at 80 mm — so a line that will not fit comes back as an error rather than as a receipt missing its right-hand half.

Flowchart of the print path: where bytes enter from the JSON print route, raw port 9100, the raw byte route and a composed job; how they are chunked; and how the transfer handles a stall, a timeout, a short transfer and the terminating zero-length packet.
Four entrances, one path to the printer. The chunking layer is where a stall, a timeout and a short transfer are told apart — a stall completes the transfer, so the endpoint is reset and the device stays bound: one job is lost, not the printer.

The limits worth knowing up front

These are the ones that change how you write the client, and every one of them is a refusal rather than a silent clamp:

  • A print job renders into a bounded buffer. A job that renders past it is refused with nothing written, because a truncated receipt looks finished.
  • Raw bytes are capped per request, and bad base64 is refused.
  • The event stream has four slots. A fifth subscriber is refused with 503 no_slots, and the check runs before the stream preamble is written — it used to answer 200 OK and then be permanently silent while holding a socket. Browser tabs on the node’s own page consume the same four slots, which is why a poll route exists for weight and why webhooks exist for events.
  • The camera streams to one client at a time, and refuses a stream when fewer than two event slots are free.
  • A config write is strict: an unknown key is refused rather than ignored, so a typo cannot read as success.
  • A fixed IPv4 address replaces rather than patches. Every other write on this API is a partial patch; that one starts from empty and fills only what the body carried, so omitting the DNS server erases it. It takes effect on the next boot and says so — re-addressing a live interface drops every socket on it, including the one carrying the response.

Reading the hardware honestly

{
  "identity": { "deviceId": "pn-7f3a", "variant": "wifi" },
  "online": true,
  "printers": [{
    "endpoint": "receipt", "online": true,
    "paperOut": false, "coverOpen": false, "drawerOpen": false
  }]
}

The interesting part is what happens when a reading cannot be taken. A printer with no status channel can never report paper state, and that is a property of the device rather than a fault — so the driver layer models it as unknown rather than as "no faults". It still reports itself online, because it is plugged in and printing. Status separates a peripheral that was never plugged in, one refused for want of a USB channel, one seen and gone with an age, and one bound and silent, because those four need different actions from whoever is standing in front of it.

Discovery, and finding the rest of the fleet

Nodes advertise over mDNS, so proxynodes-<id>.local resolves on the LAN without anybody typing an address. They also beacon to each other over UDP and serve a peer list, which means one known address reaches the whole fleet.

Flowchart of the peer beacon: broadcasts every five seconds, a fifteen-second time to live on each entry, and the table keyed on the datagram's observed source address rather than the address the beacon claims.
The peer table keys on where a datagram actually came from, not on the address inside it — a beacon cannot talk its way into the table as somebody else. An empty list is honest both for a node alone on the network and for one whose beacon task never started, which is what makes it safe to serve unconditionally.

Two things people expect and do not get

There is no remote access to a node, and none is planned. No NAT traversal, no tunnel, no hosted path for print jobs. Everything on this page happens on the network the node is plugged into. That is the product, not a limitation of it.

Port 9100 has no authentication and cannot have any. It is a socket carrying printer bytes; there is no header, handshake or envelope anywhere in that protocol to put a credential in. It is protected by network segregation or it is not protected. Plan the segment.

Where to go next

  • Error codes — the complete vocabulary with retry advice. Read this before you write your first error branch.
  • Auth — the three tiers, the header, and the firmware credential.
  • Webhooks — the node calling you, and the honest delivery guarantees.
  • Hardware variants — which endpoints a given SKU declares at all.
  • The simulator — the same API on your laptop, with fault injection.

Frequently asked questions

Can I get the full route list?
Ask a node or the twin: the identity and status responses describe what that build serves, and an unrouted path answers with a clear refusal rather than a silence. A frozen table on a web page is exactly what went stale in this repo's own internal reference, so this page links the surface instead of copying it.
Do I have to use the JSON API at all?
No. If your software already prints raw ESC/POS to network printers, point it at the node's port 9100 and there is no integration — it answers DLE EOT status queries too. The JSON API is what you use when you want structured status, scans, weight or configuration.
Does anything in the print path need the internet?
No, with exactly one exception, and it is named on this site wherever it comes up: composing a receipt from a description relays to a hosted model, so that feature needs a connection. Printing, status, the drawer, scans and weight never do.
Is the JSON contract versioned?
The shapes are typed in one shared package that firmware, the twin, the tooling and this site all compile against, and a contract check holds the firmware's strings to it. The `error` field is deliberately an open string rather than an enum so a node that is newer than your client does not fail validation.
How do I test without hardware?
Run the digital twin. It serves the same HTTP API, the same port 9100 behaviour and the same telemetry, and it can inject faults — paper out, cover open, offline — that you cannot schedule on a real printer.

Related reading