ZPL Printing from a Browser: The 2026 Developer Guide
Zebra printers speak ZPL — a text-based language of commands. Sending ZPL from a web app used to require Java or a signed browser plugin. Here's the clean modern way.
Published April 2026 · 8 min read · Developer Guide
TL;DR
Install PrintBridge on the Windows machine with the Zebra printer. Then POST your ZPL string to http://127.0.0.1:1337/print with format: "zpl". Done. No Java, no WebSocket handshake, no cert.
What is ZPL?
Zebra Programming Language (ZPL) is the native command language for Zebra Technologies label printers — the ZT, ZD, GX, and GK series. A ZPL document is plain text (ASCII or UTF-8) with commands prefixed by a caret (^) or tilde (~).
Every ZPL job starts with ^XA (start label) and ends with ^XZ (end label). Between those, you place text fields, barcodes, graphics, and formatting commands.
// Minimal ZPL shipping label
^XA ^FO50,50^GFA,500,500,10,...^FS ^FO50,120^ADN,36,20^FDOrder #12345^FS ^FO50,170^ADN,24,12^FDShip to: Jane Smith^FS ^FO50,200^ADN,24,12^FD123 Main St, Austin TX 78701^FS ^FO50,260^BY3^BCN,80,Y,N,N^FD1234567890^FS ^XZ
Why you can't send ZPL directly from a browser
Web browsers have no API to open a raw socket or write directly to a printer port. window.print() renders the DOM through the OS printing subsystem — it can't send raw ZPL bytes.
Your options without a local agent:
- Cloud-to-printer services (e.g. PrintNode, Printix) — your ZPL travels to a cloud service, then back to the printer. Adds latency, requires cloud dependency, and your label data leaves the machine.
- QZ Tray — WebSocket bridge, requires Java, complex client setup.
- Raw TCP socket from your backend — works only if your server is on the same LAN as the printer. Doesn't work for remote workers or SaaS products.
A local REST agent like PrintBridge solves this cleanly: the ZPL never leaves the machine, there's no cloud dependency, and your web app integration is a single fetch().
Sending ZPL from JavaScript with PrintBridge
Step 1: Discover your Zebra printer name
const res = await fetch('http://127.0.0.1:1337/printers')
const { printers } = await res.json()
// e.g. "ZDesigner ZT410-300dpi ZPL"
const zebraPrinter = printers.find(p => p.name.includes('ZDesigner'))
console.log(zebraPrinter.name)Step 2: Build your ZPL payload
function buildShippingLabel(order) {
return [
'^XA',
'^CF0,50',
`^FO50,50^FDOrder #${order.id}^FS`,
`^FO50,120^FD${order.recipient}^FS`,
`^FO50,180^FD${order.address}^FS`,
// Barcode (Code 128)
`^FO50,260^BY3^BCN,80,Y,N,N^FD${order.trackingNumber}^FS`,
'^XZ',
].join('\n')
}Step 3: Send to PrintBridge
async function printZplLabel(printerName, zpl) {
const response = await fetch('http://127.0.0.1:1337/print', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
printer: printerName,
format: 'zpl',
data: zpl,
}),
})
if (!response.ok) {
const err = await response.json()
throw new Error(err.message ?? 'Print failed')
}
return response.json()
}Printing copies
Need 3 copies of a label? Add a copies field — PrintBridge handles the repetition without re-sending the full payload each time:
{
"printer": "ZDesigner ZT410-300dpi ZPL",
"format": "zpl",
"data": "^XA...^XZ",
"copies": 3
}Common ZPL commands reference
| Command | Purpose |
|---|---|
| ^XA / ^XZ | Start / end label |
| ^FO x,y | Field origin (position) |
| ^FD text ^FS | Field data (text content) |
| ^CF font,height | Set default font and size |
| ^ADN height,width | Alphanumeric down font |
| ^BY width | Barcode field default |
| ^BCN height,Y,N,N | Code 128 barcode |
| ^BQN,2,10 | QR Code barcode |
| ^GFA,data | Graphic field (logo) |
| ^LL length | Label length |
| ^PQ copies | Print quantity |
| ^PR speed | Print rate (ips) |
Tips for ZPL web app integration
- →Use Labelary to preview ZPL — labelary.com/viewer.html renders ZPL in the browser. Generate your ZPL server-side and preview before sending to the printer.
- →Store printer names server-side — Don't hardcode printer names in your frontend. Store them per-device in your backend and fetch them via the API on first run.
- →Handle PrintBridge offline gracefully — Catch
fetch()network errors. If PrintBridge isn't running, you'll get a connection refused error — show the user a "PrintBridge not running" prompt. - →Test in the live lab first — test.printbridge.app lets you test ZPL against a real Zebra printer in a browser tab before touching your codebase.
Start printing ZPL from your web app today
Download PrintBridge and send your first ZPL label in under 60 seconds. No Java, no plugin, no drama.