Skip to content

Ruby Quickstart

Terminal window
gem install labelzoom
gem "labelzoom", "~> 1.0"

Ruby 3.1+, no runtime dependenciesnet/http, json, uri and openssl are all standard library, so the gem drops into a Rails app that already pins its own HTTP stack without an argument.

An API key is optional — without one you get the free tier.

require "labelzoom"
client = LabelZoom::Client.new # reads LABELZOOM_API_KEY; anonymous if unset
result = client.convert(
:zpl, :pdf,
"^XA^FO50,50^ADN,36,20^FDLabelZoom^FS^XZ",
label: { width: 4, height: 6 }, # inches
dpi: 203
)
result.save("label.pdf")

result.bytes is authoritative — five of the eleven targets are binary. result.text decodes it with the response charset for the textual ones (zpl, xml, json).

Only options you actually set are sent, so a change to a server default reaches you without a gem upgrade.

Source, target and body are positional; everything else is a keyword argument, with nesting expressed as a hash rather than flattened into label_width:

client.convert(
:zpl, :png, zpl,
dpi: 300,
rotation: 90, # must be a multiple of 90
color_mode: "GRAYSCALE",
watermark: false, # an explicit false IS sent
label: { width: 4.0, height: 6.0 }, # inches
pdf: { conversion_mode: "IMAGE", page_number: 0 },
zpl: { commands_to_ignore: ["^PQ"], image_compression: "Z64" }
)

A misspelled key raises rather than vanishing. Ruby has no compiler to catch label: { widht: 4 }, and the server ignores keys it does not recognise, so a silent drop would hand you a wrong label and no signal.

explicit = LabelZoom::Client.new(api_key: "lz_live_...")
from_env = LabelZoom::Client.new # reads LABELZOOM_API_KEY
anonymous = LabelZoom::Client.new(api_key: nil) # ignores the env

Omitting api_key: and passing nil mean different things on purpose: omitting it consults the environment, nil (or "") suppresses that fallback. The client is stateless once constructed — create one per application, not one per request.

result = client.convert(
:pdf, :zpl,
File.binread("shipping-label.pdf"),
label: { width: 4, height: 6 },
pdf: { page_number: 0 } # 0-based; omit for every page
)
zpl = result.text

:jpg and :url are source-only on the server. Ruby has no compile step, so the SDK enforces it at the call: passing either as a target raises LabelZoom::ValidationError before any request goes out.

The printer languages :epl, :tspl and :dpl are targets — :pdf:epl is a real conversion. Read result.bytes rather than result.text for those: EPL’s GW and TSPL’s BITMAP commands inline raw binary.

Each record produces one label:

result = client.convert(
:zpl, :pdf, template,
data: [
{ name: "ACME Corp", sku: "12345" },
{ name: "Globex", sku: "67890" }
]
) # a 2-page PDF, with an API key

The free tier returns the first label only, so this is a 1-page PDF until a key is configured. A single hash may be passed on its own — it is wrapped rather than rejected.

Every non-2xx raises a typed error carrying the status, the message, the raw body, and the X-LZ-Request-Id support handle:

begin
result = client.convert(:zpl, :json, zpl)
rescue LabelZoom::ForbiddenError => e
# "JSON export is a paid feature" — the most common free-tier refusal.
warn "paywall" if e.paid_feature?
rescue LabelZoom::APIError => e
warn "request #{e.request_id} failed with #{e.status}: #{e.message}"
end

BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError, PayloadTooLargeError, RateLimitedError and ServerError all descend from LabelZoom::APIError, so one rescue catches the lot.

LabelZoom::ValidationError deliberately does not — it descends from ArgumentError. It reports a request rejected locally, before any network call, which is a bug in the calling code rather than a server response, so rescuing APIError for fallback behaviour will not swallow it.

429, 5xx and transport failures are retried automatically — 3 attempts, 1s/2s/4s with jitter, honouring a longer Retry-After. Other 4xx responses raise immediately.

The sleeper and the environment lookup are both injectable, so a test never sleeps and never picks up a developer’s real key:

slept = []
client = LabelZoom::Client.new(
sleeper: ->(seconds) { slept << seconds },
jitter: false,
env: {}
)

Stub HTTP with WebMock, which is what the gem’s own suite uses — it intercepts at the Net::HTTP layer, so the real request-construction path is exercised rather than bypassed.

The SDK is a wrapper over one HTTP endpoint; net/http works just as well.

require "json"
require "net/http"
require "uri"
zpl = "^XA^FO50,50^ADN,36,20^FDLabelZoom^FS^XZ"
uri = URI("https://api.labelzoom.com/api/v2/convert/zpl/to/pdf")
uri.query = URI.encode_www_form(
"params" => JSON.generate({ "label" => { "width" => 4, "height" => 6 }, "dpi" => 203 })
)
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "text/plain"
request["Accept"] = "*/*"
api_key = ENV["LABELZOOM_API_KEY"]
request["Authorization"] = "Bearer #{api_key}" if api_key && !api_key.empty?
request.body = zpl
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
raise "LabelZoom returned #{response.code}: #{response.body}" unless response.code == "200"
File.binwrite("label.pdf", response.body)

Two things to watch when calling the API by hand, both of which the SDK handles for you:

  • Accept: */* is the safe default. Naming the target’s exact media type works, but */* is the one value valid for every target against every deployed server version — older servers answer an exact image/gif, image/bmp or image/jpeg with a 406.
  • Prefer a single params object. Dot notation carries JSON as well — ?label.width=4&dpi=203 and ?data=[{"sku":"A-100"}] both work — but one serialization path with no per-field exceptions is the habit that keeps working as you add options.

Note File.binwrite, not File.write — a PDF is binary, and writing it in text mode corrupts it on a non-UTF-8 default external encoding.