Skip to content

Merge fields

A merge field is a named slot on a label. Declare it once in the template, then supply a value on every conversion or print call — one design prints thousands of different labels.

A merge field lives in the text content of a variableText, barcode, or addressBlock component in LabelZoom XML. The content is an expression: a double-quoted string literal in which ${name} marks a placeholder.

<component xsi:type="variableText" x="20" y="30" fontSize="10"><![CDATA["${sku}"]]></component>
<component xsi:type="variableText" x="20" y="60" fontSize="10"><![CDATA["Qty: ${qty} of ${total}"]]></component>
<component xsi:type="barcode" x="20" y="90" barcodeStyle="Code128"><![CDATA["{{sscc18}}"]]></component>

Placeholders mix freely with literal text — that’s how you concatenate — and {{name}} is an alias for ${name}. A quoted literal with no placeholder is valid too; it just prints itself.

Three rules:

  • The double quotes are required. Without them the expression is rejected and the fields render blank.
  • Wrap the content in <![CDATA[ ... ]]> so quotes and braces survive XML parsing.
  • Only \n and \" are escapes. Any other backslash stays a backslash.

Address blocks are the one exception to the ${...} form — see Address blocks. Everything here describes schemaVersion v2; v1 used a bare unquoted field name and is still read, but author new templates as v2.

The name is taken literally and trimmed. Anything but a brace is allowed, so dots, spaces, and hyphens are legal characters in a name:

Placeholder JSON key it reads
${sku} sku
${Ship-To} Ship-To
${ line qty } line qty
${order.id} order.id

That last row surprises people: a dot is not a path. ${order.id} reads one top-level key whose name contains a dot — it never descends into a nested order object. Flatten your data first.

Fields inserted from the Studio or designer UI are limited to letters, digits, and underscores. That’s the UI’s rule, not the engine’s, but staying inside it keeps a template portable.

The syntax resembles a scripting language but isn’t one. Filling is a single pass of plain name lookup, and everything between the braces is one field name:

Not supported Read as the field name
Nested paths — ${order.items.sku} order.items.sku
Array indexing — ${items[0]} items[0]
Arithmetic — ${qty * price} qty * price
Functions — ${upper(sku)}, ${today()} upper(sku)
Defaults or fallbacks — ${sku ?: 'N/A'} sku ?: 'N/A' — you get null, never the fallback
Date/number formatting, padding, substring Not available

Each unsupported form prints the literal text null, because no key by that name exists in the payload. Two more limits:

  • Concatenation works only by embedding placeholders in literal text — "${first} ${last}". A + operator doesn’t exist, and "PO " + ${po} is rejected because the expression no longer ends in a quote.
  • Substitution runs once, so a value containing ${...} lands on the label verbatim.

Do the computing in your application and send finished strings.

A placeholder naming a key you didn’t send is replaced with the four literal characters null — not a blank, and not an error. Template "Qty: ${qty}" with data {"sku":"A-100"} prints Qty: null.

Send every field the template declares, even when empty: {"qty":""} prints Qty: as intended. For published templates, ?validate=true enforces this server-side.

Two names are always available without being in your payload:

Name Value
${LABEL_NUMBER} Index of the current label in the batch, starting at 0
${LABEL_COUNT} Total number of labels in the batch

"Label ${LABEL_NUMBER} of ${LABEL_COUNT}" over two entries prints Label 0 of 2, then Label 1 of 2. Add your own field for a 1-based counter.

${...} is LabelZoom syntax, not ZPL — a Zebra printer won’t understand it in a ^FD. Instead, LabelZoom recognizes the two placeholder conventions WMS and ERP systems already emit into ZPL and converts them as it reads the label:

^FO50,50^A0N,30,30^FDPO Number: ~cponum,20~^FS
^FO50,100^A0N,30,30^FD{BFFNAME}^FS

~name,len~ gives a name and the field’s maximum length; {NAME} is a bare braced name. Those become merge fields cponum and BFFNAME. Field data with neither pattern is static text and ignores whatever you send.

Two ZPL features look like merge fields and aren’t:

  • ^FN doesn’t create one. A ^DF format recalled with ^XF resolves its ^FN values while the label is read, giving a field fixed for every copy.
  • ^SN isn’t incremented. Only its starting value is read, printing as static data. Generate sequences yourself, or use ${LABEL_NUMBER}.

Merge fields are derived from the label, never declared separately — no field list to maintain, no “is this variable?” switch. Place a text or barcode element and type literal text with ${field} tokens in it.

In LabelZoom Studio, the Expression Editor offers Static Text, Variable Text (type or pick a field name), and Dynamic Expression for the raw form.

Studio’s Expression Editor, with Variable Text selected and a field name entered

In the web designer, type into the Content box on the property panel or use Insert field; the Data tab lists every field the label uses.

Publishing scans the label, and the names it finds become the template’s integration contract — shown at publish time and returned as mergeFields. Your payload keys must match that list.

Studio’s Publish Template dialog, listing the merge fields derived from the label

Endpoints differ in what they accept, which is the most common source of a confusing 400:

Endpoint Accepts One label per
POST /api/v2/convert/{source}/to/{target} a JSON array only array entry
POST /api/v2/labels/{labelId}/generate a JSON array only array entry
POST /api/v3/templates/{id}/convert an object or non-empty array of objects object
POST /api/v3/printers/{printerId}/templates/{templateId}/print an object or non-empty array of objects object

An object or scalar where an array is required returns 400 params.data must be a JSON array, with one entry per output label.

Send the template as the request body and the values in data:

