API reference

LabelCraft - barcode labels for Shopify that print at exactly the size you set.

The LabelCraft API prints labels and reads templates from outside the Shopify admin: a warehouse system, a receiving script, a scheduled job. It is the app's own print engine reached with a key instead of a session, so a label printed through the API is the label the app would have printed for the same template and the same products, warnings included. It answers with the PDF, or sends it to a printer of the shop's PrintNode account.

It is part of the Retail Pro plan. Keys are created in LabelCraft, under Settings, API access.

Authentication

Send the key as a bearer token on every request. Keys start with lc_live_ and are shown once, when created. A shop holds at most 5 active keys; revoking one takes effect on the next request.

A request without a valid key answers 401. A valid key on a shop below the Retail Pro plan answers 402.

curl https://labelcraft.tech/api/v1/templates \
  -H "Authorization: Bearer lc_live_0123456789abcdef0123456789abcdef01234567"

Limits

GET /api/v1/templates

Lists the built-in paper sizes (by slug) and the shop's own templates (by id), with the label size in millimetres and the resolution the template was designed at. Archived templates are not listed and cannot be printed.

A thermal roll with two or more labels side by side, such as zebra-2-375x1-2across, also answers rollAcross: true. Its kind stays sheet and its label size is one label of the row; the row itself is in layout.sheet of GET /templates/:id. Every other template answers without the key.

curl https://labelcraft.tech/api/v1/templates \
  -H "Authorization: Bearer YOUR_KEY"
{
  "templates": [
    {
      "id": "cmf1x9q2h0001",
      "name": "Shelf 62 x 29",
      "kind": "roll",
      "source": "custom",
      "labelWidthMm": 62,
      "labelHeightMm": 29,
      "dpi": 300,
      "version": 3
    },
    {
      "id": "dymo-30252",
      "name": "Dymo 30252 Address (3½ × 1⅛ in)",
      "kind": "roll",
      "source": "preset",
      "labelWidthMm": 88.89999999999999,
      "labelHeightMm": 28.575,
      "dpi": 203,
      "version": 1
    }
  ]
}

POST /api/v1/print

Renders one PDF for a template and a list of variants with quantities, records the job in the shop's print history (source API, with the key's name), and returns the bytes as base64 beside the warnings.

Variants are read live from the shop at print time: title, SKU, barcode, price, weight, metafields. You send ids and quantities, nothing else.

With printerId, the PDF goes to that printer of the shop's PrintNode account instead, the way the app's Send to printer sends it: the printer is checked online right before the job goes, the labels go as one job, and the answer names the printer instead of carrying the PDF. The API also checks the printer before anything is counted, so a printer that is off costs nothing. Only a job PrintNode accepted is recorded and counted; a print refused before that answers an error and counts nothing.

curl https://labelcraft.tech/api/v1/print \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "dymo-30252",
    "items": [
      { "variantId": 45678901234567, "qty": 2 },
      { "variantId": "gid://shopify/ProductVariant/45678901234568", "qty": 1 }
    ]
  }'
{
  "jobId": "cmf1xa77c0003",
  "filename": "labels-dymo-30252.pdf",
  "pageCount": 3,
  "labelCount": 3,
  "warnings": [
    {
      "code": "textOverflow",
      "labelIndex": 1,
      "element": "productTitle",
      "message": "Text overflows its box: \"Organic cotton crew neck, heavyweight, oversized fit\""
    }
  ],
  "pdfBase64": "JVBERi0xLjcK..."
}

GET /api/v1/printers

Lists the printers of the PrintNode account connected to the shop in LabelCraft (Settings, Send labels straight to a printer), so a print can go to one of them: printerId on POST /print. online says whether LabelCraft would send to it right now (the printer online in PrintNode and its computer connected); selectedInApp marks the printer the app's own Send to printer uses.

A shop without PrintNode connected answers 409 printnode_not_connected. A key PrintNode refuses, or PrintNode out of reach, answers 502 printnode_error with stage, kind and retryable. The PrintNode key is only ever sent to PrintNode, never back in an answer.

curl https://labelcraft.tech/api/v1/printers \
  -H "Authorization: Bearer YOUR_KEY"
{
  "printers": [
    {
      "id": 73065,
      "name": "Zebra ZD421",
      "state": "online",
      "computerName": "Front desk PC",
      "computerState": "connected",
      "online": true,
      "selectedInApp": true
    },
    {
      "id": 73071,
      "name": "Rollo X1040",
      "state": "offline",
      "computerName": "Warehouse PC",
      "computerState": "connected",
      "online": false,
      "selectedInApp": false
    },
    {
      "id": 73066,
      "name": "Zebra GK420d",
      "state": "online",
      "computerName": "Warehouse PC",
      "computerState": "connected",
      "online": true,
      "selectedInApp": false
    }
  ]
}

GET /api/v1/templates/:id

One template with its full layout: a built-in paper size by slug or one of the shop's non-archived templates by id. The layout is the exact object the editor stores, so a system can read a paper size, change a few fields and POST the result under its own name.

curl https://labelcraft.tech/api/v1/templates/dymo-30252 \
  -H "Authorization: Bearer YOUR_KEY"

POST /api/v1/templates

Creates a template from { name, layout } and answers 201 with the same shape as GET /templates/:id. The layout goes through the editor's own rules: unknown fields are dropped, fields that only make sense on some element types are removed from the others, a logo or a custom font must decode, and an element outside the label is refused with 422 and the box that would fit.

A template created here appears in the app's template list and prints from the app and from POST /print alike.

