Integration guide: printing from another appHome & Business

A worked walkthrough for connecting a point-of-sale, inventory or shipping app to Laberty, with examples in curl, JavaScript and Python.

This guide connects another app to Laberty so it can print: order labels, shelf labels, receipts. Your app says "print these lines on that printer". Laberty renders the label at the right size, checks the right labels are loaded, and hands the job to the print bridge on your network. Your app needs no printer drivers and no container of its own.

You'll need:

  • A Home plan (20 API actions a month, enough to try it) or Business (unlimited).
  • A print bridge running on the same network as the printers.
  • Your printers added on Admin → Printers & stock, each with its IP address and In it now set.

Every endpoint and field is documented in the API reference. This guide is the path through it.

The one thing to understand: printing is a queue

POST /api/print answering 201 means the job is queued, not printed. The bridge picks it up within about five seconds, prints it, and reports back. To know whether it really came out, check GET /api/print/{id}.

So design your app for three outcomes: sent, printed, and failed, with a reason.

Step 1: Make a Printing key

  1. In Laberty, go to Admin → API keys.
  2. Name the key after your app, for example "Front counter POS".
  3. Choose Printing and press Make key.
  4. Copy the key. It's shown once.

Store it in your server's configuration, for example an environment variable named LABERTY_API_KEY. Never put it in browser JavaScript or in an app people download. If your app runs only in a browser, put a small server in between that holds the key. More in API keys explained.

Step 2: List the printers

curl https://laberty.app/api/printers \
  -H "Authorization: Bearer $LABERTY_API_KEY"
{
  "bridge": { "online": true, "lastSeenAt": "2026-09-13 14:02:11" },
  "printers": [
    {
      "id": 1, "name": "Kitchen QL-820NWB", "isDefault": true,
      "type": "label", "accepts": ["pdf"],
      "loadedStock": { "id": 3, "code": "2205", "name": "Continuous 2.4in roll (2205)", "kind": "continuous", "widthIn": 2.4, "heightIn": null, "dkPart": "DK-2205" }
    },
    {
      "id": 2, "name": "Front counter receipt", "isDefault": false,
      "type": "receipt", "accepts": ["escpos", "raw"], "loadedStock": null
    }
  ]
}

The fields that matter:

  • id: what you send as printerId.
  • type: label, receipt or tape.
  • accepts: the raw job kinds this printer understands.
  • loadedStock: what Laberty believes is in the printer. code and dkPart are what you send as stock.
  • bridge.online: whether the bridge picked up a job in the last five minutes. An idle bridge reports false while being perfectly healthy, so treat it as a hint.

Step 3: Pick the right printer

Choose by type, and let the person setting up your app confirm the choice once. Store the printer id in your app's settings.

const { printers } = await (await fetch('https://laberty.app/api/printers', {
  headers: { Authorization: `Bearer ${process.env.LABERTY_API_KEY}` },
})).json()

const labelPrinter = printers.find((p) => p.type === 'label' && p.isDefault) || printers.find((p) => p.type === 'label')
const receiptPrinter = printers.find((p) => p.type === 'receipt')

if (!receiptPrinter) {
  // Say it plainly, rather than failing later
  console.log('No receipt printer is set up in Laberty yet.')
}

If a printer you stored disappears (someone removed it), POST /api/print answers 404. Fetch the list again and ask the user to choose.

Step 4: Send a label

Send label with lines of text. Laberty renders plain, legible text at the exact size of the labels: as long as the text needs on a continuous roll, or exactly the label's size on die-cut stock. Up to 20 lines, shrunk to fit.

Always send stock: the labels this job was designed for, as a code ("2205"), a DK part ("DK-2205") or a stock id. If different labels are recorded as loaded, the job is refused before anything is queued. And always send an Idempotency-Key (see step 8).

curl -X POST https://laberty.app/api/print \
  -H "Authorization: Bearer $LABERTY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-label" \
  -d '{
        "printerId": 1,
        "stock": "2205",
        "label": { "lines": ["Jane Smith", "Order 1042", "2 x Sourdough"], "align": "center" },
        "copies": 1,
        "detail": "Order 1042"
      }'
const res = await fetch('https://laberty.app/api/print', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LABERTY_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': `order-${order.id}-label`,
  },
  body: JSON.stringify({
    printerId: labelPrinter.id,
    stock: '2205',
    label: { lines: [order.customerName, `Order ${order.id}`, order.summary] },
    detail: `Order ${order.id}`,
  }),
})
const data = await res.json()
// 201 = queued, 200 = a retry of a job already queued (data.duplicate === true)
import os
import requests

res = requests.post(
    "https://laberty.app/api/print",
    headers={
        "Authorization": f"Bearer {os.environ['LABERTY_API_KEY']}",
        "Idempotency-Key": "order-1042-label",
    },
    json={
        "printerId": 1,
        "stock": "2205",
        "label": {"lines": ["Jane Smith", "Order 1042", "2 x Sourdough"]},
        "detail": "Order 1042",
    },
    timeout=15,
)
data = res.json()

A queued job answers:

{ "jobId": 88, "status": "queued", "kind": "pdf", "printer": { "id": 1, "name": "Kitchen QL-820NWB" }, "stock": { "code": "2205" }, "statusUrl": "/api/print/88" }

Step 5: Send a receipt

Send receipt to a printer whose type is receipt. Each item in lines is one of:

ItemPrints
"plain text"A left-aligned line, wrapped to the paper
{ "text": "TOTAL", "align": "center", "bold": true, "size": "big" }align is left, center or right. size is normal, tall, wide or big
{ "key": "Coffee", "value": "$3.00" }Coffee..............$3.00 across the paper
{ "rule": true }A line of dashes across the paper
{ "feed": 2 }Blank lines