Terminal window
curl -X POST "https://api.labelzoom.com/api/v2/convert/zpl/to/pdf" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: text/plain" \
--data-binary @template.zpl \
--url-query 'params={"data":[{"sku":"A-100"},{"sku":"A-101"}]}' \
--output labels.pdf

data also works as a flat query parameter, ?data=[{"sku":"A-100"}], which wins where the two overlap — though the official SDKs always emit the single params object, and that’s the form to prefer.

Omitting data doesn’t leave placeholders intact: the label is still filled, with one empty record, so everything resolves to null.

See Conversion parameters for the rest of params and the --url-query caveat, and Multi-label jobs for per-format batch behavior.

For a label stored in your account, reference it by id and put the data in the body:

Terminal window
curl -X POST "https://api.labelzoom.com/api/v2/labels/{labelId}/generate?copies=2&dpi=300" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '[{"sku":"A-100","qty":"12"}]' \
--output label.zpl
Parameter Default Notes
format zpl Printer language to generate
copies 1 Adds a print-quantity command to the output; minimum 1
dpi 203 Minimum 1

Content-Type must be application/json or the call returns 415. The response is the printer data stream as text/plain.

Once a design is published you never send label code again — only values. Both endpoints take one object for a single label or an array for a batch, and the response shape mirrors the request.

Render a filled template without printing it:

Terminal window
curl -X POST "https://api.labelzoom.com/api/v3/templates/{templateId}/convert?target=pdf" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"orderNumber":"SO-1001","sku":"A-100"}' \
--output label.pdf

An object returns raw label bytes with the format echoed in an X-LZ-Format header, and ?target= overrides the template’s default. An array returns JSON, so one response carries the batch:

{
"labels": [
{ "format": "pdf", "contentType": "application/pdf", "base64": "JVBERi0..." },
{ "format": "pdf", "contentType": "application/pdf", "base64": "JVBERi0..." }
]
}

Print a filled template to one of your cloud printers:

Terminal window
curl -X POST "https://api.labelzoom.com/api/v3/printers/{printerId}/templates/{templateId}/print?validate=true" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: SO-1001" \
-d '{"orderNumber":"SO-1001","sku":"A-100"}'
{ "jobId": "9f1c7e2a-4b30-4d6e-8a11-5c2f9d4e7b83", "status": "dispatched", "copies": null }

The call returns 202. An array returns { "jobs": [ ... ] }, each job deriving its own idempotency key from yours, so a batch stays safe to retry whole. Reusing a key with different merge data, a different printer or template, or a different copies is refused with 422 rather than replayed — see Idempotency. The target format comes from the printer, so one template feeds a ZPL printer and a PDF-capable one unchanged.

Print several copies by adding copies, a whole number from 1 to 9999, which sends the job to the printer that many times. To print 25 price tags for one product:

Terminal window
curl -X POST "https://api.labelzoom.com/api/v3/printers/{printerId}/templates/{templateId}/print?validate=true&copies=25" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sku":"A-100","description":"Widget, blue","price":"4.99"}'
{ "jobId": "4e8b1d6f-2a9c-4f3e-b7d0-6c1a5e9f8b24", "status": "dispatched", "copies": 25 }

The tag prints 25 times as one job. A print quantity the template was designed with multiplies — a template that prints 2 of each label, sent with copies=25, prints 50 — so design a template meant for copies to print one. Leaving copies off sends the job once, and the response then reports "copies": null. In a batch, every entry’s job is sent that many times. See Printing copies for the details, including the 422 returned when a Print Agent needs updating.

Leave ?validate=true on. It checks each payload against the template’s merge fields before rendering, returning 422 instead of printing a label with null on it — index identifies the entry in a batch:

{
"message": "Payload is missing required fields",
"problems": [{ "index": 0, "missing": ["sku"] }]
}

An addressBlock formats a postal address into the right number of lines for its country, so it takes a whole address rather than one string. Its content is a bare field name — no quotes, no ${}:

<component xsi:type="addressBlock" x="20" y="200" fontSize="10"
useLocalizedAddressFormat="true" showContactName="true"><![CDATA[ship_to]]></component>

The matching key holds a nested object. All twelve keys are optional; anything else is ignored:

[
{
"sku": "A-100",
"ship_to": {
"name": "ACME Corp",
"honorific": "Ms.",
"first_name": "Dana",
"last_name": "Reyes",
"line1": "1 Main St",
"line2": "Suite 400",
"line3": "",
"district": "",
"city": "Austin",
"state": "TX",
"zip": "78701",
"country": "US"
}
}
]

Nested objects mean nothing outside an address block: read by an ordinary ${...} placeholder one yields an internal identifier, and an array value flattens to an empty string.

Symptom Cause
Every variable field and barcode is blank The expressions aren’t valid — usually missing the surrounding double quotes. Filling is skipped.
A field prints null No key of that name was in the payload. Check spelling and case, and remember a dot is part of the name.
A field prints null, and its name contains ?: or an operator Fallbacks and expressions aren’t evaluated; the whole body is one field name.
400 params.data must be a JSON array An object or scalar was sent where an array is required.
400 Request body is required but Content-Length was 0 The label was moved into the query string, usually by curl’s --get. Use --url-query.
415 from /api/v2/labels/{id}/generate Content-Type wasn’t application/json.
422 Payload is missing required fields validate=true caught a missing key. The problems array names it.
422 with code idempotency_key_reused from a template print The Idempotency-Key was used before with different merge data, printer, template, or copies. Send a new key for a new print.
The data is ignored entirely The source is ZPL whose ^FD data has no ~name,len~ or {NAME} placeholder, so the fields are static.
Only one label came back from a batch Image and XML targets return only the first label. See Multi-label jobs.