Skip to content

MuleSoft integration

MuleSoft sits in exactly the systems that produce and consume barcode labels: a WMS that emits ZPL, an ERP that emits a PDF, a TMS that needs one turned into the other. There is no LabelZoom connector on Anypoint Exchange — you don’t need one. The API is a single POST with the document as the request body, which the stock HTTP Request connector handles directly.

This guide covers the conversion API and the Cloud Print API from a Mule 4 flow.

  • Mule runtime 4.4 or later, Anypoint Studio 7.x
  • HTTP Connector 1.7+ (bundled with Studio)
  • Optionally, a LabelZoom API key — see Authentication

An API key is optional. Without one you get the free tier, watermarked, so every snippet below runs as-is. Add the key when you’re ready to ship.


One request config, reused by every flow:

<http:request-config name="LabelZoom_Config" responseTimeout="60000">
<http:request-connection protocol="HTTPS" host="api.labelzoom.com" port="443"
maxConnections="8"/>
</http:request-config>

maxConnections matters more than usual here; see Rate limits.

Keep the key out of the flow XML. Put it in a secure properties file and read it with p():

<secure-properties:config name="Secure_Props" file="config-${mule.env}.yaml"
key="${encryption.key}"/>
config-prod.yaml
labelzoom:
apiKey: "![encrypted...]"

<flow name="zpl-to-pdf">
<http:listener config-ref="HTTP_Listener_Config" path="/label/pdf"/>
<http:request method="POST" path="/api/v2/convert/zpl/to/pdf"
config-ref="LabelZoom_Config"
requestStreamingMode="NEVER"
outputMimeType="application/octet-stream">
<http:body><![CDATA[#[payload]]]></http:body>
<http:headers><![CDATA[#[output application/java
var apiKey = p('secure::labelzoom.apiKey') default ''
---
{
"Content-Type": "text/plain",
"Accept": "*/*",
"User-Agent": "labelzoom-mulesoft/1.0",
("Authorization": "Bearer " ++ apiKey) if (apiKey != '')
}]]]></http:headers>
</http:request>
<file:write path="label.pdf"/>
</flow>

POST ZPL to http://localhost:8081/label/pdf and label.pdf lands next to your app. The response body is the PDF — there is no JSON envelope to unwrap.

Accept: */* is the safe default. Naming the target’s exact media type works, but */* is the one value valid for all eleven targets against every deployed server version — older servers reject an exact image/gif, image/bmp or image/jpeg with a 406 from content negotiation, before the handler ever runs, and Studio will cheerfully set one for you if you let it infer the type.

Content-Type must be the source format’s exact media typetext/plain for the printer languages, application/pdf for PDF, image/png for PNG, and so on. See the table below. Anything else is a 400.

Set a deliberate User-Agent. It must not begin with LabelZoomStudio/ — the server reads that prefix as a backwards-compatibility signal for field-deployed LabelZoom Studio clients and adjusts conversion defaults accordingly. Your integration is not Studio; don’t claim to be.

requestStreamingMode="NEVER" makes Mule send a real Content-Length instead of chunking. The gateway rejects a body-less request outright, and chunked transfer from a lazily-evaluated payload is the usual way to end up sending one by accident.


Every option — DPI, rotation, label size, variable data — travels in one params query parameter holding URL-encoded JSON. Build it in DataWeave and let the connector do the encoding:

<http:query-params><![CDATA[#[output application/java
---
{
"params": write({ dpi: 300, rotation: 90 }, "application/json")
}]]]></http:query-params>

Prefer a single params object over dot notation. The server also accepts ?dpi=300&label.width=4, and it is tempting in Mule because it maps cleanly onto a query-params map. It works — JSON values such as ?data=[{},{}] included — but one serialization path with no per-field exceptions is the version that keeps working as you add options, and it behaves the same against every deployed server version.

Two rules that change how you build that object:

  • Send only what you actually set. Don’t hardcode dpi: 203 into a shared sub-flow. It’s already the server default, and pinning it means you won’t inherit a future change.
  • Omitting label is a feature, not an oversight. There is no client-side 4 × 6 default. Leaving label out is what triggers server-side size detection from the source document; supplying it defeats that. Set label.width / label.height (in inches) only when you genuinely need to override the detected size.

The full parameter list — scaling, colorMode, darkness, position, pdf.*, zpl.* — is in Conversion parameters.


This is where a Mule integration earns its keep: one call turns a collection of records into a multi-page PDF. Each entry in the data array produces one label.

