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[], oritems[].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://orhttps://link, adata:URL, or a//…link (read ashttps://). Any other value, such as an upload app’s hidden_upload_idor a text option likeGloss, 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 toid. 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 (exceptAccept), so anAuthorizationheader 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.
- MagLab standard:
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.