Blog/Developer Guide

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

CommandPurpose
^XA / ^XZStart / end label
^FO x,yField origin (position)
^FD text ^FSField data (text content)
^CF font,heightSet default font and size
^ADN height,widthAlphanumeric down font
^BY widthBarcode field default
^BCN height,Y,N,NCode 128 barcode
^BQN,2,10QR Code barcode
^GFA,dataGraphic field (logo)
^LL lengthLabel length
^PQ copiesPrint quantity
^PR speedPrint rate (ips)

Tips for ZPL web app integration

  • Use Labelary to preview ZPLlabelary.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 firsttest.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.