<flow name="picklist-to-labels">
<db:select config-ref="ERP_DB">
<db:sql>SELECT item_code, qty, location FROM pick_lines WHERE wave_id = :wave</db:sql>
</db:select>
<set-variable variableName="labelData" value="#[payload]"/>
<set-payload value="#[vars.zplTemplate]"/>
<http:request method="POST" path="/api/v2/convert/zpl/to/pdf"
config-ref="LabelZoom_Config"
requestStreamingMode="NEVER"
outputMimeType="application/octet-stream">
<http:body><![CDATA[#[payload]]]></http:body>
<http:query-params><![CDATA[#[output application/java
---
{
"params": write({
data: vars.labelData map {
sku: $.item_code,
qty: $.qty as String,
loc: $.location
}
}, "application/json")
}]]]></http:query-params>
<http:headers><![CDATA[#[output application/java
var apiKey = p('secure::labelzoom.apiKey') default ''
---
{
"Content-Type": "text/plain",
"Accept": "*/*",
"User-Agent": "labelzoom-mulesoft/1.0",
("Authorization": "Bearer " ++ apiKey) if (apiKey != '')
}]]]></http:headers>
</http:request>
</flow>

Twelve pick lines in, a twelve-page PDF out, in one request. data is always an array of objects even for a single label.


Same shape, different path and content type:

<file:read path="shipping-label.pdf" outputMimeType="application/pdf"/>
<http:request method="POST" path="/api/v2/convert/pdf/to/zpl"
config-ref="LabelZoom_Config"
requestStreamingMode="NEVER"
outputMimeType="application/octet-stream">
<http:body><![CDATA[#[payload]]]></http:body>
<http:query-params><![CDATA[#[output application/java
---
{ "params": write({ pdf: { pageNumber: 0 } }, "application/json") }]]]></http:query-params>
<http:headers><![CDATA[#[output application/java
---
{ "Content-Type": "application/pdf", "Accept": "*/*",
"User-Agent": "labelzoom-mulesoft/1.0" }]]]></http:headers>
</http:request>

pdf.pageNumber is 0-based; omit it to convert every page. pdf.conversionMode is IMAGE by default, which rasterizes each page and preserves appearance exactly; NATIVE extracts the underlying vector and text content instead.

If your payload is already base64 — a common shape when the PDF arrived as a field in an upstream JSON or SOAP message — you don’t have to decode it. Every image and PDF source accepts a base64 body sent as Content-Type: text/plain. Send the string as-is and change the one header.


Set outputMimeType="application/octet-stream" on the request, as every snippet above does. Mule will otherwise coerce the response toward a String based on the response content type, and several targets do not survive that.

Source format Request Content-Type Target format Response type
zpl epl tspl dpl text/plain zpl epl tspl dpl text/plain
xml application/xml xml application/xml
json application/json json application/json
pdf application/pdf pdf application/pdf
png image/png png image/png
bmp image/bmp bmp image/bmp
gif image/gif gif image/gif
jpeg / jpg image/jpeg jpeg image/jpeg
url text/plain (body is the URL)

Image and PDF sources also accept a base64 body as text/plain. jpg and url are source-only; there is no url target.

One more surprise worth designing around: not every target returns every label. PDF and the printer languages include all of them (as pages, or concatenated). png, bmp, gif, jpeg and xml return only the first. On the free tier, only the first label renders normally regardless of target — the rest are replaced with a watermark label.


By default the HTTP Request connector throws on any non-2xx status, and Mule’s error-type mapping doesn’t cover every status LabelZoom returns. Widening the success validator and branching explicitly gives you the full picture:

