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:

RouteUse it whenRequest → 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/renderThe 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 bodyPrinter resolution4 x 6 in label4 x 3 in label2 x 1 in label
6152 dpi608 x 912 px608 x 456 px304 x 152 px
8203 dpi (most desktop and industrial printers)812 x 1218 px812 x 609 px406 x 203 px
12300 dpi1200 x 1800 px1200 x 900 px600 x 300 px
24600 dpi2400 x 3600 px2400 x 1800 px1200 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):

HeaderMeaning
X-Total-CountHow many labels the ZPL produced (one per ^XA…^XZ, more with ^PQ serial numbers)
X-WarningsCommands the renderer ignored or could not draw, e.g. Unsupported command ^XY. The label still renders.
X-Request-IdThe 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:

StatusWhen
400 invalid_requestBad dpmm, size or index, or malformed JSON
402 license_required / license_expired / product_not_licensedThe install's license does not cover the Viewer (always JSON, also on the PNG route): License
404 not_foundindex past the last label
413 payload_too_largeThe body is over 8 MiB
422 unprocessableThe ZPL exceeds a render limit or the time budget, or produced no labels
503 unavailableThe 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.