curl https://labelcraft.tech/api/v1/templates \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Shelf 62 x 29",
    "layout": {
      "kind": "roll", "labelWidthMm": 62, "labelHeightMm": 29, "dpi": 300,
      "elements": [
        { "type": "productTitle", "xMm": 2, "yMm": 2, "widthMm": 58, "heightMm": 8, "fontSize": 9 },
        { "type": "barcode", "xMm": 2, "yMm": 11, "widthMm": 58, "heightMm": 16, "barcodeFormat": "ean13" }
      ]
    }
  }'

PATCH /api/v1/templates/:id

Changes the name, the layout or both. Every change bumps the version and stores an immutable snapshot, so a job printed before the change reprints with the same layout. Built-in paper sizes answer 409 template_read_only: GET the paper size, POST it under your name, then edit that one.

curl -X PATCH https://labelcraft.tech/api/v1/templates/cmf1x9q2h0001 \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Shelf 62 x 29 (v2)" }'

DELETE /api/v1/templates/:id

Archives the template: it leaves the list and can no longer print, and answers 204. Nothing is erased, so past jobs keep reprinting from their snapshots. Built-in paper sizes answer 409.

curl -X DELETE https://labelcraft.tech/api/v1/templates/cmf1x9q2h0001 \
  -H "Authorization: Bearer YOUR_KEY"

POST /api/v1/barcodes/audit

Runs the app's barcode check over a list of values, the same check behind the Barcode check page and the pre-print warnings. Nothing is read from the shop and nothing is written: send the codes your system holds and get, for each, whether it is a valid retail GTIN, missing, invalid (with the corrected value when only the check digit is wrong), a restricted prefix (in-store, coupon, ISBN) or an internal Code 128 value. An 8-digit value the app reads as a UPC-E (starting with 0, outside the 0000 block GS1 leaves empty) gets no corrected value: it may be the UPC-E of a UPC-A, which upcA names.

curl https://labelcraft.tech/api/v1/barcodes/audit \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "values": ["4006381333931", "4006381333932", "SKU-0042"] }'
{
  "results": [
    { "value": "4006381333931", "status": "valid", "format": "GTIN-13", "message": "..." },
    { "value": "4006381333932", "status": "invalid", "suggestion": "4006381333931", "message": "..." },
    { "value": "SKU-0042", "status": "internal", "message": "..." }
  ]
}

Errors

Every error is a JSON object { error: { code, message } } with the extra fields named below. The message says what to change; the code is stable.

StatusCodeWhen
401unauthorizedno bearer token, unknown, malformed or revoked key, or the shop uninstalled the app
402plan_requiredthe key's shop is below the Retail Pro plan; carries plan and pricingUrl
429rate_limitedmore than 60 requests in the trailing minute; carries retryAfterSeconds and a Retry-After header
413payload_too_largethe body is above 1048576 bytes; carries maxBytes
400invalid_requestthe body is not valid; details[] names each field
400too_many_labelsthe quantities sum above 2000; carries max and requested
404template_not_foundno paper size slug and no non-archived template of this shop matches
409template_read_onlyPATCH or DELETE on a built-in paper size; create your own from it
409template_conflictanother change landed on the template while yours was saving; GET it again and retry. Nothing saved
409template_limit_reachedPOST /templates past 200 non-archived templates; carries max and count
500write_faileda template write failed after validation; carries a reference to quote to support. Nothing saved
422layout_not_printablean element sits outside the label; carries faults[] with the element, the overflow per edge and the box that would fit. Nothing saved
422variants_not_foundone or more variant ids returned nothing on this shop; carries missing[]. Nothing printed, nothing counted
409printnode_not_connectedGET /printers, or a print with printerId, on a shop with no PrintNode account connected in LabelCraft. Nothing printed, nothing counted
404printer_not_foundPrintNode lists no printer with that printerId on the account connected to the shop; carries printerId. Nothing printed, nothing counted
409printer_offlinethe printer is not online in PrintNode, or its computer is not connected; carries printer. Nothing printed, nothing counted
502printnode_errorPrintNode refused the key or the job, or could not be reached; carries stage (printers for the list, job for the job), kind, retryable, and printNodeCode when PrintNode gave one. Nothing counted. stage job with kind network or server means PrintNode never confirmed the job, which may still print: retryable is then false, so check the job list in PrintNode before you retry
422held_from_printera print with printerId whose product photo or variants' options could not be read, so labels would have printed without them; carries reason and warnings[]. Nothing printed, nothing counted
405method_not_allowedwrong HTTP method; the Allow header names the right one
503shop_unavailablethe shop's offline access is gone, or Shopify refused the token (uninstalled, frozen or locked store); carries detail. Open LabelCraft once in the Shopify admin of that shop, then retry
500render_failedsomething failed after validation; carries a reference to quote to support. The job was not recorded and nothing was counted

What the API shares with the app

The print endpoint calls the same functions the in-app print calls: the same variant read, the same money formatting, the same metafield resolution, the same brand kit rules for the plan, the same font and barcode floors before and after the merchant's own printer calibration, and the same render engine. A test in the code base fails if one of those calls is replaced by a copy or dropped.

It sends to PrintNode only when printerId names a printer, and then through the functions of the app's Send to printer: the same online check before sending, the same rule that holds a print back, the same job.

What the API does not do: it never asks for a review, never appends the calibration ruler, and never offers the in-app metafield fixes.

Coming next

Generating barcodes for products that have none (writing to the catalogue) is planned as a further slice. It is not available yet, and this page only documents what the code does today.

Questions, or a system you want to connect: support@labelcraft.tech.

Available on Shopify App Store