Render from your backend
Send ZPL to the install over HTTP and get the label back as a PNG. There are two routes with the same renderer:
| Route | Use it when | Request → response |
|---|---|---|
POST /v1/printers/{dpmm}dpmm/labels/{W}x{H}/{index}/ | You want one image. Compatible with Labelary: only the host changes. | The raw ZPL → one label as image/png |
POST /v1/render | The ZPL holds several labels, or you need the warnings or upright. | JSON { zpl, dpmm?, widthIn?, heightIn?, index?, upright? } → every label as base64 PNG, plus the warnings |
The examples use BASE=https://labelviewer.example.internal, the address of your install. The API needs no key (see
Install for who can reach it).
Choosing the density and size#
The image size in pixels is the label size times the printer's resolution. Pick the density of the printer the label is for, and the size of the label stock.
dpmm in the URL or body | Printer resolution | 4 x 6 in label | 4 x 3 in label | 2 x 1 in label |
|---|---|---|---|---|
6 | 152 dpi | 608 x 912 px | 608 x 456 px | 304 x 152 px |
8 | 203 dpi (most desktop and industrial printers) | 812 x 1218 px | 812 x 609 px | 406 x 203 px |
12 | 300 dpi | 1200 x 1800 px | 1200 x 900 px | 600 x 300 px |
24 | 600 dpi | 2400 x 3600 px | 2400 x 1800 px | 1200 x 600 px |
Sizes are in inches, from 0.1 to 15, and may have decimals (2.25x1.25). Label size in dots uses the nominal dpi
(152/203/300/600). ZPL positions are in printer dots, so the same ZPL at another density prints at another physical
size: render at the density of the printer you will use.
Content outside the label (beyond the width or height) is cut off, as on the printer. ^PW narrows the printable width
further; ^LL does not change the image size (the physical label does).
One label as a PNG#
POST /v1/printers/{dpmm}dpmm/labels/{W}x{H}/{index}/ with the raw ZPL as the body (or a form field file). index is
the 0-based label to return (default 0); index and the trailing slash are optional. The response is a 1-bit
black-and-white PNG whose pHYs chunk records the density, so image tools show the right physical size.
curl -s --data-binary @label.zpl "$BASE/v1/printers/8dpmm/labels/4x6/0/" -o label.png
With Accept: application/json this route answers JSON in the same shape as /v1/render (only the label at index
when the path has one).
Response headers (both routes):
| Header | Meaning |
|---|---|
X-Total-Count | How many labels the ZPL produced (one per ^XA…^XZ, more with ^PQ serial numbers) |
X-Warnings | Commands the renderer ignored or could not draw, e.g. Unsupported command ^XY. The label still renders. |
X-Request-Id | The request id, also in error bodies and the server's access log |
Every label as JSON#
POST /v1/render with { zpl, dpmm?, widthIn?, heightIn?, index?, upright? }. Defaults: dpmm 8, widthIn 4,
heightIn 6. Without index it returns every label:
{ "dpmm": 8, "total": 2, "warnings": [],
"labels": [ { "index": 0, "width": 812, "height": 1218, "png": "iVBORw0KGgo..." }, { "index": 1, "...": "..." } ] }
upright: true ignores ^PO I, so labels read the right way up (see
Upside-down labels). The PNG route has no such option.
curl -s -X POST "$BASE/v1/render" -H 'Content-Type: application/json' \
-d '{"zpl":"^XA^FO20,20^GB200,100,4^FS^XZ","dpmm":8,"widthIn":4,"heightIn":6}' | jq '.total, .warnings'
Code#
Node.js (18 or later, built-in fetch)
import { writeFile } from 'node:fs/promises';
const BASE = process.env.LABELVIEWER_URL ?? 'https://labelviewer.example.internal';
// One label, as a PNG
const res = await fetch(`${BASE}/v1/printers/8dpmm/labels/4x6/0/`, { method: 'POST', body: zpl });
if (!res.ok) throw new Error(`render failed: ${res.status} ${await res.text()}`);
await writeFile('label.png', Buffer.from(await res.arrayBuffer()));
// Every label, as JSON
const r = await fetch(`${BASE}/v1/render`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ zpl, dpmm: 8, widthIn: 4, heightIn: 6 }),
});
const { labels, warnings } = await r.json();
for (const l of labels) await writeFile(`label-${l.index}.png`, Buffer.from(l.png, 'base64'));
Python (requests)
import base64, os, requests
BASE = os.environ.get("LABELVIEWER_URL", "https://labelviewer.example.internal")
r = requests.post(f"{BASE}/v1/printers/8dpmm/labels/4x6/0/", data=zpl.encode("utf-8"), timeout=30)
r.raise_for_status()
open("label.png", "wb").write(r.content)
r = requests.post(f"{BASE}/v1/render", json={"zpl": zpl, "dpmm": 8, "widthIn": 4, "heightIn": 6}, timeout=30)
r.raise_for_status()
for label in r.json()["labels"]:
open(f"label-{label['index']}.png", "wb").write(base64.b64decode(label["png"]))
Java (11 or later, java.net.http)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class LabelImages {
private static final String BASE = System.getenv().getOrDefault("LABELVIEWER_URL", "https://labelviewer.example.internal");
private final HttpClient http = HttpClient.newHttpClient();
public void renderPng(String zpl, Path out) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/v1/printers/8dpmm/labels/4x6/0/"))
.POST(HttpRequest.BodyPublishers.ofString(zpl, StandardCharsets.UTF_8))
.build();
HttpResponse<byte[]> res = http.send(req, HttpResponse.BodyHandlers.ofByteArray());
if (res.statusCode() != 200) {
throw new IllegalStateException("render failed: HTTP " + res.statusCode());
}
Files.write(out, res.body());
}
}
C# (.NET 6 or later)
public class LabelImages(HttpClient http)
{
// http.BaseAddress = new Uri("https://labelviewer.example.internal/") from configuration
public async Task RenderPngAsync(string zpl, string path, CancellationToken ct = default)
{
using var res = await http.PostAsync("v1/printers/8dpmm/labels/4x6/0/", new StringContent(zpl, System.Text.Encoding.UTF8), ct);
res.EnsureSuccessStatusCode();
await File.WriteAllBytesAsync(path, await res.Content.ReadAsByteArrayAsync(ct), ct);
}
}
Errors#
Errors answer JSON {"error": {"code", "message", "details"?, "requestId"}} (the PNG route answers plain text unless the
request asked for JSON, as Labelary does). The ones you will meet when rendering:
| Status | When |
|---|---|
400 invalid_request | Bad dpmm, size or index, or malformed JSON |
402 license_required / license_expired / product_not_licensed | The install's license does not cover the Viewer (always JSON, also on the PNG route): License |
404 not_found | index past the last label |
413 payload_too_large | The body is over 8 MiB |
422 unprocessable | The ZPL exceeds a render limit or the time budget, or produced no labels |
503 unavailable | The render queue is full: retry with backoff |
Every status and limit is in the API reference. On a trial license every PNG carries a "TRIAL" watermark (License).
Labels from a template#
Templates made with the Editor hold {{variables}}. To show one filled with an order's data, merge it first, then render
the merged ZPL: see Print from your backend.