Reference

ZPL fundamentals: what your Zebra printer actually speaks

← Back to Blog

ZPL fundamentals: what your Zebra printer actually speaks

Published: · 10 min read · Reference

Every label a Zebra printer produces is the result of a tiny program in a language called ZPL, Zebra Programming Language. The printer doesn't take PDFs, it doesn't take Word documents, it doesn't even take generic raster images directly. It takes ZPL commands and obeys them, one by one, until it hits a marker that says "label done", at which point the print head fires.

You don't need to learn ZPL to use a Zebra printer. mylabelmaker writes it for you. But understanding the basics is genuinely useful for two reasons:

  1. It demystifies what's happening when something goes wrong, blurry barcodes, cut-off text, missing fields, are usually traceable to one specific ZPL command.
  2. If you build any backend automation (a print queue, an ERP integration, a fulfillment script), you'll eventually copy ZPL out of an editor like mylabelmaker and send it programmatically. Knowing what's in that string is useful.

This is a friendly, no-jargon tour. By the end you'll be able to look at any ZPL string and roughly understand what label it produces. The article finishes by showing how mylabelmaker's Copy ZPL works so you can plug it into your own pipeline.

The shape of a ZPL label

The simplest possible ZPL label looks like this:

// Start a label
^XA

// Move to position (50, 50) and print "Hello World"
^FO50,50^A0N,36,36^FDHello World^FS

// End the label, fire the printer
^XZ

Every label, no matter how complex, starts with ^XA and ends with ^XZ. Everything between those two markers is a sequence of commands. Each command starts with a caret (^) followed by two letters that name the command, plus optional parameters separated by commas.

The three commands in the example are the absolute minimum you need:

Dots, DPI, and why coordinates feel weird

All ZPL coordinates and sizes are in dots, the physical resolution of the print head. Three printer DPI tiers cover almost everything:

This is important: the exact same ZPL string produces different physical results on printers at different DPIs. A ^FO100,100 on a 203 DPI printer puts your field at ~0.5 inch, on a 600 DPI printer the same command puts it at ~0.17 inch. ZPL itself has no "inches" concept. Everything is dots.

This is exactly why mylabelmaker tracks the active printer's DPI and re-renders bitmaps at the printer's native resolution, see Why your Zebra prints fuzzy at 300 DPI for the full story.

The handful of commands you'll see most often

Command What it does
^XAStart of label. Always the first thing.
^XZEnd of label. The printer fires when it sees this.
^PW nPrint width in dots. Optional, defaults to the printer's loaded label width.
^LL nLabel length in dots. Same idea for height.
^PQ nPrint Quantity. ^PQ3 means "print 3 copies of this label".
^FO x,yField Origin. Where to place the next thing.
^FD textField Data. The actual content of a text or barcode field.
^FSField Separator. "I'm done defining this field."
^A0N,h,wUse scalable font 0, rotation N (normal), height h dots, width w dots.
^BCCode 128 barcode.
^BQQR code.
^BY w,r,hSet barcode field defaults (module width, ratio, height).
^GFAGraphic Field, ASCII. The most powerful command. Embeds a 1-bit bitmap directly in the ZPL.
^MD nMedia Darkness. 0 to 30. Controls how dark the print is. (Higher = darker.)
^PR nPrint Rate. Inches per second the head moves. Lower = sharper, higher = faster.
~JRReset printer (rarely needed).

You can absolutely write a complete label by hand using just these. A small product label with a name, a price, and a barcode is maybe ten lines.

A real example, line by line

Here's a moderately complex label, the kind mylabelmaker produces when you design a shipping label, then a line-by-line decode:

^XA
^MD12
^PR4
^PW812
^PQ1
^FO0,0
^GFA,121800,121800,150,[hex bitmap data omitted]^FS
^XZ

That ^GFA is mylabelmaker's default approach: render the whole canvas (text, barcodes, QR, shapes, images) into one bitmap at the printer's native DPI, then ship it as a single graphics field. It's the most reliable way to guarantee the printed result matches the on-screen design, because you're skipping the printer's font rendering, barcode generation, and any potential firmware quirks.

Why ^GFA wins. The alternative is to send ZPL that uses the printer's built-in fonts (^A0) and barcode encoders (^BC, ^BQ). That's lighter (smaller ZPL strings), but it ties you to whatever fonts your specific printer happens to have loaded and to its specific barcode implementation. Pre-rendering to ^GFA means the same ZPL produces identical output on any Zebra with the same DPI.

How to get ZPL out of mylabelmaker

You don't have to write any of this. mylabelmaker generates the ZPL for whatever you design. To grab it for your own use:

1

Design your label in the editor

Open app.mylabelmaker.com. Pick a size, drop in text, barcodes, QR codes, images, whatever you need. Use Auto-detect in the printer settings popover (gear icon) so mylabelmaker renders for your printer's actual DPI.

2

Options menu → Copy ZPL

In the top-right of the editor, click Options, then Copy ZPL. The full ^XA … ^XZ string for the current label is copied to your clipboard. Paste it anywhere: a text file, a print server, a curl request to your fulfillment API, a Slack message.

mylabelmaker Save Options ▾ Copy ZPL Export label Import label
Options menu in the editor header. Copy ZPL puts the native ZPL for the current label on your clipboard.

What about variables and batch print?

When you use batch print from a CSV or Google Sheet, mylabelmaker generates a separate ZPL string per row (with the row's values substituted in) and sends them sequentially. The "Copy ZPL" button copies just the current row's ZPL, useful for testing one row in your backend before turning loose on the whole sheet.

Sending ZPL programmatically (from a backend)

Once you have the ZPL string, your backend can send it to a Zebra over a TCP socket (port 9100 on network printers), via the BrowserPrint helper's localhost API, or through any Zebra print server. The simplest example, a network-connected ZD621 at 192.168.1.50:

# Python
import socket
zpl = "^XA^FO50,50^A0N,36,36^FDHello^FS^XZ"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.50", 9100))
s.send(zpl.encode("utf-8"))
s.close()

That's the whole pattern. Whatever language you're in, "open TCP socket to printer's IP on port 9100, write the ZPL bytes, close" is all you need. The printer will fire as soon as it sees ^XZ.

The handful of commands you should know if you skim ZPL

If you find yourself reading ZPL someone else generated (a previous developer, a system you inherited), these are the ones you'll see most often:

Everything else is either a variation of one of these or a specialty command for things like RFID encoding, status queries, or printer configuration. The official ZPL II reference (a free PDF from Zebra) covers everything if you ever need the long form.

The takeaway

ZPL is a small, simple, ASCII command language that's been around since the 1990s and shows no sign of going away. You don't need to write it. mylabelmaker's job is to handle every quirk (DPI mapping, font rasterization, barcode encoding, bitmap packing) and ship a clean ^XA … ^XZ string to your printer. But knowing what's in that string makes you better at debugging, better at integrating with backends, and better at explaining things when someone asks "what's the label software actually doing?"

Skip the ZPL, design visually

Open mylabelmaker, design a label, hit Print. We'll handle the ZPL. You can always copy it out later if you need to.

Open mylabelmaker

Related reading