Migrate from the Labelary API to OpenZPL
To move a Labelary v1 client to OpenZPL, change the host from api.labelary.com to api.openzpl.com and keep the paths, headers and request bodies. Then check the few status codes and limits that differ, listed below with the tests we ran.
Mis à jour le 2026-10-20 · Renders made with the OpenZPL engine
1. Change the host
The render path is the same, so only the base URL changes:
- https://api.labelary.com/v1/printers/8dpmm/labels/4x6/0/ + https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/0/
curl https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/0/ \ --data-binary @label.zpl > label.png curl https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/ \ -H "Accept: application/pdf" --data-binary @labels.zpl > labels.pdf
In Python, keep the base URL in one setting so you can switch back:
import os
import requests
BASE = os.environ.get("ZPL_API", "https://api.openzpl.com") # was https://api.labelary.com
def render_pdf(zpl: str, size: str = "4x6", dpmm: int = 8) -> bytes:
res = requests.post(
f"{BASE}/v1/printers/{dpmm}dpmm/labels/{size}/",
data=zpl.encode("utf-8"),
headers={"Accept": "application/pdf"},
timeout=15,
)
res.raise_for_status()
return res.content
2. Keep your headers
Accept (image/png or application/pdf), X-Quality and X-Rotation work as they do on Labelary, and the response carries X-Total-Count. X-Page-Size, X-Page-Layout, X-Label-Border and X-Linter aren't supported: OpenZPL returns one label per page at the label's own size. Other Accept values such as application/epl get 406.
3. Add a key if you need more than the free plan
The free plan needs no key and is limited per IP. With a key from your dashboard, send it in X-API-Key and your workspace's limits apply. A Labelary private hostname has no equivalent: everyone uses api.openzpl.com.
curl https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/0/ \ -H "X-API-Key: oz_live_…" --data-binary @label.zpl > label.png
4. Recount your limits
Labelary's daily quota counts requests; OpenZPL's counts labels. If your requests return multi-label PDFs, divide your daily requests by the labels per request to see what fits in 5,000 labels, or in your plan on the pricing page.
5. Handle 429 and 503
On 429, wait the seconds in Retry-After: 1 for the per-second limit, until midnight UTC for the daily one. On 503 (a label over 10 seconds of render time, or the service at capacity), retry once with a short delay.
import time
def post_with_retry(url, body, headers, attempts=3):
for _ in range(attempts):
res = requests.post(url, data=body, headers=headers, timeout=15)
if res.status_code == 429:
wait = int(res.headers.get("Retry-After", "1"))
if wait > 60:
res.raise_for_status() # daily allowance used up
time.sleep(wait)
continue
if res.status_code == 503:
time.sleep(1)
continue
res.raise_for_status()
return res
res.raise_for_status()
What we tested
Tested 2026-10-20 with synthetic labels, the same request sent to api.labelary.com (free tier) and to the OpenZPL engine. We compared status codes, headers, page counts and image size, not pixels.
| Demande | OpenZPL | Labelary |
|---|---|---|
| PNG, one label, 4 × 6 in at 8 dpmm | 200, 812 × 1218 px | 200, 812 × 1218 px |
| PNG at 12 dpmm | 200, 1216 × 1824 px | 200, 1216 × 1824 px |
| PDF, one label | 200, 1 page | 200, 1 page |
PDF, no index, 2 formats with ^PQ2 on the second | 200, 3 pages, X-Total-Count: 3 | 200, 3 pages, X-Total-Count: 3 |
| PNG, index 2 of 3 | 200 | 200 |
| Index 5 of 1 | 404, X-Total-Count: 1 | 404, X-Total-Count: 1 |
^XA^XZ with no fields | 404, X-Total-Count: 0 | 404, X-Total-Count: 0 |
Accept: application/x-nope | 406 | 406 |
Accept: application/epl | 406 | 200, EPL |
X-Rotation: 90 | 200, 1218 × 812 px | 200, 1218 × 812 px |
X-Quality: Bitonal | 200, 1-bit PNG | 200, 1-bit PNG |
^GFA image, 32 × 32 dots | 200, same position | 200, same position |
^CI28 with accented text | 200 | 200 |
| Label 16 in wide | 400 | 400 |
Differences to watch
- Output formats other than PNG and PDF answer
406; keep those calls on Labelary or drop them. - Pixels can differ where fonts differ. Compare images visually or by the position of key fields, not byte by byte.
- Error texts differ. Both start with
ERROR:; don't match on the rest. - Commands the engine doesn't support yet print differently or not at all; see compatibility.
Rolling back
Because nothing else changes, rolling back is setting the base URL to api.labelary.com again. Keep it in configuration, not in code, and switch a share of traffic first: render a day of real labels on both, compare a sample, then move the rest.
Questions
Do I have to change my ZPL?
No. The ZPL goes in the body or the URL exactly as before.
Does OpenZPL keep my labels?
No. The API renders the label and returns it; the ZPL isn't stored. API key requests are counted for the usage dashboard without their content.