How to test ZPL without a Zebra printer
Zebra printers have no screen, no preview, and no test mode. The traditional way to test a label is to print it, walk to the printer, and squint — which is why label bugs ship to production more than any other kind of output bug. Two better ways:
1. Render the ZPL to an image
Paste ZPL into the free viewer and get a pixel-true PNG at exact label size and density — what you see is what the printhead does. Or from code:
curl https://api.stripyhorse.io/v1/render.png \
-H "X-Api-Key: sh_live_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"zpl":"^XA^FO50,50^A0N,45,45^FDHello^FS^XZ","preset":"4x6"}' -o label.png
Rendering answers "what does this ZPL look like?" — but it doesn't test the thing that actually breaks in production: your system's printing pipeline.
2. Point your code at a virtual Zebra printer
Real label bugs live in the integration: the template variables your WMS fills in, the
bytes your print service actually sends, the ~HS status polling, the reconnect logic
when the printer jams. To test those you need something that behaves like a printer,
not an image converter.
A virtual Zebra printer is a hosted endpoint that speaks the printer's own protocols:
- Raw TCP port 9100 — the same socket every warehouse system already prints to. Point your existing code at a different host:port; zero code changes.
~HShost status — three STX/ETX lines with fault flags, paper-out, buffer counts, exactly like firmware, so your status-polling code is exercised for real.- SGD queries (
! U1 getvar "device.friendly_name") for discovery flows. - Every job it receives is rendered and stored — the raw ZPL your system sent and the PNG of what it would print — queryable over REST.
Try it right now against the public demo printer, no signup:
printf '^XA^FO50,50^A0N,45,45^FDhello from my terminal^FS^XZ' | nc -q1 print.stripyhorse.io 9100
Testing label printing in CI
This is where virtual printers earn their keep. The pattern:
- Your pipeline creates a printer via the API and gets a TCP address.
- Your fulfillment code runs against that address, exactly as in production.
- The test asserts on captured jobs — and fails the build if a template change broke a label.
sim = SimulatorApi(client)
printer = sim.create_printer(CreatePrinterInputBody(name="ci-run", preset="4x6"))
run_fulfillment_against(f"{printer.tcp.host}:{printer.tcp.port}")
jobs = sim.list_jobs(printer.id)
assert len(jobs.jobs) == 1
assert jobs.jobs[0].status == "rendered"
Golden-diff the rendered PNGs and a one-pixel barcode regression fails in CI instead of on the dock. You can also inject faults — paper out, head open, paused — and watch jobs hold in the receive buffer, then flush on clear: the failure modes you can never trigger politely on production hardware.
FAQ
Is there a free ZPL viewer online?
Yes — stripyhorse.io/tools/zpl-viewer renders multi-label streams at 6–24 dpmm with rotation, free, no signup. Labelary is the long-standing alternative; ours adds exact label-size presets and the virtual-printer integration.
Can I run a Zebra emulator locally?
Zebra's own ZebraDesigner has no emulator, and hobby emulators cover fragments of ZPL. The practical options are a rendering engine (this site's is open-source-derived and cross-validated on thousands of carrier labels) or a hosted virtual printer with the TCP/status behavior included.
Does the virtual printer support ^XA…^XZ framing quirks?
Yes — partial frames buffer until complete, multiple labels per connection work, and leftovers flush on disconnect, matching real firmware behavior.
Published 2026-08-24 · Ben Faerber — software engineer, five years building warehouse fulfillment and label-printing systems; maintainer of pdf-to-zpl (26,000+ installs powering production label generation).