API reference

MagLab Pro's local REST API: sending print jobs from a shop, script or other system, the job format, webhooks, order feeds and field mapping.

MagLab Pro (and the free trial) can take print jobs from other programs through a local API: a small web server built into MagLab. This page is for whoever connects a shop, script or other system to it, and assumes you’re comfortable with JSON and HTTP. For a simpler overview, see section 5 of the user guide.

Turn it on in Automate → Local API. It listens on http://localhost:5719, so only this computer can reach it, unless you change the port or turn on Allow other computers on my network. With MagLab running, open that address in a browser for interactive docs, or fetch the OpenAPI 3 spec from /api/v1/openapi.json.

Authentication

Every endpoint except /, /api/v1/health and /api/v1/openapi.json needs the API key shown in Automate → Local API (it starts with ml_). Send it in one of these ways:

Authorization: Bearer ml_xxxxxxxx
X-API-Key: ml_xxxxxxxx
?key=ml_xxxxxxxx        (for webhook senders that can't set headers)

A wrong or missing key gets 401. Errors come back as JSON: { "error": "…" }.

Endpoints

Method Path Description
GET /api/v1/health { ok, version, paused, queued, licence, workingHours }; licence is ok, needs-pro or inactive; see Working hours
GET /api/v1/templates Templates: id, name, perSheet, page size
GET /api/v1/templates/{id} Full template JSON (.mltpl format)
GET /api/v1/products Saved products: id, name, code (SKU), template, sheets, filled
GET /api/v1/printers { default, printers: [{ name, displayName, isDefault }] }
POST /api/v1/jobs Queue a job → 202 with the job
POST /api/v1/render Same body → returns application/pdf (no printing); 402 if the licence doesn’t include automation
GET /api/v1/jobs?limit=50 Recent jobs, newest first (up to 400)
GET /api/v1/jobs/{id} Job status
GET /api/v1/jobs/{id}/pdf The job’s PDF (true size, no calibration)
GET /api/v1/jobs/{id}/pdf/{n} The PDF of the job’s nth extra document (documents[n-1])
POST /api/v1/jobs/{id}/retry Re-run a failed or finished job
DELETE /api/v1/jobs/{id} Cancel a job that hasn’t started (409 once it has)
POST /api/v1/hooks/{hookId} Inbound webhook with field mapping → 202 with the job, or 200 when nothing was queued (see Incoming webhooks)
POST /api/v1/queue/pause · /resume Pause/resume automatic printing

Job status moves through queued → preparing → rendering → printing → done, or ends at failed / cancelled. Shopify orders can wait first: backlog (placed before the shop was connected), held (arrived while the shop’s Print automatically was off) or collecting (waiting to share sheets with other orders). Backlog and held orders wait until someone prints them (queued) or skips them (cancelled). Collecting orders are replaced by one job for the shared sheets when they print.

Jobs print automatically with MagLab Pro (and during the free trial). Without it they are still accepted and wait as queued, then print once MagLab is upgraded or activated; /api/v1/health reports why in licence.

Working hours

When working hours are switched on in MagLab (Automate → Working hours), jobs that print are only started during them. A job that arrives outside working hours is still accepted (202) and waits as queued, with two extra fields:

{ "status": "queued", "waitingFor": "working-hours", "waitingUntil": "2026-10-18T07:00:00.000Z" }

waitingUntil is when working hours next start (null if no days are chosen). Both are null for every other job. Waiting jobs print in the order they arrived when working hours start, and a job that has started printing finishes. Jobs that only save a PDF ("print": false) and /api/v1/render don’t wait.

Add "ignoreWorkingHours": true to a job to print it straight away. POST /api/v1/jobs/{id}/retry waits for working hours like a new job. /api/v1/health reports the current state:

"workingHours": { "enabled": true, "open": false, "opensAt": "2026-10-18T07:00:00.000Z", "closesAt": null }

open is true whenever printing may run (always, while working hours are off). opensAt is set while closed, closesAt while open (null if they never close). Times are in UTC; MagLab uses the computer’s own clock and time zone for the hours themselves.

Job format

{
  "template": "button-2.25in-letter",
  "reference": "Order #1042",
  "items": [
    { "image": "https://cdn.example.com/a.jpg", "quantity": 9 },
    { "image": "/Users/me/Orders/1042/b.png", "fit": "contain" },
    { "imageBase64": "data:image/jpeg;base64,/9j/4AAQ...", "filename": "c.jpg", "quantity": 3 },
    { "text": "Happy birthday\nSam", "quantity": 2 },
    { "image": "https://cdn.example.com/d.jpg", "text": "MAX", "quantity": 1 }
  ],
  "print": { "printer": "EPSON_ET_8500_Series", "copies": 1, "calibrate": true },
  "savePdf": "/Users/me/MagLab/Printed",
  "cutLines": true,
  "dpi": 300,
  "callbackUrl": "https://example.com/hooks/maglab",
  "imageHeaders": { "Referer": "https://mystore.example/" },
  "data": { "customer": { "name": "Ann Lee" }, "gift": true },
  "documents": [
    { "name": "Address label", "template": "label-thermal-4x6",
      "items": [ { "text": "Ann Lee\n1 Elm St\nAustin, TX 78701" } ],
      "print": { "printer": "Label_Printer" } }
  ]
}
Field Notes
template Template id or exact name (GET /api/v1/templates lists them). Defaults to the API’s default template. Not needed when every item is a product.
reference Your name for the job, such as an order number. Shown in the queue and printed by {$reference}.
items[] image (http/https URL, data: URL or local file path), imageBase64, filename, text, quantity (0–5000, default 1), fit (cover or contain), size (on a packed die-cut template, a sticker’s longest side: "3in", "76mm" or millimetres). Each item needs an image or text, or is a saved product: { "product": "Cat stickers", "quantity": 2 }.
print true/false, or { "printer", "copies" (1–99), "calibrate" }. Omit to use the API’s default output. calibrate: false prints without the printer’s calibration.
savePdf Folder to save a copy of the PDF in.
cutLines true/false: print the template’s cut lines.
dpi Resolution photos are prepared at, 72–600 (default 300).
callbackUrl Receives job.completed / job.failed for this job.
ignoreWorkingHours true prints the job straight away, even outside working hours.
imageHeaders Headers sent when downloading image URLs, e.g. a Referer for an image host with hotlink protection. An object or a list of [name, value] pairs.
data Any JSON object. The template’s sheet text can show its fields with {field} placeholders (see Sheet text).
documents[] Up to 10 further documents printed with this job, each with its own template, items and optional name, print, savePdf and cutLines (unset values follow the main sheet).

Items fill shapes in the template’s reading order and spill onto extra sheets as needed.

Products. A saved product (Layout → Save as product) is a template filled with designs, such as a sticker sheet you sell. An item { "product": "Cat stickers", "quantity": 2 } prints its sheets twice, exactly as laid out, on the product’s own template. Name it by its name, SKU/code or id (GET /api/v1/products). Products on a different template from the job’s print as an extra document of the job, after its photos; a job of products alone prints them as its main sheet. An unknown product answers 404.

A job prints completely or not at all. MagLab downloads every photo and lays out every document first, then checks that every printer the job needs is installed. If one is missing, nothing prints and the job fails with the printer’s name. If printing stops part-way (for example the label printer jams), the job fails but remembers what already printed: retry prints only the rest, while print again on a finished job reprints everything.

Job responses include documents (each with its result), plus acknowledged and ackError for jobs from order feeds, and waitingFor and waitingUntil for jobs waiting for working hours.

Multipart upload

curl -X POST http://localhost:5719/api/v1/jobs \
  -H "Authorization: Bearer $KEY" \
  -F 'payload={"template":"magnet-rect-2.5x3.5in-letter","quantities":{"dog.jpg":9}}' \
  -F images=@dog.jpg -F images=@cat.jpg

Any file field is treated as an image. quantities maps file names to counts; otherwise quantity (default 1) applies to every file.

Get a PDF without printing

curl -X POST http://localhost:5719/api/v1/render -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -o sheet.pdf \
  -d '{"template":"label-avery-5160","items":[{"text":"Ann Lee\n1 Elm St\nAustin, TX 78701"}]}'

Webhooks MagLab sends

When a job finishes, MagLab POSTs to the URL configured in Automate → Webhooks & order feeds, and to the job’s callbackUrl:

{ "event": "job.completed", "sentAt": "2026-10-11T14:09:00Z",
  "job": { "id": "job_…", "status": "done", "reference": "Order #1042",
           "result": { "pages": 2, "placed": 18, "printer": "EPSON_ET_8500_Series", "pdf": "/api/v1/jobs/job_…/pdf" } } }

event is job.completed or job.failed. Headers: X-MagLab-Event, and X-MagLab-Signature: sha256=<hex HMAC-SHA256 of the raw body> signed with the Signing secret shown in MagLab.

// Node.js verification
const sig = 'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(req.headers['x-maglab-signature']));

Incoming webhooks

POST /api/v1/hooks/{hookId} takes an order as JSON, in whatever shape the sender uses, and turns it into a job with the webhook’s field mapping. The answers:

Status When Body
202 The order was queued The job
200 The order has nothing to print with this mapping, such as a shop order with no photo product {"printed": false, "reason": "no printable items", "reference": "#1001"}
200 This delivery was already received (see below) {"printed": false, "reason": "already received", "reference": "#1001"}
404 Unknown webhook id, or the webhook is switched off {"error": "…"}

An order that isn’t queued still gets 200, because shops such as Shopify treat any other status as a failed delivery, keep resending it, and can remove a webhook that keeps failing. Both cases are noted in Automate → Activity. reference is null when the mapping finds no reference.

Resent orders print once. Senders resend an order when MagLab’s answer is slow or gets lost. Each webhook remembers its last 3000 deliveries, by the sender’s delivery id (the X-Shopify-Webhook-Id or Idempotency-Key header) or, without one, by the order reference from the field mapping. Without a delivery id header, point Order reference at something unique such as the order number: two orders with the same reference would print only once. Only queued orders are remembered. To send the same test order again, click Forget received orders in the webhook’s settings.

Field mapping (incoming webhooks and order feeds)

Paths select values from JSON:

Path Meaning
id key
customer.name nested key
line_items[] every element of an array
line_items[0] first element
properties[name=Photo].value the element whose name is Photo (case-insensitive), then its value
items[].photos[] every photo of every item (arrays nest)
^quantity from inside an item: the object it was found in, e.g. a photo’s line item (^^ goes up two)
$.order_id from inside an item: the whole order

A mapping has these parts:

  • Items: the list of products, e.g. line_items[], or items[].photos[] when each line item holds several photos. Leave it empty if the payload itself is a single item (for example one address label per order).
  • Photo URL, Quantity, Text and Reference: read from each item or the order. A quantity on the line item rather than each photo is ^quantity.
  • Only links are photos. A Photo URL value is used when it’s an http:// or https:// link, a data: URL, or a //… link (read as https://). Any other value, such as an upload app’s hidden _upload_id or a text option like Gloss, isn’t treated as a photo, and an item with neither a photo link nor text is left out.
  • Template override (optional): if the payload carries a template id, it’s used instead of the hook’s default template.
  • Product (optional): a path to a saved product’s name or SKU on each item, e.g. sku. An item without a photo whose value names a saved product prints that product’s sheets, once for each one ordered, on the product’s own template. Values that aren’t a saved product (other things your shop sells) print their text if they have any, and are otherwise left out.

Placeholders

Text (and acknowledgement URLs and bodies) use {path} placeholders:

Placeholder Result
{customer.name} the field’s value
{address.country|country} a 2-letter country code as a name: FR → France (in the computer’s language)
{name|upper} · {name|lower} upper/lower case
{gift ? GIFT} · {gift ? Gift : Not a gift} text depending on a yes/no field (empty, 0, false and no count as no)

In text, a line whose placeholders all come out empty is left out, so a missing “Address 2” doesn’t leave a gap.

Text:  {shipping.name}
       {shipping.address.line1}
       {shipping.address.line2}
       {shipping.address.city} {shipping.address.postal_code}
       {shipping.address.country|country}

Shopify example

Product personalisation apps usually store the uploaded photo as a line-item property:

Items:      line_items[]
Photo URL:  properties[name=Photo].value
Quantity:   quantity
Reference:  name            (e.g. "#1042")

Webhook URL to give Shopify: https://<your tunnel>/api/v1/hooks/<hookId>?key=<API key>.

Order feeds (pull)

MagLab requests your URL every N seconds (GET or POST, with any headers such as Authorization) and expects JSON:

{ "orders": [ { "id": "A-1001", "items": [ { "image": "https://…/a.jpg", "quantity": 9 } ] } ] }
  • Set List of orders to orders[] and Order id to id. Each id is printed only once; MagLab remembers the last 3000 ids.

  • Load a sample order from the feed fetches your URL once and uses its first order to test every mapping. Nothing is printed.

  • Also print for each order: add documents such as an address label on a label printer. Each has its own template, fields and printer; leave its List of items empty for one per order. They print together with the main sheet, under the rules in Job format.

  • Photo download headers are sent when downloading the order’s photos (not to your API). Some image hosts’ hotlink protection refuses downloads without, for example, Referer: https://your-shop.example/.

  • Acknowledge: after everything for an order has printed, MagLab calls your URL, e.g. PATCH https://store.example/api/orders/{id}/printed, with the feed’s headers (except Accept), so an Authorization header works for both. The body is one of:

    • MagLab standard: {"status":"printed","reference":"A-1001","jobId":"…","printedAt":"…"}
    • Custom JSON: your own template. Order fields go inside quotes ("order": "{order_id}"); {$printedAt}, {$reference}, {$jobId} and {$pages} describe the print. For example {"printed": true, "printedAt": "{$printedAt}"}.
    • No body.

    If your server doesn’t answer with a 2xx status, the job still counts as printed (it won’t print again) and the queue shows Server not notified with a Notify again button.

This is the easiest way to connect a website: expose an authenticated endpoint that lists paid, unprinted orders. No tunnel or open port is needed.

Sheet text and placeholders

Templates can print pictures, text and lines on every sheet (Designer → Pictures, text & lines): a logo on every label, a header, an order number by every shape, a website on a fold-over flap. Their text takes the placeholders above, filled from the job’s order (or data), plus:

Variable Value
{$reference} the order number / job reference (a manual job’s name)
{$page}, {$pages} sheet number and sheet count
{$slot} the shape’s number (text by every shape)
{$order} the order the shape is for, when several orders share a sheet; otherwise the same as {$reference} (text by every shape)
{$orders} the orders on the sheet, each with its shapes’ numbers: #1001 (1–9), #1002 (10–12)
{$photo} the photo’s file name (text by every shape)
{$text} the shape’s own text (text by every shape)
{$date}, {$time}, {$name}, {$template} today’s date, the time, the job name, the template name

Example header: Order {$reference} · page {$page} of {$pages} {social_consent ? · SOCIAL}.

To fill order fields, a job keeps a copy of its order (up to 128 KB; larger orders print with those fields empty and a warning in the activity log). Jobs whose templates don’t use order fields don’t keep it.