Skip to content

MuleSoft integration

Call the LabelZoom conversion and Cloud Print APIs from a Mule 4 flow with the standard HTTP Request connector. There’s no LabelZoom connector on Anypoint Exchange.

  • Mule runtime 4.4 or later, and Anypoint Studio 7.x.
  • HTTP Connector 1.7 or later, which is bundled with Studio.
  • Optional: a LabelZoom API key. Without one, conversions run on the free tier. Printing needs a key with the print scope; see Authentication.

To set up a connection that every flow shares, follow these steps:

  1. Add an HTTP request config:

    <http:request-config name="LabelZoom_Config" responseTimeout="60000">
    <http:request-connection protocol="HTTPS" host="api.labelzoom.com" port="443"
    maxConnections="8"/>
    </http:request-config>
  2. Store the API key in a secure properties file, not in the flow XML:

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

Flows read the key with p('secure::labelzoom.apiKey'). Every conversion request follows these rules:

Setting Value
Accept header */*
Content-Type header The source format’s media type, such as text/plain for printer languages or application/pdf for PDF. A mismatch returns 400. See Supported formats.
requestStreamingMode NEVER. Chunked transfer can send a request with no body, which the API rejects.
outputMimeType application/octet-stream, so Mule keeps the response as bytes.
Options One params query parameter that holds JSON, built with DataWeave write().

This flow takes ZPL from an HTTP listener and writes label.pdf:

<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": "*/*",
("Authorization": "Bearer " ++ apiKey) if (apiKey != '')
}]]]></http:headers>
</http:request>
<file:write path="label.pdf"/>
</flow>

The response body is the PDF itself. To set options, add a params query parameter to the request:

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

Omit label to render at the label’s own size. For every option, see Conversion parameters.

Use the same request shape with the PDF 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": "*/*" }]]]></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 keeps its appearance. NATIVE extracts the vector and text content.
  • If the PDF arrives as a base64 string, send the string as the body with Content-Type: text/plain.

Write printer-language output as bytes, because decoding it to a String can corrupt labels with graphics; see Binary output.

Each object in the data array fills one label, so one call turns a record set into a batch of ZPL labels. data is always an array of objects, even for one label. This flow fills a ZPL template that has the merge fields sku, qty, and loc:

<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:input-parameters><![CDATA[#[{ wave: vars.waveId }]]]></db:input-parameters>
</db:select>
<set-variable variableName="labelData" value="#[payload]"/>
<file:read path="pick-label.zpl"/>
<http:request method="POST" path="/api/v2/convert/zpl/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({
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": "*/*",
("Authorization": "Bearer " ++ apiKey) if (apiKey != '')
}]]]></http:headers>
</http:request>
<file:write path="pick-labels.zpl"/>
</flow>

By default, the HTTP Request connector throws on any status outside 2xx. To log the API’s error message and request ID, accept every status and branch on it:

<http:request method="POST" path="/api/v2/convert/zpl/to/pdf" config-ref="LabelZoom_Config">
<!-- body, headers, and query-params as in the earlier examples -->
<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>

For what each status means, see Status codes. If you keep the default throwing behavior, these error types map reliably: HTTP:UNAUTHORIZED, HTTP:FORBIDDEN, HTTP:TOO_MANY_REQUESTS, HTTP:INTERNAL_SERVER_ERROR, HTTP:CONNECTIVITY, and HTTP:TIMEOUT.

Retry only 429, 5xx, and connectivity failures, because a bad request fails the same way every time:

<until-successful maxRetries="3" millisBetweenRetries="1000">
<try>
<http:request method="POST" path="/api/v2/convert/zpl/to/pdf" config-ref="LabelZoom_Config">
<!-- body, headers, and query-params as in the earlier examples -->
</http:request>
<error-handler>
<on-error-continue type="HTTP:BAD_REQUEST, HTTP:FORBIDDEN, HTTP:UNAUTHORIZED"/>
</error-handler>
</try>
</until-successful>

Log the x-lz-request-id response header on every conversion request, and include it when you contact support.

The Cloud Print API sends a document to a printer connected through the LabelZoom Print Agent, so CloudHub doesn’t need network access to the printer:

<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>
  • Idempotency-Key makes a retry safe: the same key with the same request returns the original job instead of printing again. Reusing a key for a different request returns 422. See Idempotency.
  • sourceFormat is required for printer languages, which all arrive as text/plain. PDF and images are detected from Content-Type.
  • If the document isn’t in the printer’s native format, LabelZoom converts it in transit. Conversion parameters also work on this endpoint, in the same params query parameter.

The response is JSON:

{ "jobId": "9f2c4e1a-6b7d-4c3e-8a21-5d0f7b9e2c41", "status": "dispatched", "copies": null }

queued also means accepted: no agent is online, and the job prints when one reconnects. To track a job, call GET /api/v3/jobs/{jobId}; see Job statuses. On this path, 403 means the key lacks the print scope or the printer isn’t yours, 404 means the printer doesn’t exist, and 502 means the document couldn’t be converted to the printer’s format.

A key with the print scope can also list printers with GET /api/v3/printers and read printer status and jobs. Manage agents and printers, and retry failed jobs, on the Printers page.

To print a template designed in LabelZoom Studio, send its values as a JSON object:

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

The response is 202 with the same job fields as a raw print. With validate=true, a missing merge field returns 422 instead of printing the text null. To find template IDs, call GET /api/v3/templates. To print several labels in one request, send an array of objects; see Templates.

Symptom Fix
400 because the content type doesn’t match Send the source format’s media type in Content-Type, such as text/plain for ZPL.
400 because the request body is missing Set requestStreamingMode="NEVER" on the request.
406 Accept names a media type the API never returns. Send Accept: */*.
The output has a watermark The key is missing or invalid, or the account has no paid plan; see Authentication troubleshooting.
422 with idempotency_key_reused Send a new Idempotency-Key for each different print request.
429 Wait for the number of seconds in Retry-After, and then retry; see Rate limits.
  • Use a live key, and keep it in secure properties or Anypoint Secrets Manager; see Authentication.
  • A bad key converts anonymously. Check your first output for a watermark; see Authentication troubleshooting.
  • Every CloudHub worker that uses a key shares its rate limit, so size maxConnections and parallel processing for one budget; see Rate limits.
  • Retry only 429, 5xx, and connectivity failures; see Retry failed requests.
  • Send a new Idempotency-Key for each deliberate reprint; see Idempotency.