# Migrate from the Labelary API to OpenZPL

Source: https://openzpl.com/migrate/labelary/ (updated 2026-10-20)

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.

OpenZPL is an independent project. It is not affiliated with, endorsed by or connected to Labelary or Zebra Technologies; the names are used only to identify the services compared.

## 1. Change the host

The render path is the same, so only the base URL changes:

```diff
- https://api.labelary.com/v1/printers/8dpmm/labels/4x6/0/
+ https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/0/
```

```bash
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:

```python
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.

```bash
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.

```python
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.

| Request | 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.

**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.