The paper is cut at the end; send "cut": false to feed a little paper instead. Add "openDrawer": true to open a cash drawer plugged into the printer. Receipts don't need stock.

await fetch('https://laberty.app/api/print', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LABERTY_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': `order-${order.id}-receipt`,
  },
  body: JSON.stringify({
    printerId: receiptPrinter.id,
    receipt: {
      lines: [
        { text: 'MAPLE STREET BAKERY', align: 'center', size: 'big' },
        { rule: true },
        ...order.items.map((item) => ({ key: item.name, value: item.price })),
        { rule: true },
        { key: 'TOTAL', value: order.total },
        'Thank you!',
      ],
      cut: true,
      openDrawer: order.paidInCash,
    },
  }),
})

Step 6: Handle refusals

A refusal comes back straight away, with an HTTP status and JSON like { "error": "…", "code": "…" }. Nothing was queued. The error text is written for people, so you can show it as it is.

StatuscodeWhat happenedWhat your app should do
409stock_mismatchDifferent labels are recorded as loadedShow error. It names the labels to load. Your admins were emailed
409stock_unknownNothing is recorded as loaded in that printerAsk an admin to set loaded stock on Printers & stock
422wrong_kindA label sent to a receipt printer, or the reverseA bug in your printer choice. The response lists what the printer accepts
400unknown_stockNo label size matches your stockCheck the code on Printers & stock
404The printer no longer existsFetch printers again
429Over 60 jobs a minuteWait Retry-After seconds, then retry with the same Idempotency-Key
401 / 403Key revoked, wrong kind of key, or planTell an admin. Retrying won't help
if (res.status === 201 || res.status === 200) {
  return { queued: true, jobId: data.jobId }
}
if (res.status === 409) {
  // stock_mismatch or stock_unknown: a person needs to act at the printer
  return { queued: false, message: data.error, needsPerson: true }
}
if (res.status === 429) {
  const wait = Number(res.headers.get('Retry-After') || 60)
  return { queued: false, retryAfterSeconds: wait }
}
return { queued: false, message: data.error || `Laberty answered ${res.status}` }

The full list, with a sentence for each, is in Error codes and what to tell your users.

Step 7: Find out whether it printed

Check the job until it's done or failed. Once every second or two is plenty: the bridge only checks for work every five seconds. Give up after a minute or so and tell the user the printer hasn't picked it up.

async function waitForPrint(jobId, timeoutMs = 60000) {
  const started = Date.now()
  while (Date.now() - started < timeoutMs) {
    const res = await fetch(`https://laberty.app/api/print/${jobId}`, {
      headers: { Authorization: `Bearer ${process.env.LABERTY_API_KEY}` },
    })
    const job = await res.json()
    if (job.status === 'done') return { ok: true }
    if (job.status === 'failed') return { ok: false, code: job.errorCode, message: job.error }
    await new Promise((resolve) => setTimeout(resolve, 1500))
  }
  return { ok: false, code: 'timeout', message: 'The printer has not picked this up yet. Is the print bridge running?' }
}
import time

def wait_for_print(job_id, timeout=60):
    started = time.time()
    while time.time() - started < timeout:
        job = requests.get(
            f"https://laberty.app/api/print/{job_id}",
            headers={"Authorization": f"Bearer {os.environ['LABERTY_API_KEY']}"},
            timeout=15,
        ).json()
        if job["status"] == "done":
            return True, None
        if job["status"] == "failed":
            return False, job.get("errorCode") or job.get("error")
        time.sleep(1.5)
    return False, "timeout"

status is one of queued, claimed (the bridge has it), done or failed. When a job fails, error says why in plain language, and errorCode is set when your app can act on it:

errorCodeTell the user
stock_mismatchThe wrong labels are in the printer. Load the right ones and print again
no_mediaThe printer is out of labels
printer_errorThe printer needs attention, for example its cover is open
unreachableLaberty can't reach the printer. Is it on?
no_addressThe printer has no IP address in Laberty yet. Ask an admin
nullShow the error text

For the first three, nothing was printed and your admins were emailed.

Step 8: Retry without printing twice

Networks drop requests. If your app retries a print, the customer must not get two labels. Send an Idempotency-Key header that's unique per real-world print, such as the order number plus the kind of label.

  • A retry with the same key returns the first job (200, "duplicate": true) instead of queueing another.
  • A genuine reprint needs a new key, such as order-1042-label-reprint-1.
  • Keys can be up to 100 characters.

Step 9: When staff swap a roll

If your app tells staff to change labels, it should tell Laberty too, so the next job isn't refused:

curl -X POST https://laberty.app/api/printers/1/loaded-stock \
  -H "Authorization: Bearer $LABERTY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "stock": "208" }'

This needs a Full access key, because it changes what every later job is checked against. With a Printing key, ask staff to press Swap roll on the printer's card on Printers & stock instead.

The roll is recorded as not sure: jobs are sent, and the printer reports a wrong roll. Only a Laberty device at the printer confirms a roll for sure. If your app truly knows what's loaded, an admin can tick Apps confirm it through the API on Printers & stock, and then "confirmed": true confirms it. Without that, the answer says "confirmed": false with a confirmNote explaining why.

Before you go live

  • The key lives on your server, not in the browser or a downloadable app.
  • Printers are chosen by type, and the id is stored in settings.
  • Every label job sends stock.
  • Every job sends an Idempotency-Key.
  • 409, 422 and 429 each show a clear message.
  • Your app checks each job's status and shows errorCode problems in plain words.
  • You've tried it with the wrong loaded stock recorded, and seen the 409.

API printing isn't metered or billed per job. See Limits for the guards that do exist.

Still stuck?

Contact support and tell us what you tried. We reply by email, usually within 1–2 business days.