API Reference
Labelixa REST API reference: render and barcode endpoints, request/response headers, error codes and API-key authentication.
Authentication
The free tier works without a key (IP-based limit); no plan adds a watermark to the output. Send the key in the X-API-Key header; it raises the limits. The ?key= query parameter still works but is deprecated (sunset 2027-01-31): query strings leak into browser history, Referer headers, proxy logs and shared links. Requests using it receive Deprecation and Sunset response headers.
Anonymous (keyless) requests are counted per IP address (IPv6: per /64 prefix) — everyone behind that address shares one bucket: 5,000 labels and 10,000 requests per calendar month (UTC), a daily ceiling of 5,000 labels, 3 requests per second, up to 50 labels per request. A free account (labelixa.com/panel) gets an API key with its own monthly quota, independent of the IP, and can save labels; paid plans raise the limits.
API stability
Inside /v1 we only add: new response fields, new optional parameters and new endpoints can appear at any time, so ignore fields you do not recognise. A breaking change — a field removed or renamed, a type or meaning changed, an error code redefined — ships as a new major version running alongside this one. When an endpoint or an auth method is withdrawn you get at least six months' notice, Deprecation and Sunset headers carrying the removal date, and afterwards 410 Gone plus a pointer to the replacement — never a silent 404. This is a contractual commitment, not a habit: Terms of Service §24.4.1. The commitment covers the endpoints documented on this page. Other paths under /v1 may answer as well; those exist for internal flows, carry no stability promise and can change without notice — build your integration on the documented endpoints only.
Quick start
- Send one ZPL label to the anonymous endpoint — no key needed, the free quota is enough:bash
curl -X POST "https://api.labelixa.com/v1/printers/8dpmm/labels/4x6/0" \ -H "Content-Type: text/plain" \ --data-binary '^XA^FO50,50^A0N,40,40^FDHello^FS^XZ' \ -o label.png - The PNG you get back is the label. Send the same body with
Accept: application/pdfto receive a PDF instead. - Create an account and add the
X-API-Keyheader to get your plan's quota and rate limit; details in the sections below.
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /v1/printers/{dpmm}dpmm/labels/{w}x{h}/{index} | Renders the ZPL to a single label (PNG or PDF). |
| POST | /v1/printers/{dpmm}dpmm/labels/{w}x{h}/ | Returns all labels in a single PDF (no index). |
| GET | /v1/barcodes | Produces a single barcode (PNG/SVG/PDF). |
| GET | /v1/barcodes/catalog | Returns the barcode type catalogue (JSON). |
| POST | /v1/graphics | Converts an image to a ZPL graphic command (^GF). State the PHYSICAL size and we do the dot arithmetic: X-Label-MM: 100x150 with X-Dpmm: 8 (203 dpi) prints at exactly that size. Without it one source pixel becomes one printer dot, so the print size is whatever the pixel count happens to be. X-Wrap: label returns a ready-to-print job (^XA/^PW/^LL/^XZ). Every response carries X-Print-Size-MM. Pre-processing: X-Resize-Width/Height, X-Crop, X-Img-Rotate, X-Contrast, X-Dither, X-Invert, X-Threshold. X-Output: tspl returns a ready BINARY TSPL job for TSC printers instead of ZPL (SIZE/CLS/BITMAP/PRINT; in BITMAP a 0 bit is black). X-Output: cpcl returns a CPCL session for Zebra mobile printers (! header, PAGE-WIDTH, EG hex graphic, PRINT; in EG a 1 bit is black). X-Snap-Grid: 1 scales with nearest-neighbour (no grey edges, dithering off). The response carries X-Scale, and X-Scale-Integer: false flags a non-whole ratio (barcode modules may print unevenly). You may also post a PDF: the page is rasterised at your printer resolution (X-Dpmm) and then follows the same path as an image. In a multi-page PDF EACH PAGE BECOMES ONE LABEL (see X-Label-Count), each wrapped in its own ^XA/^XZ; for a single page use X-PDF-Page: 3. Limits: 5 MB, 20 pages, 12 MP per page. If the label sits in a corner of a Letter/A4 carrier page, X-Label-Crop: 4x6 finds the label region, redraws it from the PDF, turns it upright and returns exactly 4x6; the cropped region is in X-Crop-Box (inches x,y,w,h). |
| POST | /v1/fonts | Converts a TrueType font to a ZPL font command (~DU); subsetting with ?chars=. |
| POST | /v1/fonts/library | Uploads a font into your account's font memory (virtual printer memory); it is auto-prepended to renders that use ^A@. GET lists, DELETE .../{name} removes. Requires an account. |
| POST | /v1/graphics/preview.png | Converts a ^GF command back to PNG (a preview of what reaches the printer). |
| POST | /v1/text/measure | Measures text width in dots (for centering and ^FB): JSON {text, font (0-9/A-Z), height, width, dpmm, block_width, label_width}. Returns width_dots, height_dots, lines when block_width is given, and center_x (the ^FO x to centre it) when label_width is given. Measured by the preview engine itself; a printer's ROM font may differ by a few dots. Rate limit only. |
| POST | /v1/diagnostics | Analyses ZPL without rendering it and returns a list of findings (JSON). Label context can be given with ?dpmm=&w=&h=. Pass a printer model such as ?model=GK420d and compatibility findings for that model's resolution and width limits are added too (ZPL7xxx). The response's fields array gives the line/column of every ^FD/^FV field — used to jump from the preview back to the code. Every finding carries the address of its rule page (url, /zpl/rules/ZPL####). |
| POST | /v1/fields/map | Returns the editable fields (^FD/^FV) of a ZPL with their start/end offsets in the source text; each field is marked as text or barcode. This is the data source for form-based editors: substitute into the range to change one field without touching the rest of the ZPL. |
| POST | /v1/variables/schema | Extracts the {{variables}} in a template (type/required/default). |
| POST | /v1/variables/bind | Binds JSON data to a template safely; 422 on missing or invalid data. |
| POST | /v1/variables/bind-rows | Binds up to 20 rows ({"zpl", "satirlar": [...]}) in one request and returns one ZPL stream — no account, nothing stored, rate limit only. 422 with the row index when a row cannot be bound; more than 20 rows returns 422 satir_tavani. For a PDF, send the result to the index-less render endpoint (counts against your quota). |
| POST | /v1/variables/csv | Uploads a CSV or XLSX; the format is detected from the file CONTENT, not its name. Header/encoding/delimiter detection plus a first-row preview; files of up to 20 rows also return every row (satirlar). With bulk job access, larger files return every row too, up to your plan's per-job row limit (satir_tavani). Legacy .xls and password-protected workbooks are rejected with 400 — save as .xlsx or CSV first. |
| POST | /v1/bulk/jobs | Starts a bulk generation job (1 operation per row; Pro, Business and Enterprise plans). GET /v1/bulk/jobs lists your own jobs; GET .../{id} status, .../{id}/download ZIP, .../{id}/download?bicim=pdf a single merged multi-page PDF, .../{id}/cancel cancels. Without the parameter the response is unchanged (ZIP). A job too large for one PDF returns 413 and states roughly how many labels fit. |
| POST | /v1/designs/zpl | Converts a design JSON document into ZPL. Stateless and NOT stored: the design is never written to disk or logged, and it does not consume quota (a designer must be able to ask for a preview on every edit). Validation runs again on the server; an invalid design returns ALL errors in one response (400). |
| POST | /v1/webhooks | Registers an outgoing webhook endpoint (Pro, Business and Enterprise plans). The signing secret is returned in THIS response only. GET lists, DELETE .../{id} removes. |
| GET | /v1/plans | Lists the plan tiers and their limits. |
| POST | /v1/accounts | Creates an account and generates an API key. The e-mail address is not verified here: the first time the address's owner signs in to the panel (magic link or Google), keys created before that sign-in are revoked; webhook endpoints and connectors set up in that period are deactivated, agent pairings are removed and other workspace members are removed (the disabled items are kept for review; the panel lists them once). A new key is issued in the panel. |
| GET | /v1/usage | The API key's usage over the last 7 days. Requires a key: calling it without one returns 401. A key that is not recognised is rejected with 401 on every endpoint (body {"hata": ..., "kod": "gecersiz_anahtar"}); requests without a key run anonymously. |
| GET | /v1/keys | Lists your additional API keys (Business: 3 keys; all draw from the same quota). POST creates one — the full value is returned ONLY in that response; DELETE .../{id} revokes. Management works with the primary key only. Regenerating the primary key in the panel also revokes all additional keys. |
Request headers
| Header | Purpose | Values |
|---|---|---|
X-API-Key | API key (also key= in the query — deprecated) | lbx_... |
Idempotency-Key | A retry does not create a second job; the stored response is replayed (/v1/bulk/jobs only) | any unique client-chosen value |
Accept | Output format | image/png (default), application/pdf, application/json, application/zpl, application/epl |
X-Target-Dpmm | Target resolution in ZPL conversion | 6, 8, 12, 24 |
X-Formatter | ZPL formatting (application/zpl) | On, Off |
X-Page-Size | PDF page size | A4, A5, A6, Letter, Legal |
X-Page-Layout | Grid per page | e.g. 2x3 |
X-Quality | PNG quality | Grayscale, Bitonal |
X-Rotation | Rotates the label | 0, 90, 180, 270 |
Response headers
| Header | Meaning |
|---|---|
X-Total-Count | Total number of labels produced. |
X-Warnings | Skipped/unsupported commands. For EPL output it also leads with what the job writes into the printer itself: the label width (persists after the job) and the form length (drifts if it does not match your media). Read it before printing. |
X-Plan | The plan the request was recognised under. |
X-RateLimit-Remaining | Labels left under today's daily label ceiling — labels, not requests, and not your monthly quota. The ceiling counts labels (one rendered label = one unit), so a single call that renders 50 labels lowers this by 50; it resets at 00:00 UTC. The monthly label and request counters are not reported on a 200 response: when one of them is full the 429 carries X-Quota-Reason and X-Quota-Reset, and keyed accounts read them from /v1/usage. The separate per-second request rate limit is not reported here. |
X-Quota-Hint | Only on anonymous (keyless) 200 responses, once 150 labels of today's anonymous daily ceiling have been used (150 of 5,000, per X-RateLimit-Remaining). Anonymous use is counted per IP and capped at 5,000 labels and 10,000 requests per calendar month (UTC) within that daily ceiling; the header carries the address where a free API key is issued (own monthly quota, saved labels). Absent on keyed requests; the response itself is unchanged. |
Error codes
| Code | Meaning |
|---|---|
| 400 | Invalid parameter (dpmm, size, etc.). |
| 413 | Request body exceeded the endpoint's limit (ZPL render 1 MB; EPL/TSPL/CPCL, DPI and compatibility endpoints 512 KB), the label count exceeded the plan limit, or a single label exceeded 20,000 commands. |
| 429 | Rate/quota limit exceeded; a Retry-After header is returned. |
For the Barcode API, invalid data is returned as an image together with HTTP 200 (for no-code tool compatibility) and the detail is given in the X-Warnings header.
Errors and how to fix them
Every error is an HTTP status code plus a body. Read the status first, then the body: render endpoints answer with ONE LINE OF PLAIN TEXT, every other endpoint with JSON whose message is in hata (English). Some JSON bodies also carry a stable machine-readable name — kod (snake_case) or hata_tipi (PascalCase) — and those names are wire values: branch on them, never on the sentence. The Response field names section explains why the key names are Turkish. The table below lists the errors integrators actually meet, when each one happens and what fixes it.
401 / 403 — key, origin, plan, scope, Business Central
| HTTP | kod / hata_tipi | When it happens | How to fix it | Example |
|---|---|---|---|---|
| 401 | gecersiz_anahtar | An X-API-Key (or ?key=) was sent and no account owns that key — typos, a key revoked in the panel, or a key from another installation (self-hosted vs. cloud). | Copy the key again from the panel; regenerate it if it was revoked. A request WITHOUT a key runs anonymously, so an empty variable never produces this error — a wrong one does. | headers: WWW-Authenticate: ApiKey realm="labelixa" |
| 401 | — | An account-only endpoint (GET /v1/usage, GET /v1/keys, POST /v1/webhooks) was called without a key. | Add the X-API-Key header. A free account is enough. | |
| 403 | origin_kapisi | The request reached the application without passing through the public front door — for example by calling the hosting provider's internal hostname directly. | Use https://api.labelixa.com as the base URL. No header fixes this on purpose. | |
| 403 | — | The feature named in the message (webhooks, bulk jobs, printing, saved labels above the free cap) is not in the plan the key belongs to. X-Quota-Action is sent too. | Upgrade at the address in X-Quota-Upgrade-Url, or check with GET /v1/plans which plan includes the feature. Do not retry: the answer does not change with time. | headers: X-Quota-Action: upgrade |
| 403 | kapsam_yok | The key is scoped (created with a limited set of scopes) and this endpoint needs a scope it does not have; gereken_kapsam names the missing one. | Create a key with that scope in the panel, or use the primary key. The scope name is a contract, not a secret. | |
| 403 | birincil_gerekli | Billing or key management was called with an additional (secondary) key. | Use the account's primary key or the panel for billing and key management; additional keys are for production traffic. | |
| 403 | rol_yetkisiz | The key belongs to a workspace member whose role does not allow the action (gereken_kapsam names it): a viewer tried to write a label. | Ask the workspace owner for a higher role, or use a key of a member who has it. | |
| 403 | hesap_gerekli | Saved-label endpoints were called anonymously. Render endpoints work without a key; saving does not. X-Quota-Action: signup is sent too. | Create a free account with POST /v1/accounts (no card) and send its key. | headers: X-Quota-Action: signup |
| 401 | EntraJetonuGecersiz | The Business Central extension authenticated with a Microsoft Entra token that failed verification (signature, audience, expiry). | Sign in to Business Central again so the extension obtains a fresh token; check that the app registration points at the Labelixa API audience. | |
| 404 | EntraEslemesiYok | The Entra tenant is valid but no Labelixa account is linked to it yet. | Run Connect Account in the extension once with an API key; afterwards Entra sign-in is enough. | |
| 409 | EntraBelirsiz | More than one Labelixa account claims the same Entra tenant, so the server cannot choose. | Connect with the API key of the account that should own the tenant; remove the stale mapping from the other account. | |
| 409 | KiraciBaskasinin | The tenant / environment / company triple is already mapped to a different Labelixa account. | Delete the mapping in the owning account (DELETE /v1/bc/tenants/{id}) or map a different environment. The same account mapping the same triple again is a 200, not an error. | |
400 / 404 / 413 / 422 — parameters, size, validation
| HTTP | kod / hata_tipi | When it happens | How to fix it | Example |
|---|---|---|---|---|
| 400 | — | A path or query parameter is outside the accepted set: dpmm must be 6 | 8 | 12 | 24, a barcode type unknown, a page size not in the list. | Use 6 dpmm (152 dpi), 8 (203 dpi), 12 (300 dpi) or 24 (600 dpi). The message always names the accepted values. | |
| 400 | — | The label width or height in the path is zero, not a number, or above 15 inches. The path is INCHES: /labels/4x6/0 is 4 x 6 in, so a 100 x 150 mm label is /labels/3.94x5.91/0. | Convert millimetres to inches (divide by 25.4) before building the path; decimals are accepted. | |
| 404 | — | Unknown path, a resource id that belongs to another account, or a feature that is switched off on this installation (closed features answer 404, not 403). | Check the path against the endpoint table above; a missing id is not retried. Undocumented paths carry no stability promise. | |
| 413 | — | The body is above the endpoint's limit: 1 MB for ZPL render and diagnostics, 512 KB for EPL/TSPL/CPCL, DPI and compatibility, 5 MB for images and PDFs. | Split the stream into several labels, drop embedded graphics (^GF) and upload them as fonts or graphics instead, or compress images before converting. | |
| 413 | — | One label holds more than 20,000 commands — usually a generator that writes a graphic as thousands of ^GB boxes. | Convert the graphic with POST /v1/graphics to a single ^GF, or split the stream into several labels. | |
| 413 | — | The stream contains more labels than the plan allows in one request (Free and Starter 50, Pro 100, Business 250). | Send the labels in several requests, or use a bulk job on Pro and above; the quota cost is the same. | |
| 422 | — | A JSON body did not parse or did not match the schema (wrong type, missing required field). The body comes from the framework and has a DIFFERENT shape: a detail list with loc and msg per problem. | Fix the field named in loc. Treat detail as the only key; hata is absent in this one case. | |
| 422 | satir_tavani | POST /v1/variables/bind-rows received more than 20 rows; the body carries tavan and satir_sayisi. | Send at most 20 rows per request, or start a bulk job (POST /v1/bulk/jobs) on Pro and above. | |
| 409 | eposta_kullanimda | POST /v1/accounts was called with an e-mail that already has an account. | Sign in to the panel with that address and read the existing key; a second account is not created. | |
402 / 429 — rate limit vs. daily and monthly quota
| HTTP | kod / hata_tipi | When it happens | How to fix it | Example |
|---|---|---|---|---|
| 429 | — | More requests per second than the plan allows (Free 3/s, Starter 5/s, Pro 10/s, Business 20/s). Only Retry-After: 1 is sent — no X-Quota-Reason header. | Wait the second and retry; a client-side queue that spaces requests avoids it altogether. Free responses name the next plan in X-Quota-Next-Plan. | headers: Retry-After: 1X-Quota-Next-Plan: starterX-Quota-Upgrade-Url: https://labelixa.com/pricing?amac=kota |
| 429 | — | Today's label quota does not cover this request (X-Quota-Reason is gunluk). The quota counts LABELS, so one call rendering 50 labels uses 50. X-RateLimit-Remaining carries the labels still left. | Upgrade (X-Quota-Action is yukselt; plan and address in X-Quota-Next-Plan and X-Quota-Upgrade-Url) or wait until X-Quota-Reset (00:00 UTC). Retrying earlier returns the same 429. | headers: Retry-After: 3600X-Quota-Action: yukseltX-Quota-Reason: gunlukX-Quota-Reset: 2026-10-28T00:00:00ZX-RateLimit-Remaining: 3X-Quota-Next-Plan: starterX-Quota-Upgrade-Url: https://labelixa.com/pricing?amac=kota |
| 429 | — | The monthly label or request window is used up (X-Quota-Reason is aylik_etiket or aylik_istek). It is applied TOGETHER with the daily window, anchored to the subscription day. | Upgrade, or wait for the renewal time in X-Quota-Reset. X-RateLimit-Remaining is NOT sent here because the daily remainder is unknown at that point. | headers: Retry-After: 3600X-Quota-Action: yukseltX-Quota-Reason: aylik_etiketX-Quota-Reset: 2026-11-17T00:00:00Z |
| 429 | hiz_siniri | Too many POST /v1/accounts calls from one address in the window. | Create the account once and keep its key; wait Retry-After before trying again. | headers: Retry-After: 3600 |
| 403 | — | The account holds as many saved labels as the plan allows (Free 5, Starter 50, Pro 500, Business 2,500). This is independent of the daily render quota. | Delete or archive labels you no longer need, or upgrade (X-Quota-Action is upgrade). | headers: X-Quota-Action: upgrade |
| 402 | DenemeDoldu | The free DirectPrint trial (25 labels) is used up and the plan does not include printing. | Upgrade to Starter or higher; the job is not queued and must be sent again afterwards. | headers: X-Quota-Action: upgrade |
| 402 | TavanDoldu | The Business Central tenant reached its monthly print cap. | Upgrade the plan or wait for the next month; the counter only grows on 202, a rejected job costs nothing. | headers: X-Quota-Action: upgrade |
Two different limits share the 429 status and the headers tell them apart. The per-second rate limit answers with Retry-After: 1 and NO X-Quota-Reason header: wait one second and send the request again. The daily or monthly quota answers with Retry-After: 3600, X-Quota-Action and X-Quota-Reason (gunluk, aylik_etiket or aylik_istek); X-Quota-Reset gives the exact UTC renewal time and retrying before it is pointless — upgrade the plan (the next plan and its address are machine-readable in X-Quota-Next-Plan and X-Quota-Upgrade-Url) or wait for the reset. Anonymous 200 responses add X-Quota-Hint once 150 of the 5,000 free labels are used: a free key gives you your own quota.
409 / 412 / 428 — concurrency and retries
| HTTP | kod / hata_tipi | When it happens | How to fix it | Example |
|---|---|---|---|---|
| 428 | — | A saved label was written (PATCH /v1/labels/{id}) without the If-Match header. The body carries the current version and ETag. | Read the label first and send its ETag as If-Match; send If-Match: * to overwrite deliberately. | |
| 412 | — | The If-Match value is older than the stored version: someone else saved in the meantime. | Re-read the label, merge, and write again with the new ETag. Never blindly retry with If-Match: * from automation. | |
| 422 | — | The same Idempotency-Key was reused with a DIFFERENT body. | An idempotency key identifies one request: generate a new key for a new body. | |
| 409 | — | A retry arrived while the first request with the same Idempotency-Key is still running. | Wait Retry-After and send the identical request again; the stored answer is replayed. | headers: Retry-After: 2 |
5xx — render budget and service
| HTTP | kod / hata_tipi | When it happens | How to fix it | Example |
|---|---|---|---|---|
| 503 | — | The label is valid but too expensive to draw within the render budget (huge bitmaps, thousands of fields). Quota for the unproduced label is refunded. | Simplify the label (smaller ^GF, fewer fields), lower the dpmm, or split the stream. Retry-After is 1 s, but the same input will hit the budget again. | headers: Retry-After: 1 |
| 503 | — | A dependency is unavailable (database, key hashing, e-mail). Plain text on render endpoints, hata JSON elsewhere. | Retry with exponential backoff (start at a few seconds). Persisting for minutes: check /status. | |
| 500 | — | An unhandled exception. The body never carries details; the X-Request-ID header identifies the request in our logs. | Retry once; if it repeats, send X-Request-ID to support — it is the fastest way to find the cause. | headers: X-Request-ID: 1da4dafa121247ab8dd6e413d2aeef33 |
Print job error types
A print job that fails does not fail the HTTP request — POST /v1/print answered 202 and the result is in the job status. The hata_tipi field of a failed job is one of these classes; asama says how far the job got before it failed.
| kod / hata_tipi | Meaning |
|---|---|
AdaptorYok | Connection type not supported (nothing was sent) |
AgentHatasi | Agent error |
CihazEslenmemis | Printer not mapped on the agent (nothing was sent) |
GonderilmedenBirakildi | The agent stopped before sending (nothing was sent) |
KimlikAlinamadi | Printer identity could not be read (nothing was sent) |
KimlikUyusmazligi | Printer identity did not match (nothing was sent) |
TeslimBelirsiz | Delivery uncertain — check the printer before reprinting |
YaziciHatasi | Printer error (may have printed partially) |
ZplBulunamadi | Label content was no longer available (nothing was sent) |
Response field names
JSON response bodies use stable wire names for their keys. Most of them are Turkish words (for example ozet, mesaj, durum) and they stay exactly as they are: renaming a key would be a breaking change, which the API stability commitment above rules out inside /v1. The official integrations — the MCP server and the n8n node — translate the keys and show English names. The table below is generated from the same mapping they use, so raw responses and integration output can be read side by side. A key that is not listed keeps its JSON name in the integrations too. Error bodies carry the human-readable message, in English, in hata. When each error happens and how to fix it: Errors and how to fix them.
| JSON key | English name |
|---|---|
mesaj | message |
ozet | summary |
toplam | total |
alanlar | fields |
onizleme | preview |
baslik | title |
baslik_key | title_key |
yeni | replacement |
islem | operation |
bicim | format |
banka | bank |
veri | data |
komut | command |
oneri | suggestion |
parametre | parameter |
deger | value |
hane | digits |
verilen | given |
dogru | correct |
beklenen | expected |
gelen | actual |
uzunluk | length |
yol | path |
genislik | label_width |
yukseklik | label_height |
bitis | end |
gen | print_width |
azami | maximum |
desteklenen | supported_dpi |
tip | type |
dil | language |
guven | confidence |
karma | mixed |
puanlar | scores |
kanitlar | evidence |
kod | code |
satir | line |
uretici | manufacturer |
kategori | category |
dil_postur | language_support |
tur | type |
kaynak_url | source_url |
dogrulama | verification |
cihaz_seri | device_serial |
not | note |
seviye | level |
gerekceler | reasons |
boyut_kurali_var | has_size_rule |
boyut_bulgulari | size_findings |
onizleme_kapsami | preview_coverage |
destekli | supported |
kismi | partial |
desteksiz | unsupported |
yok_sayilan | ignored |
bilinmiyor | unknown |
sinir | limitation |
puan | score |
puan_formulu | score_formula |
degerlendirilen | assessed |
degerlendirilmeyen | not_assessed |
bolumler | sections |
notlar | notes |
durum | status |
bulgular | findings |
uyarilar | warnings |
sebep | reason |
sozdizimi | syntax |
boyut_dpi | size_dpi |
yon | orientation |
barkod | barcode |
gorsel_bellek | graphics_memory |
is_davranisi | job_behaviour |
model_uyum | model_compatibility |
komut_adedi | command_count |
etiket_adedi | label_count |
dondurulmus_alan | rotated_fields |
adet | count |
komutlar | commands |
indirilen_font_adedi | downloaded_font_count |
indirilen_font_bayt | downloaded_font_bytes |
ozel_font_cagrisi | custom_font_calls |
grafik_bayt | graphic_bytes |
grafik_adet | graphic_count |
font_bayt | font_bytes |
font_adet | font_count |
toplam_bayt | total_bytes |
tavan_bayt | limit_bytes |
en_dots | width_dots |
boy_dots | height_dots |
boyut_kaynagi | size_source |
adaylar | candidates |
en_inch | width_in |
boy_inch | height_in |
en_mm | width_mm |
boy_mm | height_mm |
standart | standard |
tahmini_dpi | estimated_dpi |
tahmin_sebebi | estimate_reason |
kopya | copies |
format_adedi | format_count |
sablon_adedi | template_count |
toplam_baski | total_prints |
tasma_olculen_format | overflow_measured_formats |
render_siniri | render_limit |
etiketler | labels |
sira | index |
alan_adedi | field_count |
barkod_adedi | barcode_count |
sablon | template |
tasan_alan | overflowing_fields |
modeller | models |
is_id | job_id |
satir_sayisi | row_count |
sonuc | result |
deneme | attempts |
uretilen | produced |
hatali_sayisi | failed_count |
hatali | failed |
sorunlar | problems |
ad | name |
cikti | output |
olcu | size |
genislik_in | width_in |
yukseklik_in | height_in |
kota_iade | quota_refunded |
islenen | processed |
isler | jobs |
hata_tipi | error_type |
baski | print |
gonderildi | sent |
ucretlendi | charged |
basim_onaylandi | print_confirmed |
etiket | label_count |
olculdu | measured |
okunan | decoded |
neden | reason |
Fixed values
Some fields hold one word from a fixed set. The JSON value stays as it is; the integrations show the English word. Paths use the English field names from the table above; [] marks an array element and * any key.
| Field (English path) | JSON value | English value |
|---|---|---|
risk.level | dusuk | low |
orta | medium | |
yuksek | high | |
risk.reasons[] | native | native |
emulasyon | emulation | |
dil_yok | no_zpl_support | |
boyut_bulgusu | size_finding | |
model.category | masaustu | desktop |
endustriyel | industrial | |
mobil | mobile | |
language_support.type | native | native |
emulasyon | emulation | |
limitation | risk_analizi | risk_analysis |
evidence[].code | zpl_xa | zpl_xa |
zpl_xz | zpl_xz | |
zpl_komut_yogunlugu | zpl_command_density | |
zpl_tilde | zpl_tilde | |
epl_metin_satiri | epl_text_line | |
epl_barkod_satiri | epl_barcode_line | |
epl_kontrol_satiri | epl_control_line | |
tspl_mm_boyut | tspl_mm_size | |
tspl_kelime | tspl_keyword | |
cpcl_baslik | cpcl_header | |
cpcl_kelime | cpcl_keyword | |
sections.*.status | temiz | clean |
bulgulu | findings | |
degerlendirilmedi | not_evaluated | |
assessed[] | sozdizimi | syntax |
boyut_dpi | size_dpi | |
yon | orientation | |
barkod | barcode | |
font | font | |
gorsel_bellek | graphics_memory | |
is_davranisi | job_behaviour | |
model_uyum | model_compatibility | |
not_assessed[] | sozdizimi | syntax |
boyut_dpi | size_dpi | |
yon | orientation | |
barkod | barcode | |
font | font | |
gorsel_bellek | graphics_memory | |
is_davranisi | job_behaviour | |
model_uyum | model_compatibility | |
sections.size_dpi.summary.size_source | komut | command |
alanlardan | from_fields | |
karma | mixed | |
onceki | previous | |
sections.job_behaviour.summary.labels[].size_source | komut | command |
alanlardan | from_fields | |
karma | mixed | |
onceki | previous | |
status | bekliyor | queued |
calisiyor | running | |
bitti | done | |
hata | failed | |
iptal | cancelled | |
jobs[].status | bekliyor | queued |
calisiyor | running | |
bitti | done | |
hata | failed | |
iptal | cancelled | |
jobs[].type | toplu_uretim | bulk_production |
result.failed[].problems[].code | eksik | missing |
sayi_degil | not_a_number | |
sozdizimi | syntax | |
temizlendi | sanitized | |
render | render |
Bulk job states
The durum field of a bulk job is one of:
| JSON value | Meaning |
|---|---|
bekliyor | Queued: waiting for a worker. A failed attempt that will be retried also returns here. |
calisiyor | Running. |
bitti | Finished: the output can be downloaded. Rows that could not be produced are reported in the result object. |
hata | Failed: the last allowed attempt failed or the error cannot be retried. No further attempts are made. |
iptal | Cancelled. Only a job that has not started yet can be cancelled. |
Compatibility levels
The compatibility check returns a risk level in risk.seviye, never a guarantee:
| JSON value | Meaning |
|---|---|
dusuk | Low: the model runs ZPL natively. |
orta | Medium: the model runs ZPL through a manufacturer-documented emulation mode. |
yuksek | High: no ZPL support is recorded for the model. |
onizleme_kapsami groups the label's commands by what OUR preview does with them. It describes the preview, not the printer:
| JSON key | Meaning |
|---|---|
destekli | Supported: drawn, no known parameter limits. |
kismi | Partial: drawn, with a documented limitation. |
yok_sayilan | Ignored: recognised and reported but not drawn (for example RFID, which only the printer writes). |
desteksiz | Unsupported: known command, not processed; diagnostics warn about it. |
bilinmiyor | Unknown: not in the command catalogue. |
Data extraction (JSON)
If you send Accept: application/json to the render endpoint, the visible text and barcode fields on the label are returned as JSON together with their coordinates (barcodes are marked with tur: barkod):
curl -X POST "https://api.labelixa.com/v1/printers/8dpmm/labels/4x6/0" \
--data "^XA^FO50,60^FDHello^FS^XZ" -H "Accept: application/json"ZPL conversion (application/zpl)
With Accept: application/zpl your ZPL is reformatted (each command on its own line; disabled with X-Formatter: Off) and, if X-Target-Dpmm is given, the coordinate/size parameters are scaled from the source resolution to the target. Data fields (^FD) do not change.
curl -X POST "https://api.labelixa.com/v1/printers/8dpmm/labels/4x6/0" \
--data-binary @label.zpl -H "Accept: application/zpl" \
-H "X-Target-Dpmm: 12" > label-12dpmm.zplEPL2 output (application/epl)
With Accept: application/epl your label is converted to EPL2, the language used by Zebra's older printers. Text, barcode, box and line fields become EPL2 commands; sizes are computed from the printer's dot density (8dpmm = 203 dpi, 12dpmm = 300 dpi — EPL2 font metrics DIFFER between the two and the right table is selected automatically). Supported barcodes: ^BC Code 128, ^B3 Code 39, ^BE EAN-13, ^B8 EAN-8, ^BU UPC-A, ^B2 Interleaved 2/5, ^BK Codabar.
EPL2 has a narrower command set than ZPL, so not every label converts one-to-one. Nothing is dropped silently: whatever could not be translated is named in the X-Warnings header. Not converted today: QR (^BQ), DataMatrix (^BX), embedded graphics (^GF) and text blocks (^FB). Note also that EPL2 text sizing is not continuous — fixed font x integer multiplier — and the narrow bar width is limited per symbology (1-10 for Code 128, 2-4 for EAN/UPC); a value outside that range is clamped and reported too.
curl -X POST "https://api.labelixa.com/v1/printers/8dpmm/labels/4x6/" \
--data-binary @label.zpl -H "Accept: application/epl" \
-D headers.txt > label.eplOutgoing webhooks
Instead of polling for the status of a bulk job we can notify you. Register an endpoint with POST /v1/webhooks; the events are toplu.tamamlandi and toplu.basarisiz.
curl -X POST "https://api.labelixa.com/v1/webhooks" \
-H "X-API-Key: $LABELIXA_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://erp.example.com/labelixa"}'The signing secret is returned in THIS response only and is never shown again in listings; if you lose it, delete the endpoint and create it again. The secret is separate from your account API key: rotating the key does not break webhook verification.
Every request carries Labelixa-Signature: t=<unix>,v1=<hmac>. The HMAC-SHA256 is computed over "<t>.<body>" — the timestamp is INSIDE the signature, so you can reject replays by checking how fresh it is. The format matches Stripe's, so code that already verifies Stripe webhooks can be adapted.
import hmac, hashlib, time
def verify(secret, body, header, tolerance=300):
p = dict(x.split("=", 1) for x in header.split(","))
t = int(p["t"])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(p["v1"], expected)Every request also carries Labelixa-Delivery-Id, and that id does NOT change across retries. If you see the same id twice, do not repeat the work — delivery is at-least-once.
Secret rotation
POST /v1/webhooks/{id}/rotate generates a new secret. The old one stays valid for a transition window (24 hours by default), and during that window every request carries TWO signatures (v1=… ,v1=…). Whichever one your verifier knows will match, so you can rotate without downtime. The remaining time is in the rotasyon_bitis field of the GET /v1/webhooks response.
Your verification code must support MULTIPLE v1 values — if you parse the header into a dictionary and read a single value you only see the last one, and you will reject valid signatures during the first half of a rotation.
Any response other than 2xx is a failure and is retried; redirects (3xx) are NOT followed. An endpoint that keeps failing is deactivated automatically — the listing then shows aktif: false and the last status code. The body contains NO row data: only the job id and counters, and you fetch the result with the job id.
Saved labels
Render endpoints are stateless and never store anything; saving happens only through these endpoints, on explicit request. Every account can save: Free 5, Starter 50, Professional 500, Business 2,500 saved labels; Enterprise unlimited. Anonymous (keyless) requests cannot save — a free account is enough, no card required. This limit is independent of the daily operation quota: one is how many labels you print, the other how many designs you keep.
| Method | Path | Description |
|---|---|---|
| POST | /v1/labels | Saves a label (201). |
| GET | /v1/labels | Lists your labels (no bodies; search, status, folder, favorite, sort, pagination). |
| GET | /v1/labels/{id} | Full content of one label + ETag. |
| PATCH | /v1/labels/{id} | Partial update; If-Match required. |
| DELETE | /v1/labels/{id} | Moves to trash; ?kalici=true deletes permanently. |
| POST | /v1/labels/{id}/restore | Restores from trash. |
| POST | /v1/labels/{id}/duplicate | Duplicates (the copy is a draft with no history). |
| PUT/DELETE | /v1/labels/{id}/favorite | Favorite flag. |
| PUT/DELETE | /v1/labels/{id}/archive | Archive / unarchive. |
| POST | /v1/labels/{id}/publish | Publishes; 409 with findings if the ZPL has errors. |
| POST | /v1/labels/{id}/unpublish | Back to draft. |
| GET | /v1/labels/{id}/versions | Version history (no bodies). |
| GET | /v1/labels/{id}/versions/{n} | Full content of one version. |
| POST | /v1/labels/{id}/versions/{n}/restore | Restores a version (forward: history is never deleted). |
| GET | /v1/labels/{id}/events | Audit trail (event type + time; carries no content). |
| POST | /v1/folders | Creates a folder (single level). |
| GET | /v1/folders | Folders + label counts. |
| PATCH/DELETE | /v1/folders/{id} | Renames / deletes (contents move to root, not deleted). |
| PUT | /v1/labels/{id}/folder | Moves a label into a folder (klasor_id: null = root). |
Concurrency (If-Match)
Every read returns an ETag and every write requires the If-Match header: writing without it returns 428, writing with a stale version returns 412 (the body carries the current version). This prevents a second browser tab from silently overwriting the first one's changes. To overwrite deliberately, send If-Match: *.
Versions and trash
Each update snapshots the previous state (the last 50 versions per label are kept). Restoring an old version is written as a new version, so restoring is itself reversible. A deleted label stays in the trash for 30 days and can be restored; active labels are kept indefinitely. Idempotency-Key also applies to POST /v1/labels: retrying with the same key after a network timeout does not create a second label.
Printing
Render endpoints hand you an image; these endpoints send the job to a registered printer. A paid plan is required (Starter and above); the free plan includes a one-time trial of 25 labels on 1 printer. Billing does not double-count: pass the id of an already-rendered job as kaynak_is_id and the print is free; send raw ZPL and the label counts as produced here, and is billed here.
| Method | Path | Description |
|---|---|---|
| POST | /v1/devices | Registers a printer profile (201). Body: takma_ad, grup, dpmm, genislik_dots, varsayilan_mi, dil, adaptor. dil is the language the device speaks (zpl, epl, tspl, cpcl or raw; default zpl) and it decides which language a test print is generated in. adaptor is the output path; available today: raw_tcp, os_driver. Serial number and network address are not accepted — finding the device on the network is the agent's job. |
| GET | /v1/devices | Lists your registered printers. |
| DELETE | /v1/devices/{id} | Deletes a printer profile. |
| GET | /v1/devices/{id}/status | What the cloud can see about the device: queued job counts, age of the oldest waiting job, last job and agent liveness. The printer's physical state (media, printhead, jam) cannot be read from here — a raw 9100 socket never reports it, and the response says so explicitly with yazici_durumu: "BILINMIYOR". kimlik reports the identity check the agent runs in front of every job for a pinned device: sabitlendi, son_dogrulama (last verified, epoch seconds), son_sonuc (verified, mismatch, unavailable or not_pinned) and son_sonuc_zamani. verified proves that the device that answered was the pinned one at job time, not that a label came out. All null until an agent that reports the check runs a job for the device. |
| POST | /v1/print | Queues a print job (202). Body: device_id (or printer_group), yuk (the payload bytes), dil, kopya, etiket_sayisi, kaynak_is_id. The payload is carried verbatim and never parsed, so any printer language goes through — dil accepts zpl, epl, tspl, cpcl or raw. Omit dil and the device's own language is used. zpl is still accepted as the old name for yuk; sending both with different values is rejected. If the declared language clearly contradicts the content the job is still queued and the response carries a uyari field. |
| POST | /v1/bulk/jobs | Adding device_id to a bulk job sends the generated ZPL stream to the printer as ONE print job — the chain no longer has to end in a ZIP. Only valid with cikti="zpl". The labels are NOT charged twice (the bulk job already charged per row). The result summary carries a baski field; if the print job cannot be queued the bulk job still succeeds and the reason is in that field. |
| GET | /v1/print/jobs/{id} | Job status. Someone else's job returns 404. |
| POST | /v1/print/jobs/{id}/cancel | Cancels a job that is still queued (200, status cancelled; its payload is deleted and the agent never receives it). Calling it again for a cancelled job returns the same 200 body. A job that is dispatching, uncertain, dispatched or failed returns 409 with its current status: once the job has been handed to the agent or the printer it cannot be recalled, so check the printer before reprinting. Someone else's job returns 404. Cancelling sends no webhook event. |
Job statuses — and why there is no `printed`
There is deliberately no “printed” status. Over raw 9100 you cannot prove a label was printed: the socket accepting your bytes only shows they reached the printer's buffer; running out of media, an open printhead or a jam all happen after the bytes are accepted, and the socket never tells you. The furthest state is dispatched = “we handed it over”. Every response also carries basim_onaylandi: false, so ignoring the field cannot be read as “then it printed”.
| Status | Meaning |
|---|---|
queued | Job accepted, waiting in line. |
dispatching | Being handed to the printer. |
dispatched | Bytes delivered to the printer. This does NOT mean the label was printed. |
failed | Delivery failed. |
cancelled | Job cancelled. |
uncertain | The job reached the agent but no result came back (for example the agent stopped in the middle of sending), so whether bytes reached the printer is NOT known. It is never retried automatically: check the printer, and if the label is missing, reprint it as a NEW job. The status response then carries uyari_kodu: delivery_uncertain and an uyari sentence. A late, definite result from the agent can still move the job to dispatched or failed; nothing moves it back to queued. In the panel (Saved work) the job offers "Reprint as new job" while its label content is still kept (at most 24 hours after the job was queued); the new job links the original in kaynak_is_id. |
So: you can build a workflow that triggers printing, but do not build business rules on print confirmation. Where confirmation genuinely matters (pharma, track-and-trace), verification has to be read back from the label itself.
`failed` does not mean “nothing was printed”
A job can fail at five different points, and in four of them not a single byte reaches the printer (device not mapped, no adapter, identity check failed). In the fifth — the connection to the printer breaking — the bytes did go out, and on a multi-copy job part of it may already be on paper. The status response carries that difference in separate fields:
| Field | Meaning |
|---|---|
asama | How far the job got: queued → delivered_to_agent → submitted_to_printer. |
gonderilen_kopya | Copies actually written to the printer. -1 means unknown, which is NOT the same as zero. |
yaziciya_giden_var_mi | true/false/null. null means an older agent build did not report it — not that nothing was sent. |
A job whose bytes reached the printer is never retried automatically — retrying it would print the label a second time. Such a job is held and the decision to continue is yours. Failures that never reached the printer keep being retried as before.
Preventing duplicates: Idempotency-Key
Resending a request after a network timeout is the right reflex — but the label may already be on paper. Send an Idempotency-Key header and a second request with the same key creates no new job: it replays the first response (Idempotency-Replayed: true). The same key with a DIFFERENT body returns 422 — recycling a key would silently print the wrong label. A 429 (queue full) does NOT consume the key; retrying with it is expected.
Printing from a stored template
Instead of yuk you can send sablon_id and degiskenler, and the payload is built from the template. Sending both is rejected. A missing or invalid variable returns 400 — a label printed with a missing variable wastes media, and only the person standing at the printer would notice.
Printing a saved label: etiket_id
Instead of yuk you can send the etiket_id of a label saved in your account (/v1/labels) together with degiskenler for its variables; the payload is built from the label. To print several labels as ONE job, send satirlar: one object per label, 1-1000 rows, where each row's values override degiskenler and etiket_sayisi becomes the row count. An empty satirlar list is rejected; omit it to print a single label. etiket_id cannot be combined with yuk, zpl or sablon_id (422). A missing or invalid variable returns 422 and names the row — the job is not queued, no row is skipped. Another account's label gets the same 404 as a missing one, and archived labels are not printed. Idempotency-Key, kopya, device_id / printer_group and billing work exactly as with a raw payload; the response carries etiket_id. With the approval workflow on, only published labels can be printed (422 otherwise); submit and approve the label in the panel under Saved work, or use POST /v1/labels/{id}/publish and /approve.
Printer groups: printer_group
Give a device a grup when you register it (DEPO, URETIM) and a print request may carry printer_group instead of device_id. The point is to keep device identifiers out of your business system: when a printer is replaced, the group moves with the device record and the ERP-side code is untouched. Resolution is DETERMINISTIC on purpose — we do not have the data a "pick the idle one" strategy would need, because a raw 9100 socket never reports load. One device in the group: that device. Several: the one marked default. No default, or more than one, and the job is NOT printed — the error names the devices that clash. Sending device_id and printer_group together is rejected. Group names are normalised to upper case and may contain only letters, digits, - and _. Nothing changes for integrations that do not use groups: grup starts empty and device_id works as before. Either way the response returns the device_id the job landed on.
Webhook notifications
You do not have to poll for status: subscribe to baski.teslim and baski.basarisiz. There is no baski.basildi event — the same reason there is no printed status. The body carries no raw label content: job id, stage and counters only. The failure event fires only once a job has failed permanently; notifying on every retry would bury the real failure in noise. A job that becomes uncertain fires baski.belirsiz — a separate event, so an automation that reprints on baski.basarisiz does not print it twice; existing subscriptions receive it only after you add it. Every baski.* body carries veri.sira (1, 2, … per job) and veri.status (the status at that event). Deliveries are retried, so an older event can arrive after a newer one: compare sira and ignore an event whose sira is lower than the last one you processed for that job.
The DirectPrint agent that collects these jobs and delivers them to the printer is downloaded here: /download
Setting up the agent (setup wizard)
On the computer the printers are connected to, run labelixa-agent setup (the one-line installer on the download page installs the agent and starts it for you). The agent opens the Labelixa page in the browser; check that it shows the same short code as the agent window and click Connect. Without a browser, create a pairing code on the Printers page of the panel and run labelixa-agent setup CODE. The free plan includes a trial with 1 computer, 1 printer and 25 labels in total (test labels don't count); beyond that a paid plan is needed. Downloading the agent is free.
The wizard looks at the computer's print queues: a single label printer is used without a question, several are offered as a list, an office printer only after you confirm it. Each chosen printer is added to your account and mapped on this computer, and a test label can be printed at the end. Nothing is sent to a printer during setup and the network is never scanned.
labelixa-agent discover -mdnslists the network printers that announce themselves (Bonjour/mDNS) without sending anything to them.labelixa-agent import FILEadds many network printers at once from a CSV file with onename,address[,group]line per printer (-dry-runchecks the file first). Every line is reported; one bad line does not stop the file.- Running
setupagain on a connected computer keeps the connection and the printers already set up.
The agent asks the cloud for work and the cloud holds the request open until a job arrives (long polling), so a label is handed over as soon as it is submitted. The agent never opens a port: every connection goes from the computer to Labelixa.
Windows: silent install for IT teams
For Intune, Group Policy or SCCM roll-outs the Windows agent also ships as an MSI package. One line installs it as the LabelixaAgent Windows service, pairs it with a single-use code from the Printers page of the panel and imports the printer list: msiexec /i labelixa-agent_0.6.1_windows_amd64.msi /qn PAIRCODE=CODE PRINTERSFILE=C:\path\printers.csv. PRINTERSFILE is optional (one name,address[,group] line per printer); a failed pairing or import rolls the installation back and the code is not written to the installer log. The package and its checksum are on the release page.
USB printers: the operating system's print queue
A desktop printer on USB does not open port 9100; it shows up as a print queue in the operating system (the Windows spooler, CUPS on macOS and Linux). Agent 0.2.0 and later hands the bytes to that queue as RAW data: the driver renders nothing and the label language reaches the printer exactly as you sent it.
- Register the device with
adaptor: "os_driver"(POST /v1/devices). - On the agent machine, find the queue name:
labelixa-agent discover -queues. - Map the device to the queue:
labelixa-agent map-printer -device DEVICE_ID -queue "QUEUE_NAME". - Optionally run the agent in the background from every login:
labelixa-agent install(a LaunchAgent on macOS, a systemd user unit on Linux; on Windows it prints the service commands to run in an administrator prompt).
On this path the furthest stage is submitted_to_os: the spooler accepted the job. That is one step short of submitted_to_printer — a printer that is switched off or out of media shows up in the operating system's queue, not here, and the queue reports no printer status (yazici_durumu: null). A job sent while the printer is off waits in that queue and prints once the printer is back on. A multi-copy job partly handed to the queue is not retried automatically.
The cloud record and the agent mapping must agree: an os_driver device mapped to a network address, or a raw_tcp device mapped to a queue, is not printed and fails with AdaptorYok — stopping beats printing on the wrong printer.
Printing ZPL to a USB Zebra printer from the cloud
PrintOps — metrics, alerts, print audit
The operations face of the print endpoints: what was printed, what is stuck, who printed it. Read-only — it summarises the account's existing print records and writes nothing. The same data is in the panel's PrintOps tab (same plan gate as printers). There is deliberately no 'printed' number: delivered means the bytes reached the agent, not the paper.
| Method | Path | Description |
|---|---|---|
| GET | /v1/printops/dashboard?gun=7 | Seven metrics for the window (1–90 days): job count, labels delivered (Σ labels × copies, dispatched), success rate, failed jobs with the five most frequent error types, jobs queued right now, average delivery time (created → agent result), jobs/failures per printer (top 10). Plus agent count and agents online (last 2 min). |
| GET | /v1/printops/alerts | Five alert types: ajan_cevrimdisi (agent last seen > 10 min, critical), kuyruk_birikmesi (jobs waiting > 5 min; ≥ 20 critical), tekrarli_hata (≥ 3 failures on one printer in 60 min), yazici_engel (printer's last status reply is a blocker), eslenmemis_cihaz (no agent maps the device). When agents have not reported mappings, 'unmapped' is not invented. |
| GET | /v1/printops/audit | Print audit — per job: who (audit-trail actor), what (content digest + template), where (printer), when, outcome (status + stage + error type), copies, and source (kaynak_is_id). Filters: device_id, status, baslangic, bitis, limit. 'Who' is empty while the audit trail is off. |
Business Central
Labelixa's Business Central extension (Labelixa Core + DirectPrint) uses these endpoints; your own integration can use them too. Authentication is an API key; an anonymous request cannot create a mapping (403). The plan gate is the same as for the print endpoints.
| Method | Path | Description |
|---|---|---|
| POST | /v1/bc/tenants | Maps a BC tenant / environment / company to this account. Body {kiraci, ortam, sirket?}; an empty company covers every company in the environment. New mapping 201, repeat from the same account 200; a triple mapped to another account is 409 KiraciBaskasinin. |
| GET | /v1/bc/tenants | The account's mappings and each one's BC print count for the current month. |
| DELETE | /v1/bc/tenants/{id} | Deletes the mapping and its counters (204). Another account's mapping gets the same answer as a missing one (404). |
| POST | /v1/bc/print | The /v1/print body plus kiraci, ortam, sirket?. The mapping is resolved, the monthly cap is checked, then the job takes the same path as /v1/print. 202 means queued, not printed. To print a saved label send etiket_id with degiskenler, or satirlar for several rows (1-1000 rows; an empty list is a 400, omitting satirlar prints one label; copies 1-100). When the label uses the parcel sequence (shipment_package_number) and the values carry shipment_package_count = N, that record prints N labels (1 / N to N / N); that is the label count charged, still at most 1000. When the cap is reached: 402 with X-Quota-Action: upgrade. The counter only grows on 202; the web quota is not used. Idempotency-Key is checked before the cap: a retry with the same key gets the original 202 back (Idempotency-Replayed: true) without counting again, even when the cap is reached; the same key with a different body gets 422. |
| GET | /v1/bc/dictionary | The BC field dictionary: source type → BC field → template variable (item_number style). |
| POST | /v1/labels/starter-set | Adds the unbranded shipment label set (English content) to the account as drafts: 10x10 cm, 10x15 cm, 4x6 in, 103x150 mm and A6, filled from posted sales shipments; every label shows the parcel sequence (PACKAGE 1 / 3) and the shipment's total gross weight (TOTAL WEIGHT). A label that already exists under the same name (archived ones included) is skipped, so calling it twice adds nothing; if the saved-label limit cannot fit the missing labels, none is added (403 + X-Quota-Action: upgrade). |
MCP server for AI assistants
Claude, ChatGPT and other MCP-capable AI clients can use Labelixa directly as a tool: the remote MCP server lives at https://api.labelixa.com/mcp (streamable HTTP, JSON-RPC; nothing to install — paste one URL into your client). No key is required — anonymous use is subject to free-tier limits; connect with Authorization: Bearer lbx_... to spend your own account quota.
| Tool | Description |
|---|---|
zpl_preview | Renders ZPL to PNG (dpmm/size/index selectable). |
zpl_validate | Lints ZPL with line/column-located diagnostics. |
barcode_png | Produces a single barcode as PNG. |
save_label | Saves the label to the account; without an API key it is held for 24 hours and a one-time account link is returned. |
printer_language_to_zpl | Converts a SATO SBPL, Datamax DPL or Intermec IPL stream to ZPL; you name the source language. Warnings mean the conversion is not 1:1; not verified on physical printers. |
MCP calls draw from the same quota and rate limits as REST — MCP is not a separate tier, it is the same API with a tool face. The table above shows highlights; the full, always-current tool list comes from the live server (tools/list) and the /mcp page.
Rule files for coding assistants
One rule file teaches Claude Code, Cursor, GitHub Copilot or Windsurf to validate and render label code with Labelixa instead of guessing from the text. Copy the file for your assistant from the Labelixa/agent-skills repository into your project: .claude/skills/labelixa/SKILL.md, .cursor/rules/labelixa.mdc, .github/instructions/labelixa.instructions.md, .windsurf/rules/labelixa.md. The rules call the MCP server or the CLI; the API key, if any, comes only from the environment.
Reading a printed label back
POST /v1/verify reads the barcodes in a photo or scan you upload — the answer to "I printed it, but will the handheld scanner read it?". Send the body as multipart/form-data with the image in the file field; PNG, JPEG, BMP and GIF are accepted, up to 5 MB and 6000×6000 pixels. The response carries olculdu (did the decoder run), an okunan list (each with format and veri) and a not field.
Do not confuse this with POST /v1/barcode-check: there the input is ZPL — we render the label and read back what we drew ourselves. Here the input is an image from the real world.
The limits of the result are printed in the response and should be taken seriously: "read" does NOT guarantee that every handheld scanner will read it, and "not read" does NOT prove the label is bad — the photo's angle, focus or lighting may simply be insufficient. This endpoint is NOT an ISO/IEC 15416 print-quality grade (A–F); that requires a verifier device.
curl -X POST "https://api.labelixa.com/v1/verify" \
-F "file=@label-photo.jpg"To do the same from your browser: Barcode Verifier.
Printer languages beyond ZPL
Labelixa renders and validates four printer languages. The endpoints below mirror the ZPL ones: render returns a PNG of the label, diagnostics returns diagnostics with line numbers. Coverage is deliberately an MVP subset per language and every response states its own limits — a command we do not draw is reported, never silently skipped.
| Method | Path | Description |
|---|---|---|
| POST | /v1/language-detect | Detect ZPL / EPL / TSPL / CPCL |
| POST | /v1/epl/render | Render EPL/EPL2 to PNG. A stream may hold several labels (each ended by P): X-Label-Count is the total, ?index= picks one. |
| POST | /v1/epl/diagnostics | EPL diagnostics with line numbers |
| POST | /v1/tspl/render | Render TSPL/TSPL2 to PNG. A stream may hold several labels (each ended by PRINT): X-Label-Count is the total, ?index= picks one. |
| POST | /v1/tspl/diagnostics | TSPL diagnostics with line numbers |
| POST | /v1/cpcl/render | Render CPCL to PNG. A stream may hold several labels (each ended by PRINT): X-Label-Count is the total, ?index= picks one. |
| POST | /v1/cpcl/diagnostics | CPCL diagnostics with line numbers |
| POST | /v1/compatibility | ZPL compatibility risk analysis |
POST /v1/language-detect answers a different question: given code you did not write, which language is it? The answer is heuristic and deterministic (no LLM), and confidence is reported as High/Medium/Low rather than a made-up percentage.
POST /v1/compatibility takes ZPL plus a printer model and returns a compatibility risk analysis — language posture, size-rule findings and preview scope. It is NOT an emulator and never returns a compatibility guarantee: we have physically tested only one of forty models, and a documented emulation mode is not native support.
Printer language conversion (SBPL / DPL / IPL → ZPL)
Three endpoints convert SATO SBPL, Honeywell (Datamax) DPL and Honeywell (Intermec) IPL streams to ZPL, so the label can be previewed, linted and printed with the ZPL tools. The body is the raw stream — real control characters and the manuals' readable notation (<STX>, <ESC>) give the same result. You choose the source language: automatic language detection does not recognise these three languages.
| Method | Path | Description |
|---|---|---|
| POST | /v1/convert/sbpl-to-zpl | SATO SBPL → ZPL |
| POST | /v1/convert/dpl-to-zpl | Datamax / Honeywell DPL → ZPL |
| POST | /v1/convert/ipl-to-zpl | Intermec / Honeywell IPL → ZPL |
Parameters
| Parameter | Default | Meaning |
|---|---|---|
dpmm | 8 | Target resolution: 6, 8, 12 or 24 dots per mm (152 / 203 / 300 / 600 dpi). Other values fall back to 8. |
etiket_boyu_mm | — | DPL only: label length in mm. DPL measures rows from the BOTTOM edge of the label and ZPL from the top; without this value (or <STX>c / <STX>KcCL in the stream) 6 inches is assumed and a warning is added. |
Response
| Field | Meaning |
|---|---|
zpl | The converted ZPL (one ^XA…^XZ block per label). |
uyarilar | List of warnings in English: every command or barcode that could not be carried over, with the manual page. |
kaynak_dil | sbpl, dpl or ipl — the endpoint you called. |
birebir | true only when the warning list is empty. |
X-Warnings-Count | Response header: number of warnings. |
If there are warnings, the conversion is not 1:1. Commands outside the supported subset — printer settings (speed, darkness, sensor), RFID, job management and several 2D symbologies — are skipped WITH a warning, never silently. Show the warnings to whoever prints the label.
curl -X POST "https://api.labelixa.com/v1/convert/sbpl-to-zpl?dpmm=8" \
--data-binary "<A><CS>6<V>100<H>200<L>0202<XM>ABCD<Q>1<Z>"{"zpl": "^XA\n^FO199,99^A0N,48,48^FDABCD^FS\n^PQ1\n^XZ",
"uyarilar": ["<CS> Print Speed not carried over to ZPL (manual p. 326; printer setting or outside the supported subset)"],
"kaynak_dil": "sbpl", "birebir": false}curl -X POST "https://api.labelixa.com/v1/convert/dpl-to-zpl?dpmm=8&etiket_boyu_mm=101.6" \
--data-binary @label.dplNot verified on a physical printer. No SATO, Datamax or Intermec printer was used: the output is the ZPL equivalent according to each manufacturer's command reference. Positions of rotated fields are approximate and other font families are mapped to ^A0; both are reported as warnings. Check the preview and a test print before production.
Conversion does not count against the request quota; only the per-second rate limit applies. Body up to 512 KB (413 above), empty body 400. The endpoints are stateless: the code is neither stored nor logged.
To do the same in your browser: Printer Language Converter.
Embedding the preview in your own page
/embed/viewer is a minimal preview component with no site chrome; you embed it in your own application with an <iframe>. The ZPL travels in the URL fragment (#), not the query string — the fragment is never sent to the server, so your label content cannot land in our logs or any proxy's. The format is IDENTICAL to the preview tool's "copy link" output (base64 JSON), so you can paste that link straight in.
<iframe src="https://labelixa.com/embed/viewer#eyJ6IjoiXlhBLi4uIiwiZCI6IjgiLCJ3IjoiNCIsImgiOiI2In0"
width="420" height="620" style="border:0"></iframe>The height is the one you give; the component fits the image into the frame. The page is noindex and absent from the sitemap — it is a component, not a content page.
Postman collection
Every endpoint in this reference is a ready-made request in the public Postman workspace: postman.com/labelixa-3901521. The collection is generated from the OpenAPI schema (/openapi.json), not maintained by hand. The key travels in the apiKey variable and is NOT embedded in the collection — fork the workspace and set it in your own environment; leave it empty and requests run on the anonymous quota.
A badge for your README
If the ZPL files in your repository are linted in CI (POST /v1/diagnostics exits non-zero on errors), you can say so with a static badge — no counter, no tracking, cached for a day:
[](https://labelixa.com/tools/zpl-preview)Use it only when the claim is true for your repository. Embedding the viewer in your own page, with a credit line under it: embedding guide.