---
name: zplcloud-api-print
description: Render, convert and print ZPL labels through the zplCloud REST API (api.zplcloud.com) - saved designs to ZPL/PDF/PNG, 21 ZPL tools (ZPL to PDF/PNG, PDF/image/SVG/EPL2 to ZPL, linter, barcode/QR/GS1), label enrichment, batch jobs that print to Weblink/remote/virtual printers, printer and agent lists, label quota and webhook callbacks. Use when the user wants to add ZPL label rendering or printing via api.zplcloud.com to an app, script or agent.
---

# zplCloud - ZPL labels via API

Use this skill to render, convert and print Zebra (ZPL) labels through the zplCloud API.

## Base

- API domain: `https://api.zplcloud.com` - paths start with `/v1` (no `/api` prefix), responses are always English.
- Interactive reference (OpenAPI/Scalar, grouped by topic): `https://api.zplcloud.com/scalar`
- Docs: `https://zplcloud.com/en/docs/api`, `/en/docs/api-rendering`, `/en/docs/api-tools`, `/en/docs/api-account`
- Auth: API key via header `X-API-Key: <KEY>` or HTTP Basic Auth `curl -u <KEY>:`
- API keys are created in the zplCloud platform (API tab). Sandbox keys only work for `/v1/virtual-printers` (403 elsewhere).
- POST bodies are JSON (camelCase). Binary input (PDF, images) as base64 strings (data URIs accepted).
- Every response of a `/v1` endpoint has `X-ZplCloud-Request-Id`. Errors are JSON `{ "error": "...", "hint": "..." }` - follow the `hint`; the endpoints with webhook support also put the `requestId` into the error body.

## Endpoints (v1)

### Rendering a saved design
- `POST /v1/zpl/render/design/{designId}` → `application/x-zpl`, one `^XA … ^XZ` block per record (max 500 records, body 1 MB; quota-checked, 1 render per record).
- `POST /v1/pdf/render/design/{designId}` → `application/pdf`, one page per record (max 2,000 records, 5 MB).
- `POST /v1/png/render/design/{designId}?record=0` → `image/png`, one label (`record` = index, default 0).
- Body: JSON array of record objects (optional for PDF/PNG). Field names must match the design's variables.

### ZPL tools - `POST /v1/tools/{slug}` (JSON body)

