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 is part of the Warehouse 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 Warehouse plan answers 402.
curl https://labelcraft.tech/api/v1/templates \
-H "Authorization: Bearer lc_live_0123456789abcdef0123456789abcdef01234567"Limits
- 60 requests per minute per key. Above that, 429 with a Retry-After header, in seconds.
- 2000 labels per print request, summed over the quantities. Above that, 400 too_many_labels; split the job.
- 500 items (variant and quantity pairs) per print request. Above that, 400 invalid_request with the count in details[].
- A body of at most 1048576 bytes, enough for a template carrying a brand font and a logo. Above that, 413 payload_too_large.
- 200 non-archived templates per shop through POST /templates. Above that, 409 template_limit_reached; archive one or PATCH an existing one.
- The rate window lives in the memory of the single machine that serves the app, so it resets on a deploy. Nothing is queued: a refused request is not printed.
- A print request is not idempotent. If your call times out after the server answered, retrying prints the batch again and records a second job.
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.
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.
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 }
]
}'- template (string, required): a paper size slug or one of the shop's template ids, as listed by GET /templates.
- items (array, required): one entry per line, { variantId, qty }. variantId is a numeric id, a numeric string or a full gid; qty is a positive integer. The same variant may appear more than once.
- startPosition (integer, optional): for sheet templates, the index of the first label to use on the first sheet, from 0. Ignored on rolls.
- locale (en, fr, de or es, optional): the format of a printed date element and of prices on the label. Warning codes are stable and their messages are English; the app translates them by code on its own screens. Default en.
{
"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..."
}- jobId: the print history row. It reprints from the app like any other job.
- pageCount and labelCount: what the PDF holds. labelCount is the sum of the quantities.
- warnings: never empty when something did not fit or a field came back empty. Same codes and sentences as the app. A print with warnings still returns the PDF; decide on your side whether to send it to the printer.
- pdfBase64: the PDF, base64. Decode it and hand it to your printer or your print server as is; it is already at the template's exact size.
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" }
]
}
}'- name (string, required): 1 to 80 characters.
- layout.kind (roll or sheet), layout.labelWidthMm and layout.labelHeightMm (mm, above 0), layout.dpi (integer, default 203).
- layout.sheet (required on a sheet): pageWidthMm, pageHeightMm, marginTopMm, marginLeftMm, gutterXMm, gutterYMm, columns, rows.
- layout.calibration (optional): offsetXMm and offsetYMm, within 5 mm of 0.
- layout.elements (1 to 60): each with type, xMm, yMm, widthMm, heightMm, and the optional fields of that type (fontSize, bold, align, overflow, value, color, barcodeFormat, qrSource, includeText, serial, imageData for a logo). GET a built-in paper size to see a complete example of every type you need.
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 exactly as it did. 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.
curl https://labelcraft.tech/api/v1/barcodes/audit \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "values": ["4006381333931", "4006381333932", "SKU-0042"] }'- values (array of strings, required): 1 to 500 values of at most 64 characters.
{
"results": [
{ "value": "4006381333931", "status": "valid", "format": "GTIN-13", "message": "..." },
{ "value": "4006381333932", "status": "invalid", "suggestion": "4006381333931", "message": "..." },
{ "value": "SKU-0042", "status": "internal", "message": "..." }
]
}- status: valid, missing, invalid, restricted or internal. suggestion appears only when a corrected value exists. prefix names the GS1 range when it is known.
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.
| Status | Code | When |
|---|---|---|
| 401 | unauthorized | no bearer token, unknown, malformed or revoked key, or the shop uninstalled the app |
| 402 | plan_required | the key's shop is below the Warehouse plan; carries plan and pricingUrl |
| 429 | rate_limited | more than 60 requests in the trailing minute; carries retryAfterSeconds and a Retry-After header |
| 413 | payload_too_large | the body is above 1048576 bytes; carries maxBytes |
| 400 | invalid_request | the body is not valid; details[] names each field |
| 400 | too_many_labels | the quantities sum above 2000; carries max and requested |
| 404 | template_not_found | no paper size slug and no non-archived template of this shop matches |
| 409 | template_read_only | PATCH or DELETE on a built-in paper size; create your own from it |
| 409 | template_conflict | another change landed on the template while yours was saving; GET it again and retry. Nothing saved |
| 409 | template_limit_reached | POST /templates past 200 non-archived templates; carries max and count |
| 500 | write_failed | a template write failed after validation; carries a reference to quote to support. Nothing saved |
| 422 | layout_not_printable | an element sits outside the label; carries faults[] with the element, the overflow per edge and the box that would fit. Nothing saved |
| 422 | variants_not_found | one or more variant ids returned nothing on this shop; carries missing[]. Nothing printed, nothing counted |
| 405 | method_not_allowed | wrong HTTP method; the Allow header names the right one |
| 503 | shop_unavailable | the 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 |
| 500 | render_failed | something 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.
What the API does not do: it never asks for a review, never pushes to PrintNode, never appends the calibration ruler, and never offers the in-app metafield fixes. Delivery to a printer is yours.
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.