Documentation · zplCore Designer
zplCore Designer
the embeddable label editor.
The zplCore Designer is a WYSIWYG editor for ZPL labels that you run yourself and embed in your own application: design JSON and data in, design JSON, ZPL, PDF, PNG and SVG out. The canvas shows what the printer prints, because every preview is the rendered ZPL of the zplCore.com engine. The editor stores nothing and prints nothing – your application keeps the design and does the printing. This page covers the quick starts (NuGet, Docker, JavaScript, Web Component, Blazor, WPF, MAUI), all options, the render API, the JavaScript API, the design schema and operations.
01Overview
The designer consists of two parts that always run together: the editor UI (static files, an Angular app without external requests) and a stateless render API in .NET that renders previews, measures the printed geometry of each element for the canvas and converts images and SVGs. Both are delivered by one ASP.NET Core library; the container is that library with a minimal host.
| Delivery | What you get | For whom |
|---|---|---|
NuGet ZplCore.Designer | AddZplCoreDesigner() + MapZplCoreDesigner("/designer"): editor UI (embedded in the package) and render API in your ASP.NET Core app, behind your own authentication | .NET teams that integrate the editor into their web app |
Docker zplcore/designer | Ready-to-run container (distroless, non-root) with editor and API, configured by environment variables; Helm chart | Teams without .NET, Kubernetes, embedding by iframe |
JavaScript SDK zplcore-designer.js | ZplCoreDesigner.mount(…): iframe embedding with a promise API, delivered by every designer host (and as npm package @zplcore/designer) | Any web app (React, Vue, Angular, server-rendered) |
Web Component <zplcore-designer> | The same editor as a custom element (a slim wrapper around the SDK, no Angular runtime in your page) | Pages that prefer an HTML tag to a script call |
Blazor ZplCore.Designer.Blazor | <ZplCoreDesigner @bind-Design="…" /> for Blazor Server and WebAssembly | Blazor apps |
| Desktop | The designer host on loopback + WebView2 (WPF, WinForms) or the MAUI WebView | Desktop applications |
What is in it
- Canvas with all element types: text, barcode (1D/2D, GS1, artistic codes), rectangle, line, circle/ellipse, image, printer-memory graphic, SVG icon, ZPL element, table, RFID.
- Property panel, element list, groups, several pages, grid, rulers, snapping, zoom, undo/redo, clipboard, keyboard shortcuts.
- Data binding with JSON records (from your app or edited in the editor), preview per record, hiding per record, expressions and number formats.
- Previews: ZPL, PDF, PNG, SVG; ZPL analyser and linter; GS1 assistant, RFID fields, image → ZPL, SVG → ZPL, icon library.
- 10 UI languages (en, de, zh, ja, es, fr, pt, it, ar, hi), light and dark, mm / inch / dots, 203 / 300 / 600 dpi.
What is deliberately not in it
- Printing of any kind – your application sends the ZPL (for example with the zplCore SDK).
- Storage, accounts, versions or teams – your application receives the JSON and stores it.
- Data-source connectors – data comes as JSON records from your application.
- An AI copilot and the old zplCloud design format
{ label, fields }(convert it first).
02Quick start: NuGet (ASP.NET Core)
dotnet add package ZplCore.Designer
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(/* your scheme */);
builder.Services.AddAuthorization();
builder.Services.AddZplCoreDesigner(o =>
{
o.Fonts.AddDirectory("fonts"); // optional: your TTF/OTF printer fonts (E:NAME.TTF)
o.PrinterGraphics.AddDirectory("graphics"); // optional: your printer-memory graphics (E:NAME.GRF)
o.Limits.MaxRecordsPerRequest = 500;
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapZplCoreDesigner("/designer")
.RequireAuthorization("label-editors"); // your auth – the designer brings none of its own
app.Run();
The editor is now at /designer/, the API at /designer/api/…, the OpenAPI document at /designer/api/openapi/zplcore-designer.json and a health answer at /designer/healthz. MapZplCoreDesigner returns a convention builder: .RequireAuthorization(…) protects UI and API, .Api and .Ui address one part each (for example a public UI with a protected API).
o.AllowAnonymous = true – for example behind your own proxy or for a public demo like the live editor.03Quick start: Docker
docker run -d -p 8080:8080 --read-only --tmpfs /tmp \ -e ZPLCORE_DESIGNER__AUTH__MODE=apikey \ -e ZPLCORE_DESIGNER__AUTH__APIKEYS__0=<a key of at least 16 characters> \ -e ZPLCORE_DESIGNER__ALLOWEDORIGINS__0=https://app.example.com \ -v ./fonts:/fonts:ro -v ./graphics:/graphics:ro \ zplcore/designer
The editor is at http://localhost:8080/designer/. The image is distroless (Azure Linux), runs as the non-root user app on port 8080 and works with a read-only root file system and a writable /tmp; it is built for linux/amd64 and linux/arm64. The container is configured with the section Designer as environment variables ZPLCORE_DESIGNER__… (double underscore = nesting, __0 = first list entry); every option of section 09 can be set this way, for example ZPLCORE_DESIGNER__LIMITS__MAXRECORDSPERREQUEST=200.
| Variable | Meaning |
|---|---|
ZPLCORE_DESIGNER__PATHBASE | Route prefix of editor and API, default /designer |
ZPLCORE_DESIGNER__AUTH__MODE | Required: apikey, jwt (OIDC) or none (explicitly, only behind your own proxy; the log warns) |
…__AUTH__APIKEYS__0 … · …__AUTH__APIKEYHEADER | API keys (at least 16 characters, from a secret) · header, default X-Api-Key |
…__AUTH__AUTHORITY, __AUDIENCE, __REQUIREDROLE, __REQUIREHTTPSMETADATA | JWT / OIDC (header Authorization: Bearer …) |
…__ALLOWEDORIGINS__0 … | Origins that may embed the editor (frame-ancestors) and call the API from the browser (CORS); empty = same origin only |
…__LIMITS__* | Protection limits, see Limits, incl. RATELIMIT__PERMITSPERWINDOW |
…__FEATURES__* | false switches an endpoint (and its tab) off |
…__UI__BRANDING__* | Title, logo, favicon, hidden tabs of the editor |
OTEL_EXPORTER_OTLP_ENDPOINT (+ OTEL_*) | OpenTelemetry export of metrics, traces and logs; unset = off |
ASPNETCORE_FORWARDEDHEADERS_ENABLED=true | Trust X-Forwarded-* of your ingress (client IP for the rate limit) |
Mounted folders (all optional, read-only is enough): /fonts printer-memory fonts, /graphics printer-memory graphics, /templates design templates (*.json), /icons your SVG icons, /svg-fonts SVG fonts and glyph sets, /graphic-fonts fonts released for graphic text (ZPLCORE_DESIGNER__FOLDERS__* for other paths). Liveness /healthz, readiness /readyz (renders a tiny label). The API needs the key or token; the static UI and {prefix}/healthz stay public.
Kubernetes: the Helm chart deploy/helm/zplcore-designer (API keys only from an existing secret, non-root, read-only root file system, probes, optional HPA and network policy):
kubectl create secret generic designer-api-keys --from-literal=key1=<key> helm install designer deploy/helm/zplcore-designer \ --set auth.apiKey.existingSecret=designer-api-keys \ --set designer.allowedOrigins[0]=https://app.example.com
04Embedding with JavaScript (iframe)
Every designer host delivers the SDK at {baseUrl}/sdk/zplcore-designer.js (no dependencies, no CDN); with a bundler use the npm package @zplcore/designer. The SDK creates an iframe on the editor and talks to it with postMessage; the iframe isolates styles and scripts completely from your page.
<div id="editor" style="height: 720px"></div>
<script src="https://labels.example.com/designer/sdk/zplcore-designer.js"></script> <!-- global ZplCoreDesigner -->
<script type="module">
const editor = await ZplCoreDesigner.mount('#editor', {
baseUrl: 'https://labels.example.com/designer/',
design, // design document { label, editor, records } or only the label JSON
data: [{ name: 'Anna', ean: '9506000134352' }],
locale: 'en', theme: 'auto', unit: 'mm',
requestHeaders: { 'X-Api-Key': key }, // container with API key; travels by postMessage, never in a URL
onChange: (doc) => save(doc) // user edits only, debounced, incl. records
});
const zpl = await editor.getZpl(); // the same ZPL the server prints
</script>
// with a bundler
import { mount } from '@zplcore/designer';
const editor = await mount('#editor', { baseUrl: '/designer/' });
When the editor runs on another origin than your app, list your origin in AllowedOrigins of the designer host; across origins load the classic script (the ES module from another origin would need CORS). Ready-made wrappers for React 19, Vue 3 and Angular are in the SDK's examples/. All options, methods and events: section 11.
05Embedding as a Web Component
<script src="/designer/sdk/zplcore-designer.js"></script> <!-- defines <zplcore-designer> -->
<zplcore-designer base-url="/designer/" locale="en" theme="auto" unit="mm"
features="-gs1,-rfid" style="display: block; height: 720px"></zplcore-designer>
<script type="module">
const el = document.querySelector('zplcore-designer');
el.design = doc; // or the attribute design='{"label":…}'
el.data = rows;
el.addEventListener('zplcore-change', (e) => save(e.detail.design));
const zpl = await el.getZpl();
</script>
With a bundler: import '@zplcore/designer/element'. The element is a slim wrapper around the iframe SDK (Shadow DOM, the iframe is the part frame): attributes base-url, design, locale, theme, unit, tab, features, read-only, autosave, allowed-origins, timeout, title; properties design, data, requestHeaders; events (bubbling, composed) zplcore-ready, zplcore-change, zplcore-selection-change, zplcore-validation, zplcore-error; all SDK methods on the element. It works in every framework that renders custom elements.
06Blazor
dotnet add package ZplCore.Designer.Blazor
<ZplCoreDesigner @ref="editor" @bind-Design="design" Data="rows" BaseUrl="/designer"
Locale="en" Theme="DesignerTheme.Auto" ReadOnly="false" Height="720px"
OnChange="OnChange" OnSelectionChange="OnSelection" />
@code {
ZplCoreDesigner? editor;
JsonObject? design; // DesignDocument { schemaVersion, label, editor, records }
List<object> rows = [new Dictionary<string, object?> { ["name"] = "Anna", ["ean"] = "9506000134352" }];
async Task Export()
{
string zpl = await editor!.GetZplAsync();
byte[] pdf = await editor.GetPdfAsync();
}
}
// same host (Blazor Server or a hosted WebAssembly app)
builder.Services.AddZplCoreDesigner();
app.MapZplCoreDesigner("/designer").RequireAuthorization(); // your auth
The component wraps the JavaScript SDK for Blazor Server and WebAssembly (interactive render modes; nothing happens during prerendering). The editor is served by a ZplCore.Designer host in the same app or by the container: BaseUrl="https://labels.example.com/designer", list your app in ZPLCORE_DESIGNER__ALLOWEDORIGINS__0 and pass the key with RequestHeaders. Designs, ZPL and PDFs come back as byte streams, so the SignalR message limit of Blazor Server does not apply.
| Parameter | Meaning |
|---|---|
BaseUrl (required) | URL of the editor (MapZplCoreDesigner prefix or container URL) |
Design / DesignChanged | Design document, two-way with @bind-Design |
Data | Records for the binding (a new collection instance is sent) |
Locale, Theme, ReadOnly | Changeable at any time |
Unit, Tab, Features, Autosave, RequestHeaders, AllowedOrigins, SdkUrl | Read when mounting |
OnChange, OnSelectionChange, OnValidation, OnReady, OnError | Events (OnValidation switches the validation on) |
Height, Class, Title, MaxResultBytes | Host element, iframe title, largest result read back (64 MB) |
Methods: GetDesignAsync, SetDesignAsync, SetDataAsync, GetZplAsync, GetPdfAsync, GetPngAsync, GetSelectionAsync, ValidateAsync, UndoAsync, RedoAsync, SetLocaleAsync, SetThemeAsync, SetRequestHeadersAsync.
07WPF / WinForms with WebView2
A desktop app starts the designer in its own process (NuGet ZplCore.Designer, Kestrel on a random 127.0.0.1 port) and shows it in WebView2. A small bridge page mounts the editor with the JavaScript SDK and forwards commands and events between C# (CoreWebView2.PostWebMessageAsString / WebMessageReceived) and the editor. The API answers only requests with a random per-process token, so other programs on the machine cannot use the port; nothing leaves the machine.
// samples/DesignerWpf: host on a random loopback port, API only with a per-process token
var host = await LocalDesignerHost.StartAsync(Path.Combine(AppContext.BaseDirectory, "bridge"));
await Web.EnsureCoreWebView2Async();
var core = Web.CoreWebView2;
var bridge = new DesignerBridge(json => { core.PostWebMessageAsString(json); return Task.CompletedTask; });
core.WebMessageReceived += (_, e) => bridge.OnMessage(e.TryGetWebMessageAsString());
core.Navigate(host.BridgePageUrl); // the bridge page mounts the editor with the JS SDK
await bridge.InitAsync(host.DesignerUrl, design, data, "en", headers: host.Headers);
string zpl = await bridge.GetZplAsync();
JsonObject? doc = await bridge.GetDesignAsync();
The sample samples/DesignerWpf (shared parts in samples/designer-embed: bridge page, DesignerBridge, LocalDesignerHost, record import from JSON or CSV) opens and saves designs, loads records, exports ZPL and PDF and switches undo/redo and the language. It needs the WebView2 runtime.
08.NET MAUI
A HybridWebView shows the same bridge page; commands and events go through DesignerBridge. On Windows the designer host runs inside the app (LocalDesignerHost); on Android, iOS and Mac Catalyst the app uses a designer host or the container on your network – allow the page origin there:
ZPLCORE_DESIGNER__ALLOWEDORIGINS__0=https://0.0.0.1 # Android ZPLCORE_DESIGNER__ALLOWEDORIGINS__1=app://0.0.0.1 # iOS / Mac Catalyst
The sample samples/DesignerMaui needs the MAUI workloads (dotnet workload install maui-windows maui-android) and is not part of the solution. The canvas is designed for mouse and pen; on touch devices the editor works with limitations.
09Options and configuration
AddZplCoreDesigner(o => …) configures ZplCoreDesignerOptions; the container binds the same object from the section Designer.
| Option | Default | Meaning |
|---|---|---|
PathBase | /designer | Prefix used by MapZplCoreDesigner() without argument |
AllowedOrigins | empty | Origins that may frame the editor and call the API cross-origin; empty = same origin only |
AllowAnonymous | false | Open the API without an authenticated user (explicitly only) |
Fonts.AddDirectory(dir, "E") | – | Printer-memory fonts (TTF/OTF) as E:NAME.TTF; Fonts.Providers for own IFontProviders |
Fonts.AddGraphicDirectory(dir) | – | Fonts released for graphic text (they must be installed on the server too); system fonts are never served |
PrinterGraphics.AddDirectory(dir) / .Provider | none | Printer-memory graphics (^XG) for canvas and render |
Icons.AddDirectory(dir) / .Provider | none | Your SVG icons next to the built-in library |
Templates.AddDirectory(dir) / .Provider | none | Design templates (id.json, translations id.de.json) |
SvgFonts.AddDirectory(dir) | none | SVG fonts and glyph sets for the fonts tab |
Limits | see below | Protection limits |
Features.* | all true | Zpl, Png, Pdf, Svg, Preview, ItemInk, ZplElement, Fonts, PrinterGraphics, ImageConvert, SvgConvert, SvgOptimize, Icons, Templates, ZplAnalyse, ZplLint, SvgFonts, ZplImport, BarcodeGenerate, BarcodeReadability, OpenApi, Ui – false does not map the endpoint (404) and hides its tab |
Ui.Branding | – | Title, PlatformTitle, Logo, LogoAlt, Favicon, HideTabs, HideHeaderLinks |
Ui.FileProvider | embedded UI | Serve a customised build of the editor UI |
Limits
| Limit | Default |
|---|---|
MaxRequestBodyBytes | 16 MB |
MaxRecordsPerRequest | 500 |
MaxItemsPerDesign | 2,000 |
MaxInkItemsPerRequest · MaxInkBatchRequests · MaxInkBatchItems | 60 · 50 · 1,000 |
MaxImageBytes · MaxImagePixels | 10 MB · 40 MP |
MaxSvgBytes · MaxGraphicDots | 2 MB · 16 M dots |
MaxZplElementBytes · MaxLabelMm | 1 MB · 1,000 mm |
MaxConcurrentRenders · MaxQueuedRenders · QueueTimeout | CPU count · 4 × CPU count · 10 s |
RenderTimeout | 30 s |
ItemInkCacheEntries | 20,000 |
RateLimit.PermitsPerWindow · RateLimit.Window | 0 (off) · 1 min, per client IP |
Extension points
Interfaces you register in DI or pass in the options: IFontProvider, IPrinterGraphicsProvider, IIconProvider, ITemplateProvider (content), IDesignerAuthorization (allow or refuse an operation per request), IRenderObserver (logging, metrics) and IDesignerLicenseValidator (licence check).
Configuration of the editor UI
The UI reads, later sources winning: {prefix}/config.json of the server (features, embedding origins, branding – no secrets) → window.__ZPLCORE_DESIGNER__ of a host page → the query string for display options only (locale, theme, unit, tab, autosave=0, features=-images,-fonts). The API base, request headers and origins never come from the URL. autosave (default on) keeps a draft in the browser's local storage under the prefix zplcore.designer.; switch it off when your app stores the design.
10Render API reference
All routes are relative to {prefix}/api (e.g. /designer/api). Requests and responses are UTF-8 JSON with camelCase names; every request is self-contained, nothing is stored. Responses carry X-ZplCore-Designer-Api: 1. The editor uses this API; your backend can call it directly (with the host's authentication or the container's API key).
Design rendering
{
"document": { "label": { "unit": "Mm", "width": 60, "height": 40, "items": [
{ "type": "text", "name": "name", "x": 3, "y": 3, "dataField": "name", "fontSize": 14 } ] } },
"records": [ { "name": "Anna" }, { "name": "Ben" } ]
}
| Route | Body | Result |
|---|---|---|
POST /zpl | document, records? | { "zpl": "^XA…^XZ" }, one format per record |
POST /render/png | document, record? or records? (first) | image/png, one label |
POST /render/pdf | document, records? | application/pdf, one page per record |
POST /render/svg | document, records? (first) | image/svg+xml (vector) |
POST /render/preview | as png | image/png – print image for the canvas overlay |
document is a design document (or { "label": … }); the label is read strictly (unknown properties → 400). Values come from records/record, not from document.records. Every image is the rendered /zpl result: preview = print.
curl -X POST https://labels.example.com/designer/api/zpl \ -H "X-Api-Key: $DESIGNER_API_KEY" -H "Content-Type: application/json" \ -d @request.json
Canvas helpers
| Route | Purpose |
|---|---|
POST /item-ink · POST /item-ink/batch | Printed geometry of each element in dots (x, y, width, height, for linear barcodes also bars*) – keeps the canvas congruent with the print; cached on the server |
POST /zpl-element | Preview of a ZPL element: { zpl, dpi, widthMm, heightMm } → { empty, x, y, width, height, png } |
GET /fonts | { graphic: [families], printer: [{ name, family }] } |
GET /fonts/font0 · /fonts/mono · /fonts/printer?name=E:X.TTF | Font files for the canvas |
GET /printer-graphics · /printer-graphics/png?name=E:LOGO.GRF | Printer-memory graphics of the host |
POST /barcode/readability | Read-back check of artistic barcodes (findings AR001–AR010) |
Conversion and analysis
| Route | Purpose |
|---|---|
POST /image/convert | Image (base64 PNG/JPEG/GIF/BMP) → grf and zpl, with crop, rotate, flip, size and blackness; stateless, no upload |
POST /svg/convert · POST /svg/optimize | SVG → ^GFA / optimised SVG (scripts, event handlers, external references removed) |
POST /zpl/analyse · POST /zpl/lint | ZPL analyser (commands, print time) and linter (issues with fix) |
POST /zpl/design · POST /zpl/decode | ZPL → design document (import) · bytes of a ZPL file → text |
POST /barcode/generate | Single barcode as ZPL + PNG (generator tab) |
GET /icons · /icons/{id} · /svg-fonts/{path} · /templates · /templates/{id}/design · /templates/{id}/thumb | Content of the host |
Errors
{ "error": "The request body is larger than 16,777,216 bytes.", "code": "too_large" }
| HTTP | code | When |
|---|---|---|
| 400 | invalid_request · render_failed | Unreadable or invalid request · the design could not be rendered |
| 401 / 403 | forbidden · license_invalid | Authentication of the host / container, authorization hook, licence check |
| 404 | not_found | Unknown font, graphic, icon, template or route, or endpoint switched off |
| 413 | too_large | Above a limit (the message names it) |
| 415 | unsupported_media_type | Image format other than PNG/JPEG/GIF/BMP |
| 429 | rate_limited | Rate limit; Retry-After header |
| 503 · 504 | busy · timeout | All render slots busy (Retry-After) · render longer than RenderTimeout |
Server options for the UI: GET {prefix}/config.json; health: GET {prefix}/healthz → { status, version, buildTimeUtc }. The complete OpenAPI description is served at {prefix}/api/openapi/zplcore-designer.json by your own host.
11JavaScript API
ZplCoreDesigner.mount(target, options)
Creates the editor in target (element or selector) and resolves with an editor object once the editor answers.
| Option | Default | Meaning |
|---|---|---|
baseUrl | – (required) | URL of the editor, e.g. /designer/ or https://labels.example.com/designer/ |
design | start design | Design document, label JSON or JSON text |
data | design.records | Records for the binding |
locale · theme · unit · tab | stored / browser · auto · mm · – | 10 languages · light / dark / auto · mm / inch · start tab (pdf, zpl, data …) |
features | all on | Switch parts off: { gs1: false }, ['-gs1'] or '-gs1,-rfid' – gs1 rfid images icons fonts zplElement jsonTab testData templates pdfPreview svgPreview pngPreview zplTab zplAnalyser zplLinter zplToDesign dpiCalc generator serverStatus commandPalette fileMenu |
readOnly | false | View, preview and export only |
autosave | false | Keep a draft in the browser (your app saves through onChange) |
requestHeaders | – | Headers of the editor's API calls (object or async function), e.g. { 'X-Api-Key': … } or Authorization: Bearer … |
allowedOrigins | origin of baseUrl | Origins whose messages are accepted |
timeout · readyTimeout | 30,000 ms | Time limit of a command (each call also accepts { timeout }) · until the editor is ready |
title, className | – | iframe title (accessibility) and CSS class |
onChange · onSelectionChange · onValidation · onReady · onError | – | Events: user edits (debounced, complete design incl. records; not your own setDesign/setData) · { names, items } · server linter result after each change · ready · { code, message } |
Methods (all return promises)
| Method | Result |
|---|---|
getDesign() · setDesign(doc, records?) | Design document ({ label, editor, … }) · load a design (document or label JSON) |
setData(records) | Replace the records |
getZpl({ records? }) | ZPL for all (or the given) records – the same ZPL the server prints |
getPdf({ records? }) · getPng({ record: 0 }) | Blob of the PDF / of one label as PNG |
undo() · redo() | History |
setLocale(l) · setTheme(t) · setReadOnly(on) · setRequestHeaders(h) | Language, colour scheme, read-only mode, new token |
getSelection() · validate() | { names, items } · { valid, errorCount, warningCount, infoCount, issues } |
ping() · whenReady() · on(event, fn) / off(event, fn) | Liveness, readiness, events after mounting |
destroy() | Remove the iframe and all listeners |
All methods return promises; failures reject with ZplCoreDesignerError (code: timeout, command_failed, ready_timeout, destroyed, invalid_options …). createDesigner() returns the instance at once (commands wait until it is ready).
The postMessage protocol
Without the SDK you can talk to the iframe directly (protocol version 2). Host → editor: { source: 'zplcore-host', type, id, payload }; editor → host: { source: 'zplcore', type: 'result', id, payload } and the events ready and change (debounced, { design, records }). Commands: ping, getDesign, setDesign { design, records? }, setData { records }, getZpl { records? }, getPdf, getPng { record? } (both { mimeType, base64 }), undo, redo, setLocale { locale }, setTheme { theme }. The first valid sender fixes the host origin (or the allow-list AllowedOrigins); answers go only to that origin, never to *. The SDK additionally uses a start handshake (?embed=sdk: the editor sends hello, the SDK answers init with headers, read-only mode, design and records before the first API call) and the commands setReadOnly, setRequestHeaders, getSelection, setValidation, validate with the events selectionChange, validation and error. The live editor on this site uses exactly this protocol.
12Design document schema
A design is a JSON document with five parts. Only label is required; the editor writes only values that differ from their default.
{
"schemaVersion": 4,
"label": { "unit": "Mm", "width": 100, "height": 50, "items": [ … ] },
"editor": { "version": 1, "dpi": 203, "items": { "ean": { "locked": true } }, "groups": [] },
"records": [ { "name": "Anna", "metadata": { "promo": { "isRendered": false } } } ],
"uiSettings": { "zoom": 120, "tab": "pdf" }
}
| Part | Content | Affects the print |
|---|---|---|
label | The zplCore label model (label.schema.json, schema version 4) – the same JSON the SDK reads with Label.FromJson | yes |
editor | State of the designer, own version editor.version (missing = 1) | partly (see below) |
records | Sample records; field names are the binding names; reserved metadata[itemName].isRendered = false hides an element for that record | yes (hiding) |
uiSettings | Optional UI state: vsplit, split, theme, zoom, tab, gridSize, snapDistance | no |
The editor part
| Property | Type · default | Meaning |
|---|---|---|
version | integer · 1 | Version of the editor part |
dpi | integer · 203 | Resolution of the preview; fallback when no printer dpi is given – affects the ZPL |
debugLabel | boolean · false | Debug label (frame, centre cross, 10 mm marks, timestamp) – affects the ZPL |
cutAfterLastLabelMode | "delayed" | "cutter" | How label.cutAfterLastLabel is printed – affects the ZPL |
grid | { enabled } | Grid on/off (further properties allowed) |
items[name] | object | Per element (key = label.items[].name, case-sensitive): locked, groupId, gs1 (elements[{ai, value}], encoding bracket / encoded / digitalLink, domain), textFit (auto / clip / wrap / shrink), rfidLockApply and rfidPermaLockApply (affect the ZPL) |
groups | [{ id, name }] | Groups of the element list |
Compatibility promise
- Within a schema version only additive changes: new optional properties whose default keeps the previous print and behaviour.
- Renaming, removing, a new type, meaning or default ⇒ a new version (
label.schemaVersionoreditor.version) with a migration step; every version reads all older ones. - Unknown properties in
editorare kept and written back, so an older host never damages a newer design. At API boundaries the label is read strictly.
13Security and operations
- No user management of its own. NuGet: your authentication and policies. Container:
apikey,jwt(OIDC) or explicitlynone. - Embedding:
frame-ancestorsand CORS fromAllowedOrigins; postMessage answers only to the fixed host origin. - Inputs: size limits per request, render slots with queue and time limit, optional rate limit per IP; SVGs are sanitised (no scripts, no external references), the server never fetches URLs, ZPL elements are only rendered.
- No storage, no telemetry: the server keeps nothing between requests (only caches of measured geometry); no phone-home, no CDN – the UI loads every file from your host.
- Scaling: stateless, so any number of instances without sticky sessions. Native dependencies (SkiaSharp, HarfBuzz, PDFium, ZXing) support Linux x64/arm64 (glibc), Windows and macOS.
- Monitoring: health
{prefix}/healthz(container also/healthz,/readyz), metrics meterZplCore.Designer(zplcore.designer.requests,zplcore.designer.duration,zplcore.designer.records), OpenTelemetry export (OTEL_EXPORTER_OTLP_ENDPOINT) and structured JSON logs in the container. - Container: distroless base (Azure Linux), non-root user
app, read-only root file system with a writable/tmp; Helm chart with dropped capabilities, seccomp and optional network policy. - Privacy: designs and records stay in your application; the editor's draft lives only in the user's browser and can be switched off (
autosave=0).
14Licensing
The zplCore Designer is part of the commercial zplCore.com product family. Licence model and prices are not published yet; contact us for a trial version or an offer. The live editor on this site is free to try with demo limits.