| slug | main body fields | response |
|---|---|---|
| `zpl-to-pdf` / `zpl-to-png` / `zpl-to-jpg` | `zpl`, `dpi` (203/300/600), `widthMm?`, `heightMm?` (default ^PW/^LL, else 75 × 50) | PDF (all labels) / PNG / JPG (first label) |
| `zpl-to-svg` | `zpl` | `image/svg+xml` |
| `zpl-linter` | `zpl`, `dpi?`, `widthMm?`, `heightMm?`, `profile?` (`amazon-fba`) | JSON `{ clean, errorCount, warningCount, infoCount, issues[] }` |
| `zpl-analyser` | `zpl`, `dpi?`, `widthMm?`, `heightMm?`, `networkMbit?` | JSON analysis (commands, label size, time predictions) |
| `html-to-zpl` | `pngBase64` (HTML rendered as PNG), `dpi`, `widthMm`, `heightMm` | JSON `{ zpl, zplBytes }` |
| `pdf-to-zpl` | `pdfBase64`, `pages?` (`"1-3,5"`, `"all"`, max 100), `rotate`, `scale`, `darkness`, `dpi`, `speed` | JSON `{ zpl, pageCount }` |
| `image-to-zpl` | `imageBase64` (PNG/JPG/GIF/BMP), `blackness?`, `rotate?`, `compress?`, `dpi?`, `fileName?` | JSON `{ zpl, grf, grfName }` |
| `svg-to-zpl` | `svg`, `dpi`, `sizeMm` | JSON `{ zpl }` |
| `epl2-to-zpl` / `dpl-to-zpl` / `tspl-to-zpl` | `code` | JSON `{ zpl, labels }` |
| `zpl-to-tspl` / `zpl-to-epl2` / `zpl-to-dpl` / `zpl-to-sbpl` / `zpl-to-cpcl` / `zpl-to-escpos` / `zpl-to-brother` / `zpl-to-pcl` / `zpl-to-easyplug` / `zpl-to-tpcl` / `zpl-to-jscript` | `zpl`, `dpi` (per language), `widthMm?`, `heightMm?`, `compress?`, `hex?` (SBPL/CPCL), `cut?` (ESC/POS/Brother), `paper?` (PCL), `model?`/`media?` (Brother) | raw print job (`application/octet-stream`) for TCP 9100 - every label rendered dot for dot as a bitmap in the printer language; send it byte-exact (e.g. `POST /v1/remote-printers/{id}/raw` or `zplcloud send --file label.tspl`) |
| `barcode-to-zpl` | `symbology` (Code128 default, Code39, Ean13, UpcA, Ean128, DataMatrix, QRCode, Pdf417 …), `data`, `dpi?` | JSON `{ zpl, widthMm, heightMm }` |
| `qr-code-to-zpl` | `type` (url, text, wifi, email, phone, vcard …), `fields {…}`, `sizeMm?`, `errorCorrection?` | JSON `{ zpl, data }` |
| `gs1-ai-128-to-zpl` / `gs1-ai-datamatrix-to-zpl` / `gs1-ai-qr-code-to-zpl` / `gs1-ai-databar-to-zpl` | `ai` (bracket notation, e.g. `(01)04006381333931(10)L-2026-0417`), `dpi?` | JSON `{ zpl, elements[] }` or 400 `{ error, issues[] }` |
| `check-barcode` | `imageBase64` (photo/scan) or `data` | JSON `{ count, valid, codes[] }` |
| `dpi-calculator` | `dpi?`, `lengthMm?`, `dots?`, `fontPt?`, `moduleMm?` … | JSON conversions |

### Label enrichment - `POST /v1/enrich/zpl` (Pro plan only: 403 on other plans; 404 if not enabled)
Adds own fields (SKU, bin, PO …) to every label of a carrier/Amazon ZPL without touching the original commands; record n → label n.
Body `{ "zpl", "dpi", "fields": [{ "column" or "text", "x", "y", "w", "h" (mm), "fontPt", "align": "L|C|R" }], "records": [{...}], "clearZone", "zoneX", "zoneY", "zoneW", "zoneH" }`
→ JSON `{ zpl, labels }`, or raw ZPL with `?format=zpl`. Limits: 4 M characters, 50 fields, 10,000 records.

### Batch jobs (Pro plan only)
- Other plans get `403 { error: "Batch jobs require the Pro plan.", plan, hint, requestId }` (503 if the plan cannot be read); `GET /v1/batch/jobs/…` is not plan-checked. Without Pro, render up to 500 records per call via `/v1/zpl/render/design` or 2,000 via `/v1/pdf/render/design`.
- `POST /v1/batch/design/{designId}` body `{ "records": [...], "output": "zpl|pdf|none", "printer": "<target>", "pickList": {...}, "async": false }`
  - `records` 1-10,000 (PDF ≤ 5,000); `output` default `zpl`; `none` needs `printer` or `pickList`.
  - `printer`: a `target` from the printer lists (`wl:{id}` or Weblink serial, `rp:{id}`, `vp:{id}`) - printed in blocks of 100 labels.
  - `pickList`: `{ title, subtitle, columns[], groupBy, sortBy, checkbox, paper: "A4|Letter" }` → pick list PDF from the same records.
  - Sync → 200 `{ jobId, status: succeeded|print_failed, labels, output { format, contentType, encoding, data }, pickList, print { sentChunks, sentLabels, error } }`.
  - `async: true` → 202 `{ jobId, status: "queued", statusUrl }`; the job runs in the background (combine with `webhookId`).
