Print from your backend

When something needs a label (an order ships, a pallet is built), your backend fills the stored template with the data and sends the ZPL to the printer:

  1. Read the template, its density and its size from your database (Store templates).
  2. Build the data for the label from your order, shipment or product: a JSON object, or an array for several labels.
  3. POST /v1/merge with { "template": "...", "data": {...} }. The response is plain-text ZPL with every {{variable}} filled in and escaped for the place it lands in (Templates & variables).
  4. Send the ZPL to the printer: raw TCP to port 9100 on network Zebra printers, or through your print server or queue.

Merging on the server keeps print jobs in your backend, where the data is. Examples use https://labelviewer.example.internal for your install; keep the address in configuration.

POST /v1/merge#

Body{ "template": string, "data": object | object[], "missing"?: "error" | "empty" } (JSON, up to 8 MiB)
Response200, text/plain ZPL: one copy of the template per record, joined by \n. Header X-Record-Count.
Errors422 merge_error with details: [{ record, placeholder, reason }]; 413 merge_limit over 1000 records, a 1 MiB template, 16 MiB of output, 64 KiB per value or 4096 placeholders; 400 invalid_request for a malformed body; 402 when the install's license does not cover the Editor (License)

Nothing is stored on the server, and merged ZPL is never watermarked (also on a trial license). missing: "empty" turns missing values into empty strings instead of errors (see Missing values).

curl -s -X POST "https://labelviewer.example.internal/v1/merge" -H 'Content-Type: application/json' -d '{
  "template": "^XA^FO50,50^A0N,40,40^FD{{name}}^FS^FO50,110^A0N,30,30^FDQty {{qty|1}}^FS^XZ",
  "data": [{"name": "Alpha"}, {"name": "Beta", "qty": 3}]
}'
^XA^FO50,50^A0N,40,40^FDAlpha^FS^FO50,110^A0N,30,30^FDQty 1^FS^XZ
^XA^FO50,50^A0N,40,40^FDBeta^FS^FO50,110^A0N,30,30^FDQty 3^FS^XZ

Merge and print#

Each snippet merges the data into a template and sends the ZPL to a network printer on port 9100.

Node.js (18+, built-in fetch)

import net from 'node:net';

const LABELVIEWER = process.env.LABELVIEWER_URL ?? 'https://labelviewer.example.internal';

export async function printLabel(template, order, printerHost) {
    const res = await fetch(`${LABELVIEWER}/v1/merge`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ template: template.zpl, data: order }),
    });
    if (!res.ok) {
        const { error } = await res.json();
        throw new Error(`merge failed: ${error.code}: ${error.message} ${JSON.stringify(error.details ?? [])} (request ${error.requestId})`);
    }
    const zpl = await res.text();
    await new Promise((resolve, reject) => {
        const socket = net.connect(9100, printerHost, () => socket.end(zpl, 'utf8'));
        socket.on('close', resolve).on('error', reject);
    });
}

Python (requests)

import os, socket, requests

LABELVIEWER = os.environ.get("LABELVIEWER_URL", "https://labelviewer.example.internal")

def print_label(template_zpl: str, order: dict, printer_host: str) -> None:
    r = requests.post(f"{LABELVIEWER}/v1/merge", json={"template": template_zpl, "data": order}, timeout=30)
    if r.status_code == 422:
        raise ValueError(r.json()["error"]["details"])   # [{record, placeholder, reason}]
    r.raise_for_status()
    with socket.create_connection((printer_host, 9100), timeout=10) as s:
        s.sendall(r.text.encode("utf-8"))

Java (11+, java.net.http; JSON with Jackson)

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.OutputStream;
import java.net.Socket;
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.time.Duration;
import java.util.Map;

public class LabelPrinter {
    private static final String LABELVIEWER = System.getenv().getOrDefault("LABELVIEWER_URL", "https://labelviewer.example.internal");
    private final HttpClient http = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
    private final ObjectMapper json = new ObjectMapper();

    public void print(String templateZpl, Map<String, Object> order, String printerHost) throws Exception {
        String body = json.writeValueAsString(Map.of("template", templateZpl, "data", order));
        HttpRequest req = HttpRequest.newBuilder(URI.create(LABELVIEWER + "/v1/merge"))
                .timeout(Duration.ofSeconds(30))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(body))
                .build();
        HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() != 200) {
            throw new IllegalStateException("merge failed: HTTP " + res.statusCode() + " " + res.body());
        }
        try (Socket socket = new Socket(printerHost, 9100); OutputStream out = socket.getOutputStream()) {
            out.write(res.body().getBytes(StandardCharsets.UTF_8));
        }
    }
}

C# (.NET 6+)

using System.Net.Http.Json;
using System.Net.Sockets;
using System.Text;

public class LabelPrinter(HttpClient http)
{
    // http.BaseAddress = new Uri("https://labelviewer.example.internal/") from configuration
    public async Task PrintAsync(string templateZpl, object order, string printerHost, CancellationToken ct = default)
    {
        using var res = await http.PostAsJsonAsync("v1/merge", new { template = templateZpl, data = order }, ct);
        var body = await res.Content.ReadAsStringAsync(ct);
        if (!res.IsSuccessStatusCode)
        {
            throw new InvalidOperationException($"merge failed: HTTP {(int)res.StatusCode} {body}");
        }
        using var client = new TcpClient();
        await client.ConnectAsync(printerHost, 9100, ct);
        await client.GetStream().WriteAsync(Encoding.UTF8.GetBytes(body), ct);
    }
}

Log merge errors with the template's id and version (see Versioning): a 422 usually means the template and your data disagree about a field name.

Preview before printing#

To show the user what will print (or attach an image to the order), render the merged ZPL with the Viewer's PNG route. The merged ZPL is exactly what goes to the printer, so the image is a preview of that print:

const zpl = await (await fetch(`${LABELVIEWER}/v1/merge`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ template: template.zpl, data: order }),
})).text();
const png = await fetch(`${LABELVIEWER}/v1/printers/${template.dpmm}dpmm/labels/${template.width_in}x${template.height_in}/0/`, { method: 'POST', body: zpl });
zpl = requests.post(f"{LABELVIEWER}/v1/merge", json={"template": t["zpl"], "data": order}, timeout=30).text
png = requests.post(f"{LABELVIEWER}/v1/printers/{t['dpmm']}dpmm/labels/{t['width_in']}x{t['height_in']}/0/", data=zpl.encode("utf-8"), timeout=30).content
// after the merge above: res.body() is the merged ZPL
HttpRequest preview = HttpRequest.newBuilder(URI.create(LABELVIEWER + "/v1/printers/8dpmm/labels/4x6/0/"))
        .POST(HttpRequest.BodyPublishers.ofString(res.body(), StandardCharsets.UTF_8))
        .build();
byte[] png = http.send(preview, HttpResponse.BodyHandlers.ofByteArray()).body();
// after the merge above: body is the merged ZPL
using var preview = await http.PostAsync("v1/printers/8dpmm/labels/4x6/0/", new StringContent(body, Encoding.UTF8), ct);
var png = await preview.Content.ReadAsByteArrayAsync(ct);

For several labels at once (an array of records) use POST /v1/render, which returns every label. Densities, sizes and the response format: Viewer › Render from your backend. In the browser, <zpl-viewer> previews a template with data without any server call (Viewer › Embed in your page).