Go Quickstart
Install
Section titled “Install”go get github.com/labelzoom/labelzoom-sdk/goimport labelzoom "github.com/labelzoom/labelzoom-sdk/go"Go 1.23+, zero dependencies — standard library only, including the response-charset decoding.
The import path ends in /go because the module lives in the go/ directory of a
multi-language repository. Its release tags are go/vX.Y.Z for the same reason.
Convert ZPL to PDF
Section titled “Convert ZPL to PDF”An API key is optional — without one you get the free tier.
package main
import ( "context" "log"
labelzoom "github.com/labelzoom/labelzoom-sdk/go")
func main() { client, err := labelzoom.New() // reads LABELZOOM_API_KEY; anonymous if unset if err != nil { log.Fatal(err) }
result, err := client.Convert(context.Background(), labelzoom.ConvertRequest{ From: labelzoom.SourceZPL, To: labelzoom.TargetPDF, Body: []byte("^XA^FO50,50^ADN,36,20^FDLabelZoom^FS^XZ"), Options: &labelzoom.Options{ Label: &labelzoom.LabelSize{Width: labelzoom.Ptr(4.0), Height: labelzoom.Ptr(6.0)}, DPI: labelzoom.Ptr(203), }, }) if err != nil { log.Fatal(err) }
if err := result.Save("label.pdf"); err != nil { log.Fatal(err) }}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).
Why a request struct and not a chain
Section titled “Why a request struct and not a chain”The other LabelZoom SDKs expose client.convert().fromZpl(...).toPdf().execute(). A fluent
chain in Go has to either panic on a bad argument or hoard errors until a terminal Do(), and
both are un-Go — so this SDK uses functional options for the client and a request struct for the
call. The wire behaviour is identical; only the ergonomics differ.
Options are pointers on purpose
Section titled “Options are pointers on purpose”Every field of Options is a pointer, and only the ones you set are sent. The SDK never
substitutes a default of its own, so a change to a server default reaches you without an SDK
upgrade. labelzoom.Ptr is the constructor:
Options: &labelzoom.Options{ Watermark: labelzoom.Ptr(false), // an explicit false IS sent PDF: &labelzoom.PDFOptions{PageNumber: labelzoom.Ptr(0)},}Type inference follows the literal, so a float64 field wants Ptr(4.0), not Ptr(4).
Configuring the client
Section titled “Configuring the client”explicit, _ := labelzoom.New(labelzoom.WithAPIKey("lz_live_..."))fromEnv, _ := labelzoom.New() // reads LABELZOOM_API_KEYanonymous, _ := labelzoom.New(labelzoom.WithAnonymous()) // ignores the envNot calling WithAPIKey consults the environment; WithAPIKey("") — or the more readable
WithAnonymous() — suppresses that fallback. The client is safe for concurrent use; create one
per application, not one per request.
Converting a PDF to ZPL
Section titled “Converting a PDF to ZPL”pdf, err := os.ReadFile("shipping-label.pdf")if err != nil { return err}
result, err := client.Convert(ctx, labelzoom.ConvertRequest{ From: labelzoom.SourcePDF, To: labelzoom.TargetZPL, Body: pdf, Options: &labelzoom.Options{PDF: &labelzoom.PDFOptions{PageNumber: labelzoom.Ptr(0)}},})
zpl := result.Text()jpg and url are source-only on the server, and the type system says so: SourceFormat and
TargetFormat are distinct types with typed constants, so To: labelzoom.SourceURL does not
compile and there is no TargetURL to write.
The printer languages epl, tspl and dpl are targets — SourcePDF → TargetEPL is a
real conversion. Read result.Bytes rather than Text() for those: EPL’s GW and TSPL’s
BITMAP commands inline raw binary.
Filling variable fields
Section titled “Filling variable fields”Each record produces one label:
Options: &labelzoom.Options{ Data: []labelzoom.DataRecord{ {"name": "ACME Corp", "sku": "12345"}, {"name": "Globex", "sku": "67890"}, },}The free tier returns the first label only, so this is a 1-page PDF until a key is configured. A single record may be passed on its own — it is wrapped rather than rejected.
Handling errors
Section titled “Handling errors”Every non-2xx becomes a typed error carrying the status, the message, the raw body, and the
X-LZ-Request-Id support handle:
result, err := client.Convert(ctx, request)
var forbidden *labelzoom.ForbiddenErrorif errors.As(err, &forbidden) && forbidden.IsPaidFeature { // "JSON export is a paid feature" — the most common free-tier refusal.}
var apiErr *labelzoom.APIErrorif errors.As(err, &apiErr) { log.Printf("request %s failed with %d: %s", apiErr.RequestID, apiErr.Status, apiErr.Message)}*BadRequestError, *UnauthorizedError, *ForbiddenError, *NotFoundError,
*PayloadTooLargeError, *RateLimitedError and *ServerError all unwrap to *APIError, so one
errors.As catches the lot.
*ValidationError deliberately does not: it reports a request rejected locally, before any
network call — a bug in the calling code rather than a server response. Catching *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 return immediately.
Testing your own code
Section titled “Testing your own code”Both seams the retry loop needs are exported, so a test never opens a socket and never sleeps:
client, _ := labelzoom.New( labelzoom.WithHTTPClient(&http.Client{Transport: stub}), labelzoom.WithSleeper(func(d time.Duration) { slept = append(slept, d) }), labelzoom.WithoutJitter(), labelzoom.WithEnvLookup(func(string) (string, bool) { return "", false }),)WithEnvLookup is worth using even when a test does not care about credentials: without it, a
developer’s real LABELZOOM_API_KEY changes what the SDK sends.
Calling the REST API directly
Section titled “Calling the REST API directly”The SDK is a wrapper over one HTTP endpoint; net/http works just as well.
zpl := "^XA^FO50,50^ADN,36,20^FDLabelZoom^FS^XZ"
params := url.Values{}params.Set("params", `{"label":{"width":4,"height":6},"dpi":203}`)
endpoint := "https://api.labelzoom.com/api/v2/convert/zpl/to/pdf?" + params.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(zpl))if err != nil { return err}req.Header.Set("Content-Type", "text/plain")req.Header.Set("Accept", "*/*")if key := os.Getenv("LABELZOOM_API_KEY"); key != "" { req.Header.Set("Authorization", "Bearer "+key)}
resp, err := http.DefaultClient.Do(req)if err != nil { return err}defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)if err != nil { return err}if resp.StatusCode != http.StatusOK { return fmt.Errorf("LabelZoom returned %d: %s", resp.StatusCode, body)}
return os.WriteFile("label.pdf", body, 0o600)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 exactimage/gif,image/bmporimage/jpegwith a406.- Prefer a single
paramsobject. Dot notation carries JSON as well —?label.width=4&dpi=203and?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.
- Reuse one client across conversions for connection pooling; see rate limits for per-plan throughput.
- All endpoint patterns and parameters: quickstart · Swagger reference.
- Package documentation: pkg.go.dev.
- Source, issues, and the shared conformance suite: labelzoom/labelzoom-sdk.