- `GET /v1/batch/jobs/{jobId}` → `{ status: queued|running|succeeded|print_failed|failed, durationMs, error, print, outputUrl, pickListUrl, expiresUtc }`.
- `GET /v1/batch/jobs/{jobId}/output` → the ZPL/PDF (≤ 20 MB) · `GET /v1/batch/jobs/{jobId}/picklist` → pick list PDF.
- Jobs are kept for 1 hour in the memory of the server that accepted them - fetch results promptly.

### Printers, agents, account (GET)
- `GET /v1/printers` - printers bound to this API key (Weblink printers with the key's certificate + remote printers of agents using the key).
- `GET /v1/company/printers` - all printers of the company: Weblink, remote (`agentOnline`), virtual.
  Each entry: `target` (pass as batch `printer`), `type` (weblink/remote/virtual), `id`, `name`, `online`, …
- `GET /v1/agents` / `GET /v1/company/agents` - zplCloud CLI agents (name, online, version, os, hostname, lastSeenUtc, printers).
- `GET /v1/quota` - `{ plan, labels { limit, used, remaining, percentUsed }, period { start, end, resetsUtc } }`.
- `GET /v1/webhooks` - `{ count, webhooks: [{ id, url, status, events }] }` (no secrets) - the `id` is your `webhookId`.

### Printing, sandbox, fonts, profiles, Weblink, DHL (no webhook parameters)
These endpoints do not take `webhookId`. Responses are JSON unless noted; IDs come from the matching list call.

- **Virtual printers** - the only endpoints for sandbox keys; no renders counted, output is a watermarked PNG.
  - `GET /v1/virtual-printers` → `{ ok, printers: [{ id, name, widthMm, heightMm, dpi }] }`
  - `POST /v1/virtual-printers/{id}/print` body `{ "zpl": "^XA^FO50,50^A0N,40,40^FDHello^FS^XZ", "jobName": "optional" }` → `{ ok, id, labels, width, height, error, image }`
  - `GET /v1/virtual-printers/prints/{id}` → `{ ok, print: { id, printerId, jobName, labels, error, createdUtc, … } }` · `GET /v1/virtual-printers/prints/{id}.png` → `image/png`
- **Remote printers** (LAN/USB printers behind a zplCloud CLI agent; `409` when the agent is offline):
  - `GET /v1/remote-printers` → `{ ok, printers: [{ id, name, host, port, usb, agent, agentOnline, agentVersion }] }`
  - `POST /v1/remote-printers/{id}/send` body `{ "zpl": "~HS" }` → `{ ok, responseText, error }` (ZPL or SGD; printer answers in `responseText`)
  - `POST /v1/remote-printers/{id}/file` body `{ "base64": "<complete ~DY payload: command header + raw file bytes>", "fileName": "ZPLCLOUD.TTF" }` → `{ ok, error, bytes, fileName }` (TCP printers only)
  - `POST /v1/remote-printers/{id}/raw` - print job in another printer language, byte-exact (no line terminator): body `{ "base64": "<finished job, e.g. from /v1/tools/zpl-to-tspl>", "fileName": "label.tspl" }` or `{ "zpl": "^XA…^XZ", "language": "tspl", "dpi": 203, "compress": true }` (converts first; same fields and render counting as the zpl-to-… tools) → `{ ok, error, bytes, language, labels }`. `language`: tspl, epl2, dpl, sbpl, cpcl, escpos, brother, pcl, easyplug, tpcl, jscript. Agent v1.2.22+ for TCP printers, v1.2.26+ for USB.
- **Watch folders** of an online agent (`409` when the agent is offline):
  - `GET /v1/agents/folders` → `{ ok, folders: [{ agent, folder }] }`
  - `POST /v1/print/folder` body `{ "agent": "<agent>", "folder": "<folder>", "zpl": "^XA…^XZ" }` → `{ ok, queued, path, agent, folder, error }` - the agent prints oldest-first.
    For other printer languages add `"language": "tspl"` (+ tool options) or send `"base64"` + `"fileName": "label.tspl"` instead of `zpl`; the file keeps the job extension, which the folder's `pattern` must include (agent v1.2.26+). Response adds `bytes, language, labels`.
- **Fonts**: `GET /v1/fonts` → JSON array of font names · `GET /v1/fonts/platform/{name}` → font file, e.g. `ZPLCLOUD.TTF` (install it with `/file` above).
- **Printer profiles and ZPL cookbook**: `GET /v1/printer-profiles` · `GET /v1/printer-profiles/{id}` (commands in send order) · `GET /v1/zpl-cookbook` · `GET /v1/zpl-cookbook/{id}`.
- **Weblink certificates**: `POST /v1/weblink/certificates/generate` body `{ "printerHostname": "zd421-lager", "weblinkEndpoint": "https://…", "printerSerial": "optional", "printerIp": "optional" }` → `{ domain, printer, weblinkUrl, ca, cert, key }` (NRD file contents; `409` if the hostname belongs to another account) · `GET /v1/weblink/certificates/domains/{domain}/printers/{printer}/{file}` with `file` = `WEBLINK2_CA.NRD`, `WEBLINK2_CERT.NRD` or `WEBLINK2_KEY.NRD`.
- **DHL Internetmarke** (uses the key owner's DHL integration):
  - `GET /v1/integrations/dhl/status` → `{ ok, walletBalanceCents, productId, products: [{ id, name, priceCents }] }`
  - `POST /v1/integrations/dhl/stamp` body `{ "receiver": { "name": "Erika Muster", "addressLine1": "Hauptstr. 1", "postalCode": "10115", "city": "Berlin", "country": "DEU" }, "senderId": "optional", "format": "pdf" }` (`format` `pdf` or `zpl`) → `{ ok, format, pdfBase64 | zpl, widthMm, heightMm, senderId, voucherId, walletBalanceCents, shopOrderId }` - **a real, paid purchase**; the API key must be enabled for the plugin `dhl` (403 otherwise).
  - `GET /v1/integrations/dhl/stamps` → `{ ok, transactions: [...] }` (last 200, newest first)

## Webhook callback (PDF/PNG render, tools, enrichment, batch, printers, agents, quota, webhooks)

- Query `webhookId=<id>` (from `GET /v1/webhooks`; must be active and yours, else 400 before any work) and optional
  `webhookResult=true` (include the result; requires `webhookId`).
- Called when the request has finished - on success AND failure. Response header `X-ZplCloud-Webhook: queued|failed`.
- Delivery: POST JSON with headers `X-ZplCloud-Event`, `X-ZplCloud-Delivery`, `X-ZplCloud-Timestamp`, `X-ZplCloud-Signature`
  (lowercase hex HMAC-SHA256 of the raw body with the webhook secret). Answer 2xx within 10 s; retries after 1, 5, 30 min.
- Payload: `{ event, timestamp, requestId, operation, status: succeeded|failed, httpStatus, durationMs, renders, summary, error, resultIncluded, result { contentType, encoding: base64|utf-8|json, size, data } }` - results over 10 MB are left out (`resultOmitted`).
- Events: `api.design.pdf`, `api.design.png`, `api.tool.completed`, `api.enrich.completed`, `api.batch.completed`, `api.batch.status`,
  `api.batch.output`, `api.batch.picklist`, `api.printers.listed`, `api.company.printers.listed`, `api.agents.listed`,
  `api.company.agents.listed`, `api.quota.checked`, `api.webhooks.listed`.

```python
import hashlib, hmac

def signature_ok(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature.lower())
```

## Quota & limits

- Labels are counted per account (API key owner) per calendar month (UTC) - same counter as the platform (plan limit + top-ups).
- `GET /v1/quota` and the 429 check see new API renders immediately (buffered counter, written to the database every 5 s).
- Pro plan required for batch jobs and `/v1/enrich/zpl` (`403` with `plan` otherwise) - check `GET /v1/quota` → `plan == "pro"` first. Everything else works on every plan.
- Quota-checked, `429 { error, hint, limit, used, requested }` when used up: ZPL/PDF/PNG design renders (1 per record/page/label) and
  batch jobs (1 per record, checked for the whole job before it starts).
- Counted but never blocked, no rate limit: `/v1/tools/*` (1 per call; `zpl-to-pdf` 1 per label, `pdf-to-zpl` 1 per page)
  and `/v1/enrich/zpl` (1 per label).
- **Before a large batch, call `GET /v1/quota` and compare `labels.remaining` with the record count.** On 429, split the
  run or tell the user to upgrade / buy a render top-up.
- Input limits: ZPL/code/SVG 2 M characters, images 20 MB, PDFs 50 MB / 100 pages, labels ≤ 400 mm.
- The design must belong to the API key owner's email or company (scope check, else 404).

## Design ID

A design is addressed as `<name>.<id>` - the design tag (lowercase a-z, 0-9, `_`, `-`)
and its numeric ID, separated by a dot. Example: `gtin-label.42`.
Save the design in the zplCloud designer; the save response/URL shows the design ID.

## Examples

Render ZPL for a saved design (response: one `^XA … ^XZ` block per record):

```bash
curl -X POST "https://api.zplcloud.com/v1/zpl/render/design/gtin-label.42" \
  -H "X-API-Key: $ZPLCLOUD_API_KEY" -H "Content-Type: application/json" \
  -d '[{"gtin":"4006381333930","produkt":"Demo","charge":"LOT-42"}]'
```

PDF with a webhook callback (without the result):

```bash
curl -X POST "https://api.zplcloud.com/v1/pdf/render/design/gtin-label.42?webhookId=12" \
  -H "X-API-Key: $ZPLCLOUD_API_KEY" -H "Content-Type: application/json" \
  -d '[{"gtin":"4006381333930"},{"gtin":"4006381333947"}]' -o labels.pdf
```

ZPL tool (ZPL → PNG):

```bash
curl -X POST https://api.zplcloud.com/v1/tools/zpl-to-png \
  -H "X-API-Key: $ZPLCLOUD_API_KEY" -H "Content-Type: application/json" \
  -d '{"zpl":"^XA^FO50,50^A0N,40,40^FDHello^FS^XZ","dpi":203}' -o label.png
```

Batch job with plan and quota check and printer target (Python, Pro plan):

```python
import time
import requests

API = "https://api.zplcloud.com"
H = {"X-API-Key": "<your-api-key>"}
DESIGN = "gtin-label.42"
records = [{"gtin": "4006381333930", "produkt": "Demo", "charge": "LOT-42"}]

quota = requests.get(f"{API}/v1/quota", headers=H).json()
if quota["plan"] != "pro":
    raise SystemExit("Batch jobs require the Pro plan - use /v1/zpl/render/design (max 500 records per call) instead.")
if quota["labels"]["remaining"] < len(records):
    raise SystemExit(f"Not enough labels left this month: {quota['labels']}")

printers = requests.get(f"{API}/v1/company/printers", headers=H).json()["printers"]
target = next((p["target"] for p in printers if p["online"]), None)

r = requests.post(f"{API}/v1/batch/design/{DESIGN}", headers=H,
                  json={"records": records, "output": "zpl", "printer": target, "async": True})
r.raise_for_status()
job_id = r.json()["jobId"]

while (job := requests.get(f"{API}/v1/batch/jobs/{job_id}", headers=H).json())["status"] in ("queued", "running"):
    time.sleep(2)
print(job["status"], job["print"], job["error"])
zpl = requests.get(f"{API}/v1/batch/jobs/{job_id}/output", headers=H).text
```

Print returned ZPL yourself on TCP port 9100:

```python
import socket

def print_zpl(zpl: str, host: str, port: int = 9100):
    with socket.create_connection((host, port), timeout=10) as s:
        s.sendall(zpl.encode("utf-8"))
```
