API reference
Every HTTP endpoint of your install, grouped by product. Each links to the page with the details and code. The
machine-readable spec is openapi.yaml (OpenAPI 3.1).
Base URL: https://labelviewer.example.internal (your install). Requests and responses are UTF-8.
Endpoints#
Each product's endpoints need a license that includes it, else they answer 402 (Errors,
License). The server endpoints below always work.
Viewer: render ZPL to PNG#
| Endpoint | Guide | Request → response |
|---|---|---|
POST /v1/printers/{dpmm}dpmm/labels/{W}x{H}/{index}/ | One label as a PNG | Raw ZPL (or a form field file) → one PNG; JSON with Accept: application/json. Headers X-Total-Count, X-Warnings. Compatible with Labelary: only the host changes. |
POST /v1/render | Every label as JSON | { zpl, dpmm?, widthIn?, heightIn?, index?, upright? } → { dpmm, total, labels: [{ index, width, height, png }], warnings } (PNG as base64) |
Editor: fill a template#
| Endpoint | Guide | Request → response |
|---|---|---|
POST /v1/merge | Print from your backend, Templates & variables | { template, data, missing? } → text/plain ZPL, one copy per record. Header X-Record-Count. |
Server#
| Endpoint | Guide | Response |
|---|---|---|
GET /v1/info | below | { name, version, license, sdk } |
GET /healthz | below | { "status": "ok", "auth": false, "license": "<status>" } |
GET /sdk/<version>/<file>, GET /sdk/latest/<file> | below, SDK | The browser SDK files |
GET /v1/info#
The same answer for every caller; no configuration values and never the license text. The Status page reads it.
{"name": "labelviewer", "version": "1.0.1",
"license": {"status": "valid", "trial": false, "source": "configured", "allowed": {"viewer": true, "editor": true},
"products": ["viewer", "editor"], "licensee": "Example Co", "id": "LIC-0007", "expires": "2027-12-31",
"daysLeft": 437, "maxInstances": 2, "instanceId": "lv-3f2a9c0e41d7b6a85c19e2f0"},
"sdk": {"version": "1.0.1", "url": "/sdk/1.0.1/", "latest": "/sdk/latest/"}}
license field | |
|---|---|
status | valid, expired (past the end of its expiry date, UTC), missing (no license) or invalid (the key was not accepted; the reason is in the server log, never here) |
trial | true for a trial license: label images are watermarked |
source | configured (LICENSE_KEY / LICENSE_FILE) or bundled (the evaluation license in the download); absent when missing |
allowed | Whether each product works on this installation now |
products | The products in the license |
licensee, id, expires, daysLeft, maxInstances | From the license. expires and daysLeft are absent for a trial without an expiry date |
instanceId | This installation's id (set LABELVIEWER_INSTANCE_ID to choose it) |
See License. sdk is null when the server has no SDK build.
GET /healthz#
For container and load-balancer health checks: 200 whatever the license state (license is its status). auth is always false (kept for
older clients). See Install › Health checks.
SDK files#
labelviewer-elements.min.js, labelviewer-elements.js, fonts/*, index.d.ts, react.d.ts, vue.d.ts,
labelviewer-elements-<version>.tgz and manifest.json (every file with its size and SRI hash). Versioned URLs are
Cache-Control: public, max-age=31536000, immutable; latest URLs are no-cache with an ETag. Only the installed
version exists. How to load them: SDK.
Legacy aliases#
POST /api/render and POST /api/merge behave like /v1/render and /v1/merge and keep the legacy error shape
(below). New integrations should use /v1/*.
No authentication#
The API has no keys and no tokens, by design: labelviewer is an internal service you run next to the systems that print labels. Every endpoint can be called by anything that can reach the server. If your deployment needs access control, put the server behind your own gateway, VPN or reverse proxy and enforce it there (Install › Access control).
CORS#
Every API response (/v1/*, /api/*, /healthz) and every SDK file (/sdk/...) carries
Access-Control-Allow-Origin: *, so browser apps on any origin can call the API. Preflight requests (OPTIONS) answer
204 with Access-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, Access-Control-Allow-Headers: Content-Type,
Accept, X-Request-Id and Access-Control-Max-Age: 600. Access-Control-Expose-Headers lets pages read
X-Total-Count, X-Warnings, X-Record-Count and X-Request-Id. Credentials (cookies) are never used or allowed.
Errors#
/v1/* answers errors as JSON:
{"error": {"code": "merge_error", "message": "1 merge error", "details": [{"record": 0, "placeholder": "name", "reason": "missing value"}], "requestId": "4f0c…"}}
details is added when there is structured data. requestId equals the X-Request-Id response header. Send your own
X-Request-Id ([A-Za-z0-9._-]{1,64}) to correlate logs; otherwise the server generates one.
The legacy /api/render and /api/merge answer {"error": "<message>", "code", "details"?, "requestId"}. The
Labelary-compatible route answers plain text unless the request asked for JSON (Accept: application/json or a JSON
Content-Type), as Labelary does; its 402 license errors are always JSON.
| Status | code | When |
|---|---|---|
| 400 | invalid_request | Malformed JSON, wrong field types, bad dpmm/size/index |
| 402 | license_required | No license, or the configured license key is not valid. Install a valid key (LICENSE_KEY / LICENSE_FILE) and restart |
| 402 | license_expired | The license expired (there is no grace period). Install the renewed key and restart |
| 402 | product_not_licensed | The license does not include this product (Viewer: render routes; Editor: merge routes) |
| 404 | not_found | Unknown route, or index past the last label |
| 405 / 406 | method_not_allowed / not_acceptable | Wrong method (see Allow) / an Accept that allows neither PNG nor JSON |
| 413 | payload_too_large / merge_limit | Body over the limit / too many records or output too large |
| 422 | merge_error | Template or data problems, listed in details |
| 422 | unprocessable | The ZPL exceeds a render limit or the time budget, or produced no labels (PNG) |
| 503 | unavailable | The render queue is full; retry with backoff |
| 500 | internal | Bug; logged on the server with the request id |
Messages never contain your data.
Limits#
Every request is bounded, so one bad template or upload cannot take the server down:
| Limit | Value | Error |
|---|---|---|
| Request body (render, merge, Labelary) | 8 MiB | 413 payload_too_large |
| Whole request (headers + body) | 30 s (REQUEST_TIMEOUT_MS); headers within 10 s | connection closed |
| Render time | 10 s per render (RENDER_TIMEOUT_MS); the worker is replaced | 422 unprocessable |
| Render memory | 512 MiB heap per render worker (RENDER_MAX_HEAP_MB) | 422 unprocessable |
| Render queue | 64 waiting renders (RENDER_MAX_QUEUE) | 503 unavailable (retry with backoff) |
| Labels | 0.1 to 15 in per side; at most 50 labels per render (more are cut off with a warning); pixel and work budgets inside the renderer | 400 / 422 |
| Merge | 1000 records, 1 MiB template, 16 MiB output, 64 KiB per value, 4096 placeholders | 413 merge_limit |
The environment variables are described in Install › Configuration.