printproxy lets a web app print straight to the printer next to you, from an iPad, Chromebook, Android phone or desktop browser. A small agent sits beside the printer and prints only jobs signed by the site you paired it with.
The real take, 18 seconds, uncut: Mobile Safari on an iOS 18.7 simulator loads a page, the page calls connect(), printers() and print(), and the job goes sent → printed on a virtual PDF printer. No dialog appears. The printed file is byte-identical to the PDF the phone sent.
Recorded 2026-10-16 with a file printer; no physical printer was used.
Your page loads printproxy.js and calls print(). The job reaches printproxy-agent, a single binary on a computer next to the printer, by one of three routes. Every route speaks the same API (/v1/printers, /v1/jobs), and printproxy.connect() probes all the routes you configure at once and uses the first that answers, in the order same PC, LAN, relay.
iPad, Chromebook and Android cannot run the agent. Put the agent on another always-on box next to the printer: a PC, a Mac, or a Linux box such as a Raspberry Pi. Those devices then reach it through the relay (works today) or on the LAN once they trust the agent's CA.
Alpha builds are sent on request: macOS, Linux (including 64-bit and 32-bit Raspberry Pi) and Windows. Join the alpha and tell us which.
Prove the path before any paper moves. A file printer writes each job to a new file:
printproxy-agent default-config > config.toml
# in config.toml:
[[file_printers]]
name = "pdf-file"
dir = "/tmp/printproxy-out"
printproxy-agent printers --config config.toml # read-only list
printproxy-agent run --config config.tomlEach key is printed once; the relay stores only its sha256. Keep one role = "admin" key for pairing (backend and console only) and give the web page a role = "print" key, which can list printers and print, nothing else.
printproxy-relay gen-key --site clinic
printproxy-relay default-config > relay.toml # add the [[keys]] block
printproxy-relay serve --config relay.tomlThe agent writes a single-use code (ABCD-EFGH, 10 minutes, burned after 5 wrong guesses) to <state_dir>/pairing_code and its fingerprint (3f9a-01bc-77de-5a20) to <state_dir>/fingerprint. Pair with both, then check the fingerprint in the answer matches the box.
curl -H "Authorization: Bearer $PP_ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d '{"fingerprint":"3f9a-01bc-77de-5a20","code":"ABCD-EFGH"}' \
https://<your-relay>/v1/pairThe relay serves an admin console at /console/: printers per agent, jobs (in-flight ones update live), agents and pairing, API keys. Sign in with the admin key; it stays in that tab's sessionStorage. Use it on a trusted device, over HTTPS.

<script src="https://<your-relay>/printproxy.js"></script>
<script>
const c = await printproxy.connect({
local: { url: "http://localhost:8789", apiKey: "ppl_…" },
lan: { url: "https://192.168.1.20:8790", apiKey: "ppl_…" },
relay: { url: "https://<your-relay>", apiKey: "pp_…" },
});
c.route; // "local" | "lan" | "relay"
const list = await printproxy.printers();
await printproxy.print(pdfBlob, { printer: "pdf-file" });
</script>
Configure only the routes you use. The ppl_ key comes from printproxy-agent local-key --config config.toml. Set cors_origins to your web app's origin on the agent and the relay.
| kind | macOS / Linux | Windows | TCP 9100 printer |
|---|---|---|---|
pdf | CUPS (lp) | SumatraPDF | refused |
image png, jpeg | CUPS, fit to page | SumatraPDF | refused |
text, markdown | rendered to an A4 PDF inside the agent, then as pdf, so every OS prints the same pages | refused | |
raw ESC/POS, ZPL | CUPS raw | raw TCP printer only | sent as-is |
html | refused (unsupported_kind): render HTML to PDF in your app and send pdf | ||
Receipts and labels. Add the printer's address to the agent and send kind: "raw" with ESC/POS or ZPL bytes:
[[printers]]
name = "receipt"
addr = "192.168.1.50:9100"
Windows. PDF, images, text and markdown print through a bundled, unmodified SumatraPDF.exe shipped beside the agent. Windows printing is new in this alpha and has not yet been verified against a real Windows print queue; tell us what you see.
| You see | Why | Fix |
|---|---|---|
| LAN route never wins on iPad / Chromebook / Android | An https page cannot call http on the LAN (mixed content), and the device does not trust the agent's certificate yet. | Set lan_bind, open https://<agent-ip>:8790/ca.crt on the device, install it and turn on full trust (compare the fingerprint with the agent log first). Until then connect() uses the relay. |
| Chrome asks to allow local network access | Chrome's Private / Local Network Access rules for public pages calling private addresses. | The agent answers the preflight; allow the prompt once per site. |
HTTP 429 rate_limited | The relay's per-key rate limit, or too many agent connections from one address. | Slow down, batch jobs, or call the relay from your backend. |
job failed: agent_too_old | The agent predates the job kind (image, text, markdown need 0.3.0 or later). | Update printproxy-agent. |
400 fingerprint_required | Pairing named the agent by display name only. | Pair with the fingerprint from <state_dir>/fingerprint. |
job stays queued, then expired | No paired agent with that printer is online. | Start the agent; check it in the console's Printers tab. |
printproxy is in alpha and has no public price. A one-time licence, no subscription, is available on request for early sites and integrators. Leave your details and we will send an alpha build.
Sources: 28 ms is one take on a single Mac (phone simulator, relay and agent all local), 2026-10-16. 1284 ms is the median of three takes (1222, 1284, 1609 ms) from a phone simulator in India through a relay on a server in Germany to an agent back on the Mac, 290 ms round trip, 2026-10-16. Both used a file printer.