<http:request method="POST" path="/api/v2/convert/zpl/to/pdf" config-ref="LabelZoom_Config">
<!-- ... body, headers, query-params ... -->
<http:response-validator>
<http:success-status-code-validator values="200..299,400..599"/>
</http:response-validator>
</http:request>
<choice>
<when expression="#[attributes.statusCode == 200]">
<file:write path="label.pdf"/>
</when>
<otherwise>
<logger level="ERROR" message="#[
'LabelZoom ' ++ (attributes.statusCode as String)
++ ' reqId=' ++ (attributes.headers['x-lz-request-id'] default 'none')
++ ' body=' ++ (payload as String)]"/>
<raise-error type="APP:LABEL_CONVERSION_FAILED"/>
</otherwise>
</choice>
Status What it means here Retry?
400 The label or file didn’t parse, or Content-Type doesn’t match the source No
401 Malformed Authorization header No
403 Valid key, but the plan doesn’t allow this path — the message matches paid feature No
406 You sent an exact Accept. Send */* No
413 Body over 1 MB on the free tier No
429 Rate limited; honor Retry-After Yes
5xx Transient Yes

If you prefer the default throwing behavior, the reliably-mapped error types are HTTP:UNAUTHORIZED, HTTP:FORBIDDEN, HTTP:TOO_MANY_REQUESTS, HTTP:INTERNAL_SERVER_ERROR, HTTP:CONNECTIVITY and HTTP:TIMEOUT.

Retry only 429, 5xx, and connectivity failures. Retrying a malformed label fails forever, three times as slowly:

<until-successful maxRetries="3" millisBetweenRetries="1000">
<try>
<http:request .../>
<error-handler>
<on-error-continue type="HTTP:BAD_REQUEST, HTTP:FORBIDDEN, HTTP:UNAUTHORIZED"/>
</error-handler>
</try>
</until-successful>

Log x-lz-request-id on every path, success and failure. It’s the support handle — the format looks like 2026/08/15/120000--3f2b8c1e-…. Read it lowercase from attributes.headers; Mule normalizes response header names, but the header is spelled inconsistently across the gateway’s own surfaces, so don’t match on case.


The Cloud Print API pushes a label to a printer connected through the LabelZoom Print Agent — no VPN, no port 9100 reachability from CloudHub.

<http:request method="POST" path="/api/v3/printers/{printerId}/print"
config-ref="LabelZoom_Config"
requestStreamingMode="NEVER">
<http:body><![CDATA[#[payload]]]></http:body>
<http:uri-params><![CDATA[#[{ printerId: p('labelzoom.printerId') }]]]></http:uri-params>
<http:query-params><![CDATA[#[{ sourceFormat: 'zpl' }]]]></http:query-params>
<http:headers><![CDATA[#[output application/java
---
{
"Content-Type": "text/plain",
"Accept": "*/*",
"Authorization": "Bearer " ++ p('secure::labelzoom.apiKey'),
"Idempotency-Key": correlationId
}]]]></http:headers>
</http:request>

The Idempotency-Key header is not optional in a Mule app. Wrap that request in <until-successful> without one and a timeout on a print that actually succeeded reprints the label. Repeating a key returns the original job instead. correlationId is a natural source; uuid() works too.

sourceFormat is required for the printer languages. ZPL, EPL, TSPL and DPL all arrive as text/plain, so the gateway can’t tell them apart from the content type alone. PDF and images are detected from Content-Type (or sniffed from magic bytes), so sourceFormat is optional there.

Format conversion is automatic. Send a PDF to a printer configured for ZPL and it’s converted on the way. Any conversion parameter also works as a query parameter on this endpoint — rotation=90, dpi=300 — and is applied in transit.

The response is JSON:

{ "jobId": "9f2c…", "status": "dispatched" }

dispatched means it reached an online agent. queued means every agent bound to that printer is offline — the job is held and drains when one reconnects. Treat it as accepted, not as failed. Poll GET /api/v3/jobs/{jobId} for progress: queued, dispatched, printing, completed, failed.

Path-specific errors: 403 the printer isn’t yours, 404 no such printer, 502 the document couldn’t be converted to the printer’s native format.


The conversion endpoint treats an unresolvable credential as anonymous rather than returning 401. A typo’d key therefore returns a perfectly healthy 200 — carrying a watermarked free-tier label. Don’t validate your configuration by checking the status code. Convert one label during setup and look at it.

Keys created from the dashboard are always test keys (lz_test_ prefix): they work everywhere, and they are always watermarked and never billed. Production keys (lz_live_) are provisioned by sales/support on Pro and above. A Mule app that “works fine but watermarks in prod” is nearly always this, not a code problem.

The gateway allows 100 requests per 60 seconds, counted against your IP and separately against your credential. The credential bucket keys on the API key string itself — so every worker in a CloudHub cluster sharing one key shares one bucket, and horizontal scaling does not buy throughput. Size maxConnections and the concurrency of any <parallel-foreach> or scatter-gather with that ceiling in mind, and handle 429 with backoff rather than designing around never seeing one. Monthly volume limits are per plan — see Rate limits and plans.

Keep the key in a secure properties file or Anypoint Secrets Manager. It’s a bearer credential: anyone holding it can spend your quota.