LabelZoom Logo

LabelZoom Ruby SDK

Official Ruby client for the LabelZoom API. Converts barcode labels between ZPL, EPL, TSPL, DPL, PDF, LabelZoom XML/JSON, and raster images.

Ruby 3.1+. No runtime dependenciesnet/http, json, uri and openssl are all standard library.

Install

gem install labelzoom
gem "labelzoom", "~> 1.0"
Build from source
git clone https://github.com/labelzoom/labelzoom-sdk.git
cd labelzoom-sdk/ruby
bundle install && bundle exec rake

Quick start

An API key is optional. Without one you get the free tier — watermarked output, first label only, a 1 MB request cap, and no multi-page, JSON-target, or image-to-image conversion.

require "labelzoom"

client = LabelZoom::Client.new          # anonymous; this works

result = client.convert(:zpl, :png, "^XA^FO20,20^A0N,28^FDHello^FS^XZ",
                        dpi: 300,
                        label: { width: 4, height: 6 })

result.save("label.png")

With a credential:

client = LabelZoom::Client.new(api_key: "lz_live_...")

Passing nothing reads LABELZOOM_API_KEY from the environment. Passing api_key: nil — or an empty string — forces the free tier and suppresses that fallback.

Formats

Sources (13): :zpl :epl :tspl :dpl :xml :json :pdf :png :bmp :gif :jpeg :jpg :url

Targets (11): :zpl :epl :tspl :dpl :xml :json :pdf :png :bmp :gif :jpeg

:jpg is an input spelling that normalizes to jpeg on the wire, and :url tells the server to go fetch a document rather than naming a format — so neither is a target. The statically typed SDKs make that a compile error; Ruby raises LabelZoom::ValidationError (an ArgumentError) on the call, before any request goes out.

The printer languages round-trip: pdfepl and zpltspl are real conversions. Their output is text/plain, but EPL's GW and TSPL's BITMAP commands inline raw binary, so read result.bytes rather than result.text whenever a label might carry graphics.

Options

Options are keyword arguments with nested hashes, and only what you pass is sent — the SDK never fills in a client-side default, so a change to a server default reaches you without a gem upgrade.

client.convert(:zpl, :png, zpl,
               dpi: 300,                                  # server default 203
               rotation: 90,                              # must be a multiple of 90
               scaling: 75.0,                             # percent; server default 100
               color_mode: "GRAYSCALE",
               darkness: 60,                              # 0-100 luminance threshold
               position: { x: 5, y: 15 },
               watermark: false,                          # an explicit false IS sent
               label: { width: 4.0, height: 6.0 },
               pdf: { conversion_mode: "IMAGE", page_number: 0 },
               zpl: { commands_to_ignore: ["^PQ"], image_compression: "Z64" },
               data: [{ sku: "1234" }])

Two units are routinely misread and are pinned by the shared fixtures:

  • label: is in inches, not dots. Omit it entirely to have the server detect the size.
  • pdf: { page_number: } is 0-based. Omit it to convert every page.

data: is one record per output label; a single Hash is wrapped rather than rejected. extra: carries anything the SDK does not model yet — unknown keys are ignored server-side, so it is a safe forward-compatibility hatch.

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

Errors

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

begin
  client.convert(:zpl, :json, zpl)
rescue LabelZoom::ForbiddenError => e
  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 reports a request rejected locally, before any network call, which is a bug in the calling code rather than a server response. It descends from ArgumentError, so rescuing APIError to implement a fallback will not swallow it.

Retries

429s, 5xx responses and transport failures are retried automatically — twice by default, for three attempts — with a 1s/2s/4s backoff under full jitter. A Retry-After header is honoured on any retryable status when it asks for longer than the backoff would wait. No other 4xx is ever retried.

client = LabelZoom::Client.new(max_retries: 0)   # disable retrying

Testing your own code

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 this gem's own suite uses — it intercepts at the Net::HTTP layer, so the real request-construction path is exercised rather than bypassed.

Development

bundle install
bundle exec rake        # rubocop + rspec

The spec suite is the shared conformance fixtures in ../conformance/ — the same cases the .NET, Node, Java, Python, PHP and Go suites run — plus an assertion that it executed every one of them.

Ruby declares the two typecheck/* cases skipped in ../conformance/skips/ruby.json, because Ruby has no compile step. spec/labelzoom/formats_spec.rb is what makes that skip's stated reason true rather than merely convenient: it asserts the runtime guard the skip claims exists.

License

MIT — see LICENSE.