# OpenZPL API

Render ZPL (Zebra Programming Language) labels to PNG or PDF over HTTP. Compatible with Labelary's v1 API: same paths, headers and `X-Total-Count`; change the host.

Base URL: `https://api.openzpl.com`. OpenAPI: https://openzpl.com/openapi.json. HTML docs: https://openzpl.com/docs/api/.

## Render a label

```
POST https://api.openzpl.com/v1/printers/{dpmm}/labels/{width}x{height}/{index}/
POST https://api.openzpl.com/v1/printers/{dpmm}/labels/{width}x{height}/      # PDF with every label
GET  https://api.openzpl.com/v1/printers/{dpmm}/labels/{width}x{height}/{index}/{url-encoded zpl}
```

- `dpmm`: print density, one of `6dpmm` (152 dpi), `8dpmm` (203 dpi), `12dpmm` (300 dpi), `24dpmm` (600 dpi).
- `width`, `height`: label size in inches, e.g. `4x6` or `2.25x1.25`, up to 15.
- `index`: which label to render, from 0. Leave it out with `Accept: application/pdf` to get every label, one per page.
- Body: the raw ZPL, or a multipart form field named `file`.

## Headers

| Header | Values | Default |
|---|---|---|
| `Accept` | `image/png`, `application/pdf` | PNG |
| `X-Quality` | `Grayscale` (8-bit, smooth edges) or `Bitonal` (1-bit, smaller) | Grayscale |
| `X-Rotation` | `0`, `90`, `180`, `270` (clockwise) | 0 |
| `X-Language` | `zpl`, `epl`, `tspl`, `auto`: the body's printer language. Without it, a body with no `^` or `~` is read as EPL or TSPL when it looks like one | zpl |
| `X-API-Key` | your workspace key (optional) | none |

Responses carry `X-Total-Count`: the labels the ZPL produces, `^PQ` copies included.

## Limits

- Without a key: 3 requests per second and 3 labels per UTC day, per IP address. With a free key from the dashboard: 20 labels per UTC day.
- Labels, not requests, count against the allowance (per UTC day on Free, per UTC calendar month on paid plans): every label rendered, `^PQ` copies and PDF pages included. A 10-label PDF is 1 request and 10 labels.
- Every request: up to 50 labels, a 1 MB body, 15 inches per side, and 10 seconds of render time.
- Paid plans (Pro $29/month: 10 requests/s and 100,000 labels/month, Business $99/month: 20 requests/s and 1,000,000 labels/month): subscriptions, monthly or yearly, from the dashboard's Billing page (card, through Stripe), and you can cancel at any time. The workspace's API keys move to the plan's limits as soon as the payment is approved.
- Over a limit the API answers `429` with `Retry-After` (seconds) and a body that names the limit, e.g. `ERROR: Daily limit reached: 20 labels per UTC day on this plan (^PQ copies and PDF pages count)`.

## Errors

Errors are `text/plain` and start with `ERROR: `.

| Status | Meaning |
|---|---|
| 400 | Invalid parameter, label too large, index out of range, or no labels in the ZPL |
| 401 | Unknown or revoked API key |
| 406 | `Accept` asks for a format that isn't available |
| 413 | Body over 1 MB, or more than 50 labels |
| 429 | Over a limit; wait `Retry-After` seconds |
| 503 | Render took over 10 s, or the service is at capacity; retry |

## Examples

Each one sends label.zpl, saves label.pdf and prints the API's error text on failure.

### cURL

```sh
curl https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/ \
  -H "Accept: application/pdf" \
  --data-binary @label.zpl --fail-with-body -o label.pdf
```

### Python

```python
import requests  # pip install requests

with open("label.zpl", "rb") as f:
    res = requests.post(
        "https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/",
        data=f.read(),
        headers={"Accept": "application/pdf"},
        timeout=30,
    )
if not res.ok:
    raise SystemExit(res.text)
with open("label.pdf", "wb") as f:
    f.write(res.content)
```

### Node.js

```js
// Node.js 18+; save as label.mjs
import { readFile, writeFile } from "node:fs/promises";

const res = await fetch("https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/", {
  method: "POST",
  headers: { Accept: "application/pdf" },
  body: await readFile("label.zpl"),
});
if (!res.ok) throw new Error(await res.text());
await writeFile("label.pdf", Buffer.from(await res.arrayBuffer()));
```

### PHP

```php
<?php
$ch = curl_init("https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => file_get_contents("label.zpl"),
    CURLOPT_HTTPHEADER => ["Accept: application/pdf"],
    CURLOPT_RETURNTRANSFER => true,
]);
$pdf = curl_exec($ch);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) !== 200) {
    exit($pdf);
}
file_put_contents("label.pdf", $pdf);
```

### C#

```csharp
using var http = new HttpClient();
var req = new HttpRequestMessage(HttpMethod.Post,
    "https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/")
{
    Content = new StringContent(File.ReadAllText("label.zpl")),
};
req.Headers.Add("Accept", "application/pdf");

var res = await http.SendAsync(req);
if (!res.IsSuccessStatusCode)
    throw new Exception(await res.Content.ReadAsStringAsync());
File.WriteAllBytes("label.pdf", await res.Content.ReadAsByteArrayAsync());
```

### Java

```java
// Java 11+
import java.net.URI;
import java.net.http.*;
import java.nio.file.Path;

public class Label {
    public static void main(String[] args) throws Exception {
        var req = HttpRequest.newBuilder(
                URI.create("https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/"))
            .header("Accept", "application/pdf")
            .POST(HttpRequest.BodyPublishers.ofFile(Path.of("label.zpl")))
            .build();
        var res = HttpClient.newHttpClient()
            .send(req, HttpResponse.BodyHandlers.ofByteArray());
        if (res.statusCode() != 200)
            throw new RuntimeException(new String(res.body()));
        java.nio.file.Files.write(Path.of("label.pdf"), res.body());
    }
}
```

### Go

```go
package main

import (
	"io"
	"log"
	"net/http"
	"os"
)

func main() {
	zpl, err := os.Open("label.zpl")
	if err != nil {
		log.Fatal(err)
	}
	req, _ := http.NewRequest("POST", "https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/", zpl)
	req.Header.Set("Accept", "application/pdf")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	if res.StatusCode != 200 {
		log.Fatal(string(body))
	}
	os.WriteFile("label.pdf", body, 0o644)
}
```

### Ruby

```ruby
require "net/http"

uri = URI("https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/")
res = Net::HTTP.post(uri, File.binread("label.zpl"), "Accept" => "application/pdf")
abort res.body unless res.is_a?(Net::HTTPSuccess)
File.binwrite("label.pdf", res.body)
```

### PowerShell

```powershell
Invoke-WebRequest -Method Post `
  -Uri "https://api.openzpl.com/v1/printers/8dpmm/labels/4x6/" `
  -Headers @{ Accept = "application/pdf" } `
  -InFile "label.zpl" -OutFile "label.pdf"
```

Guides per language: https://openzpl.com/guides/zpl-to-pdf-python/, https://openzpl.com/guides/zpl-to-pdf-nodejs/, https://openzpl.com/guides/zpl-to-pdf-php/, https://openzpl.com/guides/zpl-to-pdf-csharp/. Moving from Labelary: https://openzpl.com/migrate/labelary/.
