Print to a Receipt Printer from a Web App
Four working ways to print receipts from a web app — QZ Tray, WebUSB, cloud relays, and a LAN print API — with code, browser limits and honest trade-offs.
Published 2026-07-24 · Updated 2026-09-21 · For developers →
Browsers cannot open a TCP socket, claim a USB endpoint, or touch a serial port from ordinary JavaScript — so there is no direct way to send receipt-printer bytes from a web page. Every working setup routes the job through something else: a local agent (QZ Tray), a browser hardware API (WebUSB/Web Serial), a cloud relay (PrintNode), or a print API on the LAN. This guide shows working code for all four, with the trade-offs printed on the label.
Why window.print() fails for receipts
window.print() renders your page as a document through the operating system's print pipeline. That is the wrong tool for a receipt in at least five ways:
- It prints HTML, not ESC/POS. Receipt printers are commanded with byte sequences —
1B 40to initialize,1D 56 00to cut. The OS driver rasterizes your CSS instead, usually onto an A4/Letter page model that a 80 mm roll printer mangles. - The dialog needs a click. Browsers will not silently print. A cashier confirming a print dialog on every order is a non-starter; the workarounds (Chrome's
--kiosk-printingflag) mean you control the machine anyway. - No cutter, no drawer. Paper cut and the cash-drawer kick pulse are ESC/POS commands. A driver-rendered page can't send them.
- No status. You get no signal for paper-out, cover-open, or offline. The job "succeeds" into a void.
- A driver per workstation. You're back to installing and babysitting printer drivers — the thing a web app was supposed to avoid.
So the real question is: which piece of software turns your HTTP-world request into printer-world bytes, and where does it run? There are four honest answers.
Option 1: QZ Tray — a local WebSocket agent
QZ Tray is a Java application that bridges browser printing. A workstation installation uses a local WebSocket, as in the example below. QZ also documents a dedicated print server for mobile clients including iOS, Android and ChromeOS. That arrangement connects to the server rather than localhost and requires its own client connection and certificate setup. Validate the intended browser versions; the vendor guide includes dated platform instructions.
<script src="qz-tray.js"></script>await qz.websocket.connect();
const config = qz.configs.create("EPSON TM-T88V Receipt"); // OS printer name
const data = [{
type: "raw",
format: "command",
flavor: "plain",
data:
"\x1B\x40" + // ESC @ initialize
"ORDER #42\x0A" +
"1x Flat White 4.50\x0A" +
"\x1B\x64\x04" + // ESC d 4 feed 4 lines
"\x1D\x56\x00", // GS V 0 full cut
}];
await qz.print(config, data);Silent operation requires correctly configured message signing and trust. Published first-year packages are $749 Premium Support and $3,499 Company Branded, checked September 19, 2026; renewal and multi-year rates differ. Buying a package alone does not configure signing. Maintain the workstation or print-server installation and its client trust arrangement.
Good fit: existing OS-installed printers and teams that can manage a workstation or print server. Tradeoff: software deployment and certificate management remain necessary. Tablets alone do not rule QZ out; evaluate the documented print-server arrangement.
Option 2: WebUSB and Web Serial — no agent at all
Chromium browsers can talk to USB and serial devices directly, which means a page can send ESC/POS bytes to a thermal printer with no installed software at all:
const device = await navigator.usb.requestDevice({
filters: [{ classCode: 7 }], // USB printer class
});
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
const bytes = new TextEncoder().encode("\x1B\x40ORDER #42\n\n\n\n\x1D\x56\x00");
await device.transferOut(1, bytes);It genuinely works — on the right stack. The constraints are sharp: Chrome/Edge only (no Safari, no Firefox), HTTPS plus a user gesture for the device picker, and on Windows you typically have to replace the printer's driver with WinUSB before the browser may claim it, which breaks printing from every other application on that machine. The full ceremony — interface discovery, Web Serial, the Windows driver swap, status reads — is covered in our deep dive: WebUSB and Web Serial receipt printing.
Good fit: single-station kiosks and PWAs where you control the hardware and the browser. Weak fit: anything multi-terminal, anything on iPads, shared printers.
Option 3: A cloud print relay (PrintNode)
Cloud relays invert the problem: your server posts the job to their API, and a desktop client they provide — running on a PC, Mac, or Raspberry Pi at the shop — polls for jobs and prints them locally.
// Server-side (Node). Never call this from the browser —
// the API key must not ship to the client.
const receipt = "\x1B\x40ORDER #42\n\n\n\n\x1D\x56\x00";
const res = await fetch("https://api.printnode.com/printjobs", {
method: "POST",
headers: {
Authorization: "Basic " + Buffer.from(API_KEY + ":").toString("base64"),
"Content-Type": "application/json",
},
body: JSON.stringify({
printerId: 123456, // from GET /printers
title: "Order #42",
contentType: "raw_base64",
content: Buffer.from(receipt, "binary").toString("base64"),
}),
});PrintNode's pricing, checked September 19, 2026, lists a free 50-request monthly plan, $9/$29/$99 hosted tiers and $60/$500 integrator plans. Requests are metered; client computers must remain available. New hosted delivery needs service connectivity, while already downloaded or spooled jobs may behave differently. PrintNode also offers a separately quoted Standalone Server for private deployment. The cloud example here does not establish that option's requirements or outage behavior.
Good fit: e-commerce back offices, low-volume remote printing, teams that want zero networking work. Weak fit: front-of-house POS, kitchen tickets, anywhere offline operation matters.
Option 4: A print API on the LAN
The fourth pattern moves the byte-translation onto the local network: some device or service near the printer exposes an HTTP API, and your web app calls it with fetch() — from an origin the browser will allow, which is the part this pattern lives or dies on (see the boundary note below). The device print path can stay local. Outage operation also requires your application, authentication, order data and on-site backend to remain available; a local printer cannot make a cloud-dependent POS work offline. The service can be a Raspberry Pi running python-escpos behind Flask, a vendor "intelligent printer" web service, or a dedicated node.
Proxy Nodes builds this pattern into a palm-size device: a node sits next to the printer and serves an open HTTP/JSON API on the LAN. An on-site server or native client posts a small document model; the node renders it to ESC/POS — wrapping text node-side, 48 columns by default on an 80 mm roll — and talks USB to the printer:
// Save as print-receipt.mjs and run with Node.js on the printer LAN.
// Replace the hostname and check GET /peripherals for the endpoint ID.
const response = await fetch("http://proxynodes-pn-b2c3.local/print", {
method: "POST",
headers: {
"Content-Type": "application/json",
...(process.env.PN_API_KEY ? { "X-PN-Api-Key": process.env.PN_API_KEY } : {}),
},
body: JSON.stringify({
type: "print",
id: crypto.randomUUID(),
endpoint: "receipt",
lines: [
{ type: "text", value: "ORDER #42", bold: true, align: "center" },
{ type: "rule" },
{ type: "text", value: "1x Flat White 4.50" },
{ type: "feed", lines: 2 },
{ type: "cut" },
],
}),
});
const result = await response.json();
if (!response.ok || result.ok !== true) {
throw new Error(result.error ?? ("HTTP " + response.status));
}
console.log(result);
// A timeout or write failure may follow partial output. Do not auto-replay.The hostname is discovered, not configured: nodes advertise over mDNS as _proxynodes._tcp. The response tells you whether the job was accepted; GET /status reports paper, cover, and drawer state, and the driver layer models unreadable state as known: false instead of guessing "everything's fine." SSE on GET /events streams state changes, so paper-out reaches your UI before the cashier notices. POST /drawer/kick fires the drawer pulse in 0.15 s, and the same node accepts raw ESC/POS on :9100 — with DLE EOT status replies — for software you can't change. The shipping hardware is Proxy Node Raw, running Nodeware 1.2.55 or later. For software-only tests, request simulator evaluation access to the developer
workspace first. Its modeled printing API and injected paper/cover states can exercise
client logic, but do not qualify USB, power, actual receipt output or a named POS. It
does not enforce the device's API authentication or deliver configured webhooks.
The CORS and mixed-content reality
Use the node-served same-origin UI, a native/server LAN HTTP client, or an on-site backend that proxies the browser request from the browser app's own origin. A separate web server on the LAN is still a different origin.
Direct cross-origin browser integration is not qualified on shipping Nodeware: JSON requests need CORS preflight handling, and readable status/SSE responses need an explicit device origin policy. That policy is not implemented by the device. Do not disable browser security to work around it.
Browser platform support is a separate requirement. Chrome 142 introduced Local Network Access permission and mixed-content exceptions for qualifying local requests, replacing its earlier PNA effort. Permission does not supply CORS or device authorization. See Chrome's LNA guidance. Safari and Firefox must be qualified separately; this is not an all-browser promise.
The four options, honestly compared
| QZ Tray | WebUSB / Web Serial | Cloud relay (PrintNode hosted) | LAN print API | |
|---|---|---|---|---|
| Install per workstation | Agent locally, or shared print server with client trust setup | none (browser only) | none (client per site) | none |
| Extra hardware per site | Existing workstation or dedicated print server | no | PC/Mac/Pi running client | the node |
| Works with internet down | Depends on app and local deployment | Depends on app availability | New hosted delivery needs connectivity | Device path is local; app/backend must also work offline |
| Browser support | Qualify chosen desktop/mobile deployment | Chrome/Edge only | all (server-side call) | via same-origin backend; direct access unqualified |
| Printer status (paper/cover) | Printer/job status API; test actual driver/device | possible, often write-only | job-state only | yes, as JSON + live SSE |
| Recurring cost | $0; paid support starts at $749 for the first year | $0 | $9–500/mo, metered per print | none — no metering, no certificates |
| Sharpest edge | cert + agent lifecycle | Windows driver swap, no Safari/Firefox | offline = no printing | CORS/mixed-content setup |
Which one should you pick?
- You need Safari/Firefox and printers already installed on the OS: QZ Tray. Budget for certificates and agent management.
- Single kiosk, you own the machine, Chrome only: WebUSB or Web Serial. Read the deep dive before committing — the Windows driver swap surprises people.
- Low-volume, remote, back-office printing: a cloud relay is the least engineering. Accept the metering and the internet dependency.
- POS, kitchen, or anything that must print during an outage: put the printing on the LAN. That can be a Pi you maintain, an "intelligent" printer ($275–541 for an Epson TM-m30III), or a node purpose-built for it — one that drives the $40–160 generic printer of your choice. See the receipt printer API guide for the whole pattern space, and port 9100 printing for what network printers speak underneath.
Frequently asked questions
- Can a browser print to a receipt printer silently, with no dialog?
- Not through window.print() without controlling the machine (kiosk flags). Silent printing requires one of the four byte-level routes: a local agent like QZ Tray, WebUSB/Web Serial in Chromium, a cloud relay called from your server, or an HTTP print API on the LAN.
- Does window.print() work at all for receipts?
- It can produce a readable receipt if the driver is configured for 80 mm roll paper, but it cannot cut, kick a cash drawer, or report paper-out, and it shows a dialog. It is a fallback, not a POS printing architecture.
- What is the lowest-cost way to print from a web app?
- WebUSB/Web Serial has no software cost but the narrowest browser support. QZ Tray software is free; its Premium Support package is listed at $749 for the first year and signing must be configured. PrintNode has free and paid hosted tiers plus separately quoted private deployment. A LAN print API has no per-print fees and no certificates, and it drives $40–160 generic ESC/POS printers.
- Why does my HTTPS web app fail to reach a printer on the LAN?
- Browser permission and mixed-content rules vary, and separate origins still require device CORS support. Shipping Nodeware does not implement that support. Use the node-served UI or an on-site backend that proxies requests from your app origin.
- Do any of these report paper-out or cover-open back to my app?
- Cloud relays report job state, not printer sensors. QZ documents printer and job status callbacks; test the actual printer and driver rather than assuming a fixed sensor limitation. WebUSB can read status only if the printer exposes an IN endpoint. A LAN print API polls the printer over DLE EOT and returns real sensor state as JSON — a node reports state it cannot read as unknown rather than guessing, and streams changes live over SSE.
- Can I test the LAN print API route before buying hardware?
- Request developer-workspace evaluation access at /simulator first; there is no standalone public download. The twin models printing, status and paper/cover faults. Passing simulator checks does not prove hardware compatibility, authentication, webhook delivery or physical network-failure behavior; validate those on the intended installation.
Related reading
- WebUSB and Web Serial Receipt PrintingPrinting to thermal printers straight from Chrome with WebUSB/Web Serial: working code, browser support reality, and where the approach breaks down.
- Receipt Printer API: the Complete GuideEvery way software prints receipts in 2026 — ESC/POS, port 9100, vendor SDKs, cloud relays — and how a local HTTP API on the LAN compares.
- Port 9100 Printing: Raw TCP for POS, ExplainedHow raw port 9100 printing works, why every POS uses it, how to test it with netcat, and its blind spots — status, discovery, and error handling.