Rendering, enrichment & batch jobs

Render saved designs as PDF, PNG or ZPL, add your own fields to carrier labels, and run batch jobs of up to 10,000 labels with printing, pick lists and a webhook when the job is done - all on api.zplcloud.com with an API key.

Authentication (X-API-Key header or Basic Auth), the error format, the X-ZplCloud-Request-Id header and the webhook callback are the same for every endpoint and are described on the API overview. Every endpoint on this page except the older ZPL render path accepts the optional query parameters webhookId=<id> and webhookResult=true|false; the event name is listed with each endpoint (payload and signature).

Design IDs

Design endpoints address a saved design as <name>.<id>: the design tag chosen when saving (lowercase letters, digits, _ and -) and the numeric design ID, separated by the last dot - for example shipping-label.42.

  • Where to find it: after Save in the designer, the save dialog and the status bar show the full ID (shipping-label.42). The same ID works for ZPL, PDF, PNG and batch jobs.
  • Scope: the design must belong to the company of the API key owner (keys without a company: the owner's e-mail). Name and number must both match - otherwise 404 Design "shipping-label" not found.
  • Format errors: 400 Invalid design ID - expected format "<name>.<id>" (e.g. my-label.42).
  • Records: a JSON array with one object per label; the keys are the binding names used in the design, e.g. [{"sku":"A-1001","qty":"2"}].

Render a design as PDF

POST https://api.zplcloud.com/v1/pdf/render/design/{designId}

One PDF page per record at the label size and resolution of the design. Without a body (or with []) the design is rendered once with its own content. The quota is checked before rendering.

ParameterTypeNotes
designId (path)string<name>.<id>
bodyarrayOptional. Records, max 2,000 per call and 5 MB. Larger runs: batch jobs.
webhookId, webhookResultqueryOptional webhook callback, event api.design.pdf.
curl -X POST "https://api.zplcloud.com/v1/pdf/render/design/shipping-label.42?webhookId=12" \
  -H "X-API-Key: sk_zplcloud_…" -H "Content-Type: application/json" \
  -d '[{"sku":"A-1001","qty":"2"},{"sku":"A-1002","qty":"5"}]' \
  -o labels.pdf -D -
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename=shipping-label.42.pdf; filename*=UTF-8''shipping-label.42.pdf
X-ZplCloud-Request-Id: 3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13
X-ZplCloud-Webhook: queued
StatusError
400Invalid design ID, invalid JSON, PDF rendering failed: …, invalid webhookId.
401Invalid or missing API key.
404Design not found (or not in the key owner's scope).
413Body over 5 MB, or more than 2,000 records.
429Monthly label quota used up, see quota.

Renders: one per page. Webhook api.design.pdf, summary { design, format: "pdf", pages, bytes, widthMm, heightMm, dpi }; with webhookResult=true the PDF comes along as base64.

Render a design as PNG

POST https://api.zplcloud.com/v1/png/render/design/{designId}?record=0

One label as PNG. The body is the same optional records array; record selects the record by zero-based index.

ParameterTypeDefaultNotes
designId (path)string-<name>.<id>
record (query)int0Index into the records array. Out of range: 400 record must be between 0 and n.
bodyarray-Optional records, max 5 MB.
webhookId, webhookResultquery-Optional, event api.design.png.
curl -X POST "https://api.zplcloud.com/v1/png/render/design/shipping-label.42?record=1" \
  -H "X-API-Key: sk_zplcloud_…" -H "Content-Type: application/json" \
  -d '[{"sku":"A-1001"},{"sku":"A-1002"}]' -o preview.png

Response: 200 image/png, file name <name>.<id>.png. Errors: 400, 401, 404, 413 and 429 as for PDF. Renders: 1. Webhook api.design.png, summary { design, format: "png", record, bytes, widthMm, heightMm, dpi }.

Render a design as ZPL

POST https://api.zplcloud.com/v1/zpl/render/design/{designId}

The existing ZPL path: same design ID and records array, response application/x-zpl; charset=utf-8 with one ^XA…^XZ block per record. Limits: 500 records, 1 MB body. Like PDF and PNG it is checked against the monthly label quota: one render per record (at least 1), 429 { error, hint, limit, used, requested } when the quota is used up. It also shows up in the API statistics and has no webhook callback. For PDF output, printing or more records, use the endpoints above (every plan) or a batch job (Pro plan).

Label enrichment

POST https://api.zplcloud.com/v1/enrich/zpl[?format=zpl]

Adds your own text fields - SKU, bin location, quantity, PO, free text - to every label of ZPL that comes from somewhere else: Amazon, UPS, FedEx, DHL, USPS. The original commands stay byte-identical; for each label the overlay commands are inserted right before its closing ^XZ. The carrier barcode is never touched.

Feature switch

The endpoint exists only while label enrichment is enabled on zplCloud. Otherwise it answers 404 Label enrichment is not available.

Pro plan

Label enrichment calls require the Pro plan of the key owner. Other plans get 403 before the body is read - a requested webhook callback reports the failure. If the plan cannot be read, the answer is 503.

HTTP/1.1 403 Forbidden

{
  "error": "Label enrichment calls require the Pro plan.",
  "plan": "starter",
  "hint": "Upgrade to Pro under Billing in the platform. GET /v1/quota shows your current plan.",
  "requestId": "3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13"
}

Request body

FieldTypeDefaultNotes
zplstringrequiredCarrier or Amazon ZPL with one or more ^XA…^XZ labels, max 4,000,000 characters.
dpiint203Resolution of that ZPL: 203, 300 or 600. Converts millimetres to dots.
fieldsarrayrequired1 to 50 overlay fields (table below).
recordsarray-Objects of column → value, max 10,000. Values must be JSON strings.
clearZoneboolfalsePaint a white box first, so overlays stay readable on printed areas.
zoneX, zoneY, zoneW, zoneHmm0Position and size of the white box; drawn only when zoneW and zoneH are above 0.
Field propertyTypeDefaultNotes
x, ymm-Top-left corner of the text block (^FO).
wmm-Width of the text block (^FB), at least one character height.
hmm-Height. Lines in the block = h ÷ font height, rounded down, at least 1.
fontPtnumber9Font size in points, scalable font ^A0; dots = fontPt × dpi / 72 (min. 6).
alignstringLL, C or R within w.
columnstring-Take the value from the label's record. Key match is case-insensitive, a leading $ is ignored; a missing key prints nothing.
textstring-Static text, used when no column is set or the label has no record.
labelstring-Optional prefix, printed as label: value.

Fields whose value ends up empty are skipped. Text is sent as UTF-8 (^CI28); ^ and ~ in values are hex-escaped with ^FH, so data cannot inject commands.

How records map to labels

  • The ZPL is split into its ^XA…^XZ blocks; record n goes to label n (both counted from 0, in order).
  • Exactly one record: it applies to every label (e.g. the same PO on all parcels of a shipment).
  • No records: only text fields are printed, on every label.
  • Fewer records than labels: the remaining labels get only their static text. Extra records are ignored.
  • Anything between the blocks is kept as is. ZPL without a ^XA…^XZ block comes back unchanged with labels: 0.

Response: JSON { "zpl": "…", "labels": 2 }; with ?format=zpl the raw ZPL as application/x-zpl; charset=utf-8, ready for the printer. Renders: one per label (at least 1), never blocked by the quota. Webhook api.enrich.completed, summary { labels, fields, records, dpi, zplBytes }.

StatusError
400zpl is required …, fields is required …, Too many fields (max 50)., dpi must be 203, 300 or 600., format must be json (default) or zpl., invalid JSON (e.g. a number instead of a string in records).
403Label enrichment calls require the Pro plan. - the key owner is not on the Pro plan (body with plan and hint).
404Label enrichment is not enabled.
413zpl over 4,000,000 characters, more than 10,000 records, or body over 24 MB.
503The plan could not be checked (database) - retry later.

Example: SKU and bin on a carrier label

A 4 × 6 inch carrier label (101.6 × 152.4 mm, 203 dpi) gets the SKU on the left and the bin location on the right, at the bottom on a white strip:

{
  "zpl": "^XA…carrier label 1…^XZ^XA…carrier label 2…^XZ",
  "dpi": 203,
  "clearZone": true, "zoneX": 3, "zoneY": 136, "zoneW": 96, "zoneH": 12,
  "fields": [
    { "label": "SKU", "column": "sku", "x": 5,  "y": 139, "w": 60, "h": 6, "fontPt": 14 },
    { "label": "Bin", "column": "bin", "x": 66, "y": 139, "w": 31, "h": 6, "fontPt": 14, "align": "R" }
  ],
  "records": [
    { "sku": "A-1001", "bin": "R04-B2" },
    { "sku": "A-1002", "bin": "R11-A1" }
  ]
}
# put the carrier ZPL from a file into the body and get printable ZPL back
jq --rawfile zpl carrier-labels.zpl '.zpl = $zpl' enrich.json \
  | curl -X POST "https://api.zplcloud.com/v1/enrich/zpl?format=zpl" \
      -H "X-API-Key: sk_zplcloud_…" -H "Content-Type: application/json" \
      --data-binary @- -o enriched.zpl

Each label gets ^FO40,1111^A0N,39,39^FB480,1,0,L,^CI28^FDSKU: A-1001^FS and the right-aligned bin field before its ^XZ.

Batch jobs

POST https://api.zplcloud.com/v1/batch/design/{designId}

Renders a saved design with many records in one call: ZPL or PDF output, an optional pick list PDF and optional printing. Synchronous by default; "async": true answers immediately and calls the webhook when the job is done.

Pro plan

Batch jobs - including pick lists and printing - require the Pro plan of the key owner. Other plans get 403 before the body is read - a requested webhook callback reports the failure. If the plan cannot be read, the answer is 503. Job status, output and pick list (GET /v1/batch/jobs/…) are not plan-checked; jobs only exist if they were started on Pro. On other plans, render up to 500 records per call as ZPL or 2,000 as PDF.

HTTP/1.1 403 Forbidden

{
  "error": "Batch jobs require the Pro plan.",
  "plan": "starter",
  "hint": "Upgrade to Pro under Billing in the platform. GET /v1/quota shows your current plan.",
  "requestId": "3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13"
}
FieldTypeDefaultNotes
recordsarrayrequired1 to 10,000 objects, one per label (PDF output: max 5,000). Values may be strings, numbers or booleans.
outputstringzplzpl, pdf or none. none needs a printer or a pickList.
printerstring-Print target wl:{id}, Weblink serial, rp:{id} or vp:{id}, see printing.
pickListobject-Pick list PDF from the same records, see pick list.
asyncboolfalsetrue: 202 with jobId, the job runs in the background.

Body max 32 MB. Checks run in this order before anything is rendered: plan (Pro), body, records, output, design, quota (number of records), printer target - so a typo in the printer fails right away. Query parameters webhookId / webhookResult: event api.batch.completed.

Printing

TargetPrinterDelivery
wl:{id} or serialWeblink cloud printerThrough the printer's Weblink connection. Must be visible to the key owner; a blocked printer answers 409.
rp:{id}Remote printerThrough the zplCloud CLI agent (zplcloud proxy) in your network; the agent must be online.
vp:{id}Virtual printerLands in the virtual printer's history; max 512 KB per block. Only when virtual printers are enabled.

Targets come from GET /v1/printers (printers bound to the key) or GET /v1/company/printers (all printers of the company) - use the target field of an entry. Labels are sent in blocks of 100, one request per block. The first failed block stops printing: the job ends with print_failed, print.error holds the reason and sentChunks / sentLabels show how far it got. Output and pick list are delivered anyway; a print_failed job answers HTTP 200 and counts all records.

Target errors: 400 Invalid remote printer target - expected rp:{id}. (same for vp: and wl:), 400 Virtual printers are not available., 404 Printer not found. / Remote printer not found. / Virtual printer not found., 409 The printer is blocked.

Pick list

FieldTypeDefaultNotes
titlestringdesign IDHeading on page 1.
subtitlestringdate, time, rowsDefault: 2026-09-11 10:15 UTC · 250 Positionen.
columnsstring[]all keysColumns in this order; default all keys of the first record. Max 20.
groupBystring-Groups rows by this column (group row column: value), groups sorted.
sortBystring-Sorts rows (within groups) naturally: A-7 before A-10.
checkboxbooltrueTick box at the start of each row.
paperstringA4A4 or Letter.

Available only when pick lists are enabled (otherwise 400 Pick lists are not available.). A pick list holds at most 5,000 records; non-text values are printed as JSON text.

Synchronous response

{
  "jobId": "3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13",
  "status": "succeeded",
  "design": "shipping-label.42",
  "labels": 250,
  "durationMs": 1840,
  "output": {
    "format": "zpl",
    "contentType": "application/x-zpl; charset=utf-8",
    "bytes": 91234,
    "encoding": "utf-8",
    "data": "^XA…^XZ"
  },
  "pickList": { "contentType": "application/pdf", "bytes": 48211, "encoding": "base64", "data": "JVBERi0x…" },
  "print": {
    "printer": "rp:12", "type": "remote", "name": "Packing 1",
    "chunks": 3, "chunkSize": 100, "sentChunks": 3, "sentLabels": 250, "error": null
  },
  "outputUrl": "/v1/batch/jobs/3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13/output",
  "pickListUrl": "/v1/batch/jobs/3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13/picklist"
}
  • jobId equals the X-ZplCloud-Request-Id header. status is succeeded or print_failed.
  • output is null for none; PDF output comes as base64. pickList and print are null when not requested.
  • outputUrl is null when the output is larger than 20 MB (it is then only in this response).
  • Rendering errors: 400 { "error": "Rendering failed: …", "jobId": "…", "requestId": "…" }.

Asynchronous jobs

HTTP/1.1 202 Accepted

{
  "jobId": "3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13",
  "status": "queued",
  "design": "shipping-label.42",
  "labels": 250,
  "output": "none",
  "printer": "rp:12",
  "statusUrl": "/v1/batch/jobs/3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13",
  "webhook": { "id": 12, "includeResult": false, "event": "api.batch.completed" }
}
  • All checks (plan, records, design, quota, printer target) run before the 202, so those errors still come back directly.
  • At most two batch jobs run at the same time per server; further jobs wait as queued.
  • When the job is done the webhook is called with api.batch.completed; requestId is the jobId. With webhookResult=true the result holds the complete synchronous response as JSON (up to 10 MB).
  • The payload's status follows the HTTP result: a print_failed job arrives as "status": "succeeded" with summary.jobStatus: "print_failed" and summary.printError. Check summary.jobStatus.
  • Without webhookId, poll the statusUrl. Renders are counted when the job finishes, and only if it did not fail.
  • A server restart cancels running jobs: failed, The job was cancelled (server restart).
StatusMeaning
queuedAccepted, waiting for a free slot.
runningRendering output and pick list, then printing.
succeededDone; all blocks were accepted by the printer (if one was given).
print_failedOutput and pick list are ready, but a print block failed (print.error).
failedRendering failed or the job was cancelled (error).
LimitValue
Records per job10,000 (PDF output 5,000, pick list 5,000)
Request body32 MB
Print block100 labels
Parallel jobs2 per server, others wait
Stored output / retention20 MB / 1 hour

End to end: async job printing to rp:12

1. Find the target:

curl -u sk_zplcloud_…: https://api.zplcloud.com/v1/company/printers

{ "scope": "company", "company": "Example GmbH", "count": 3,
  "printers": [ { "target": "rp:12", "type": "remote", "id": 12, "name": "Packing 1", "online": true, … }, … ] }

2. Start the job with webhook 12 (GET /v1/webhooks lists the IDs; the key owner needs the Pro plan):

curl -X POST "https://api.zplcloud.com/v1/batch/design/shipping-label.42?webhookId=12" \
  -H "X-API-Key: sk_zplcloud_…" -H "Content-Type: application/json" \
  -d '{
    "records": [
      { "order": "SO-5001", "sku": "A-1001", "bin": "R04-B2", "qty": 2 },
      { "order": "SO-5001", "sku": "A-1002", "bin": "R11-A1", "qty": 1 }
    ],
    "output": "none",
    "printer": "rp:12",
    "pickList": { "title": "Wave 14", "columns": ["order", "sku", "bin", "qty"], "groupBy": "order", "sortBy": "bin" },
    "async": true
  }'

3. The call returns 202 with the jobId right away (see above). 4. When the last block has reached the printer, your webhook URL receives a POST with the headers X-ZplCloud-Event: api.batch.completed, X-ZplCloud-Delivery, X-ZplCloud-Timestamp and X-ZplCloud-Signature:

{
  "event": "api.batch.completed",
  "timestamp": "2026-09-11T10:15:07Z",
  "requestId": "3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13",
  "operation": "POST /v1/batch/design/{designId}",
  "status": "succeeded",
  "httpStatus": 200,
  "durationMs": 5120,
  "renders": 2,
  "summary": {
    "jobId": "3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13",
    "jobStatus": "succeeded",
    "design": "shipping-label.42",
    "labels": 2,
    "output": "none",
    "outputBytes": null,
    "printer": "rp:12",
    "printedLabels": 2,
    "printError": null,
    "error": null
  },
  "error": null,
  "resultIncluded": false
}

5. Fetch the pick list within the hour: GET /v1/batch/jobs/3f0c9a7e-…/picklist.

Job status and downloads

RouteResponseWebhook event
GET /v1/batch/jobs/{jobId}Status JSONapi.batch.status
GET /v1/batch/jobs/{jobId}/outputZPL (application/x-zpl) or PDF, up to 20 MBapi.batch.output
GET /v1/batch/jobs/{jobId}/picklistPick list PDFapi.batch.picklist
curl -u sk_zplcloud_…: https://api.zplcloud.com/v1/batch/jobs/3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13

{
  "jobId": "3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13",
  "status": "running",
  "design": "shipping-label.42",
  "labels": 2500,
  "output": "zpl",
  "outputBytes": 912340,
  "pickList": false,
  "createdUtc": "2026-09-11T10:15:02Z",
  "startedUtc": "2026-09-11T10:15:02Z",
  "finishedUtc": null,
  "durationMs": null,
  "error": null,
  "print": { "printer": "wl:5", "type": "weblink", "name": "Dock 3", "chunks": 25, "chunkSize": 100, "sentChunks": 9, "sentLabels": 900, "error": null },
  "outputUrl": "/v1/batch/jobs/3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13/output",
  "pickListUrl": null,
  "expiresUtc": "2026-09-11T11:15:02Z"
}
  • Jobs are kept in memory for 1 hour (expiresUtc) on the server that accepted them. After that, or on another server instance: 404 Batch job not found. Jobs are kept for 1 hour on the server that accepted them.
  • Only the same company (keys without a company: the same e-mail) can see a job.
  • /output answers 404 while the job is running, for output: none and for output over 20 MB; /picklist answers 404 while running or without pickList.
  • Status and downloads count no renders and are not plan-checked.

Quota and render counting

EndpointChecked (429)Renders counted
POST /v1/zpl/render/design/{designId}yesone per record (at least 1)
POST /v1/pdf/render/design/{designId}yesone per page (records, at least 1)
POST /v1/png/render/design/{designId}yes1
POST /v1/batch/design/{designId}yes, number of recordsone per record, also for print_failed
POST /v1/enrich/zplno, never blockedone per label (at least 1)
GET /v1/batch/jobs/…no0

Batch jobs and label enrichment also require the Pro plan (403, see above). Calls that fail (HTTP 400 and above) count nothing. When the monthly label quota is used up, checked endpoints answer before any work is done:

HTTP/1.1 429 Too Many Requests

{
  "error": "Render limit reached (9900/10000 labels this month, this request needs 250).",
  "hint": "GET /v1/quota shows your current label quota. Upgrade your plan or buy a render top-up under Billing in the platform. The ZPL tools under /v1/tools are not limited.",
  "limit": 10000,
  "used": 9900,
  "requested": 250,
  "requestId": "3f0c9a7e-5b1d-4c2e-9a41-7d0e6b2f8c13"
}

Plan limits, top-ups, buffered counting and GET /v1/quota: API overview → quota. The ZPL tools API is never blocked either.