API documentation

PrintTrays exposes a JSON-over-WebSocket protocol on wss://127.0.0.1:8181 (with automatic fallback ports), plus a thin client SDK that wraps it.

Getting started

Four steps, start to finish.

  1. Install PrintTrays on the machine that has the printer — see Download. It runs quietly in the system tray from then on, starting automatically at login.
  2. Include the client SDK in your page, as a native ES module (no build step, no bundler required):
    <script type="module">
      import { PrintTrays } from "https://your-host.example.com/printtrays.js";
    </script>
  3. Connect — this is also the point where, the very first time on a given browser profile, the user may need to click through a one-time certificate trust step (PrintTrays opens this as a small self-closing popup rather than sending the user to a separate tab):
    const pp = new PrintTrays();
    await pp.connect();
    console.log("Connected to bridge v" + pp.version);
  4. Print:
    const printers = await pp.getPrinters();
    await pp.print(printers[0].name, "<h1>Hello</h1>", { type: "html" });

Client SDK

import { PrintTrays } from "./printtrays.js";

const pp = new PrintTrays({
  host: "127.0.0.1",             // default
  port: undefined,                // default: tries 8181, then 18181, then 28181
  connectTimeoutMs: 2500,
  downloadUrl: "https://your-site.example.com/download",
});

await pp.connect();
MethodDescription
connect()Opens the WSS connection (trying each fallback port in turn), pins the bridge's signing key on first use, and sets pp.version to the connected bridge's version string. Throws PrintTraysNotInstalledError if unreachable.
getPrinters()Resolves to { name, isDefault, status, driver }[] for every OS-installed printer.
print(printer, data, options)options.type is one of pdf | html | raw | zpl | escpos. See below.
sendRaw(port, bytes, options?)Raw serial/HID/USB/TCP write (and optionally read). port is "COM3", "tcp:host:port", "hid:<path>", or "usb:<vid>:<pid>:<endpoint>". options.readTimeoutMs reads a response back (base64, in the result); leave bytes empty for a read-only request. Serial-only options: baudRate, dataBits, stopBits, parity, rtscts, xon, xoff.
usbControlTransfer(params)Low-level USB control transfer for device configuration/status exchanges outside normal data endpoints. params.data (base64) for an OUT transfer, or params.readLength for an IN transfer.
listDevices(type)type is usb | serial | hid.
disconnect()Closes the socket.

Wire protocol

Every request is a single JSON object with an id and action; every response mirrors that id.

listPrinters

// →
{ "id": "abc", "action": "listPrinters" }

// ←
{ "id": "abc", "action": "listPrinters", "ok": true,
  "result": [{ "name": "EPSON TM-T88", "isDefault": true }],
  "timestamp": 1234567890, "signature": "...", "keyFingerprint": "..." }

print

{
  "id": "abc",
  "action": "print",
  "printer": "EPSON TM-T88",
  "type": "escpos",           // pdf | html | raw | zpl | escpos
  "data": "...",               // utf8 text, or base64 if binary
  "encoding": "base64",        // optional, defaults to utf8
  "copies": 1,                 // optional
  "connection": "tcp:192.168.1.50:9100",  // optional: bypass OS spooler

  // The following only apply to type "pdf"/"html":
  "duplex": "longEdge",        // optional: simplex | shortEdge | longEdge
  "landscape": true,           // optional
  "paperSize": "A4",           // optional: A3 | A4 | A5 | Legal | Letter | Tabloid
  "color": false,              // optional: defaults to true
  "scaleFactor": 100,          // optional: percentage
  "margins": { "marginType": "none" }  // optional
}

rawWrite

{
  "id": "abc",
  "action": "rawWrite",
  "port": "COM3",              // or "/dev/ttyUSB0", "tcp:host:port",
                                // "hid:<path>", or "usb:<vidHex>:<pidHex>:<endpoint>"
  "bytes": "...",               // leave "" for a read-only request
  "encoding": "utf8",          // utf8 | base64 | hex
  "readTimeoutMs": 500,        // optional: read a response back (base64)
  "readLength": 64,            // optional, usb: ports only

  // Serial ports only, beyond baudRate:
  "baudRate": 9600,
  "dataBits": 8,                // 5 | 6 | 7 | 8
  "stopBits": 1,                 // 1 | 1.5 | 2
  "parity": "none",             // none | even | mark | odd | space
  "rtscts": false,
  "xon": false,
  "xoff": false
}

usbControlTransfer

{
  "id": "abc",
  "action": "usbControlTransfer",
  "vendorId": 1155,
  "productId": 22352,
  "bmRequestType": 33,
  "bRequest": 9,
  "wValue": 512,
  "wIndex": 0,
  "data": "..."                 // base64, for an OUT transfer
  // or: "readLength": 8        // for an IN transfer instead
}

listDevices

{ "id": "abc", "action": "listDevices", "type": "serial" }  // usb | serial | hid

Trust & signing

How PrintTrays avoids QZ Tray-style repeated certificate prompts.

Roadmap

PrintTrays now covers essentially the full QZ Tray feature set — silent printing with full page-setup options, raw ESC/POS/ZPL, and USB/HID/serial device I/O including control transfers and configurable serial parameters. On the platform side:

StatusDetails
WindowsBuilt and tested end-to-end across multiple real machines
Linux (.deb / .AppImage)Built and functionally verified in a real Linux container: launches, generates its certificate, binds the bridge, and a real WebSocket client completed a full signed round-trip. Only the desktop tray icon and the .deb installer's certificate-import script remain unverified on real hardware
macOSConfirmed hard-blocked (not assumed): .pkg fails on a missing Apple-only binary (productbuild), and .dmg's dependency explicitly restricts itself to macOS at the npm level. Needs real Mac hardware or cloud Mac CI to proceed

See QZ-TRAY-FEATURE-PARITY.md in the project source for the full, item-by-item comparison.