Skip to content

MuleSoft integration

There is no LabelZoom connector on Anypoint Exchange, and none is needed: 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, every request runs on the free tier and the output is watermarked, so every snippet below works 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>

Size maxConnections against your plan’s throughput cap — 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 converted file itself — there is no JSON envelope to unwrap.

Header Rule
Accept Accept: */* is the one value that needs no per-target logic. The target’s exact media type and a wildcard subtype (image/*) also work; charset and q parameters are ignored.
Content-Type Must be the source format’s exact media type — text/plain for the printer languages, application/pdf for PDF, image/png for PNG, and so on. Anything else returns 400. See the table below.
User-Agent Must not begin with LabelZoomStudio/. The server reads that prefix as a signal that the caller is a field-deployed LabelZoom Studio client and adjusts conversion defaults accordingly.

Two connector settings matter:

  • requestStreamingMode="NEVER" makes Mule send a real Content-Length instead of chunking. The API rejects a body-less request outright, and chunked transfer from a lazily-evaluated payload is the usual way to send one by accident.
  • outputMimeType="application/octet-stream" stops Mule from coercing the response toward a String based on the response content type — several targets do not survive that. See Response content types.

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>

The server also accepts flat dot-notation parameters (?dpi=300&label.width=4), and JSON values such as ?data=[{},{}] carry that way too. Where the two spellings overlap, the dot-notation value wins. Either is fine; one serialization path is simply easier to maintain.

Two rules for building the 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 triggers server-side size detection from the source document — there is no client-side 4 × 6 default. Set label.width / label.height (in inches) only when you need to override the detected size.

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

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>

data is always an array of objects, even for a single label.

Same request 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 defaults to IMAGE, 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.

The response body is the converted file. Keep it as bytes:

Source format Request Content-Type Target format Response type
zpl epl ipl tspl dpl sbpl text/plain zpl epl ipl tspl dpl sbpl 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) — —

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

By default the HTTP Request connector throws on any non-2xx status, and Mule’s error-type mapping doesn’t cover every status the API 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 Cause Retry?
400 The label or file didn’t parse, Content-Type doesn’t match the source, or Accept names a type this target can’t produce No
401 Malformed Authorization header No
403 Valid key, but the plan doesn’t allow this path — the message matches paid feature No
406 Accept names a media type the endpoint never produces (e.g. application/zip) 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 — a malformed label fails the same way on every retry:

<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 handle support asks for (it looks like 2026/08/15/120000--3f2b8c1e-…). Read it lowercase from attributes.headers: Mule normalizes response header names, and the header is spelled inconsistently across the API’s own surfaces, so don’t match on case. See Errors.

The Cloud Print API pushes a label to a printer connected through the LabelZoom Print Agent — no VPN, and 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>

Three things this flow relies on:

  • An Idempotency-Key header. Without one, wrapping the request in <until-successful> means a timeout on a print that actually succeeded reprints the label. Repeating a key with the same request returns the original job; reusing one for a different request (another document, printer, or copies) is refused with a 422 — see Idempotency. correlationId is a natural source; uuid() works too.
  • sourceFormat for the printer languages. ZPL, EPL, IPL, TSPL, DPL and SBPL all arrive as text/plain, so the API 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 happens in transit. A PDF sent to a printer configured for ZPL is converted on the way. Any conversion parameter also works as a query parameter on this endpoint — rotation=90, dpi=300.

The response is JSON:

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

dispatched means the job reached an online agent. queued means every agent bound to that printer is offline — the job is held and drains when one reconnects, so treat it as accepted, not 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.

Validate the key by converting, not by status code

Section titled “Validate the key by converting, not by status code”

The conversion endpoint treats an unresolvable credential as anonymous rather than returning 401, so a typo’d key returns a perfectly healthy 200 carrying a watermarked free-tier label. During setup, convert one label and look at it. See A bad API key does not fail.

Keys created from the dashboard are always test keys (lz_test_) — watermarked, never billed. Production keys (lz_live_) are provisioned by sales/support on Pro and above. An app that “works fine but watermarks in prod” is nearly always this. See Test keys and live keys.

The throughput cap is shared by everything using the same API key, so horizontal scaling does not buy throughput: every worker in a CloudHub cluster sharing one key draws on the same budget. Size maxConnections and the concurrency of any <parallel-foreach> or scatter-gather with that 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.