Print from any browser, no print dialog

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.

28 ms print() → printed, loopback
1284 ms median, phone → relay in Germany, 290 ms RTT
ALPHA

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.

How it works

Browseryour web app
Routesame PC · LAN · relay
Agentnext to the printer
PrinterCUPS · Windows · 9100

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.

Same PCWeb app and agent on one computer. The agent answers on http://127.0.0.1:8789; browsers allow plain http to localhost. No relay.
LANOther devices on the network call the agent on https://<lan-ip>:8790. Each device must trust the agent's own CA once (or via MDM).
RelayThe agent holds an outbound WebSocket to a relay; your page talks to the relay. Works through NAT and from anywhere. The relay is self-hostable.

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 tester guide

  1. 1

    Get the agent

    on request

    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.

  2. 2

    Start with a virtual printer

    config.toml

    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.toml
  3. 3

    Mint keys

    printproxy-relay gen-key

    Each 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.toml
  4. 4

    Pair by fingerprint + code

    once per agent

    The 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/pair
  5. a name alone → 400 fingerprint_required · wrong code → pair_err, nothing bound
  6. 5

    Open the console

    /console/

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

    printproxy console at phone width, Jobs tab, two text jobs marked PRINTED on virtual-pdf
  7. 6

    Print from your page

    printproxy.js 0.4.1
    <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.

What it prints

kindmacOS / LinuxWindowsTCP 9100 printer
pdfCUPS (lp)SumatraPDFrefused
image png, jpegCUPS, fit to pageSumatraPDFrefused
text, markdownrendered to an A4 PDF inside the agent, then as pdf, so every OS prints the same pagesrefused
raw ESC/POS, ZPLCUPS rawraw TCP printer onlysent as-is
htmlrefused (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.

Troubleshooting

You seeWhyFix
LAN route never wins on iPad / Chromebook / AndroidAn 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 accessChrome'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_limitedThe 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_oldThe agent predates the job kind (image, text, markdown need 0.3.0 or later).Update printproxy-agent.
400 fingerprint_requiredPairing named the agent by display name only.Pair with the fingerprint from <state_dir>/fingerprint.
job stays queued, then expiredNo paired agent with that printer is online.Start the agent; check it in the console's Printers tab.

Security

Join the alpha

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.