One POST, every finding, with the exact command and offset and a concrete fix. The same engine the web tool runs — there is only one.
Send X-API-Key. Without a key you get the free tier: the same 43 rules, lower rate limits, no monthly render quota to exhaust. Keys are issued by us and shown once.
POST /api/lint
curl -X POST https://labelproof.dev/api/lint \
-H 'Content-Type: application/json' \
-H 'X-API-Key: lp_...' \
-d '{
"zpl": "^XA^FO50,50^BY2^BCN,100,Y,N,N^FD>;12345^FS^XZ",
"dpi": 203,
"profile": "gs1_logistic",
"render": true
}'dpi and profile are required and have no defaults. There is no universal minimum barcode size and every dot-to-millimetre conversion depends on the printhead, so guessing either one produces confident, wrong answers. The API refuses rather than assumes. profile is gs1_logistic, gs1_other or non_gs1.
render defaults to true. Send false and you get 39 of the 43 rules for no render quota at all — the four that need pixels are the geometry rules (quiet zones, overlap).
{
"dpi": 203,
"rulesChecked": 43,
"renderUsed": true,
"tier": "developer",
"findings": [
{
"ruleId": "FLD-C128C-ODD-001",
"severity": "error",
"message": "Subset C encodes digits in pairs. This payload has an odd number of digits, so the last one is dropped.",
"fix": "Pad to an even digit count, or switch to Subset B (>:).",
"commandText": "^BC",
"offset": 23
}
]
}severity is error (will fail), warning (will look wrong) or info (works, non-standard). An empty findings array means nothing was found — check rulesChecked to see what that silence covers.
GET /api/rules — every rule with its message, fix and categoryGET /api/rules/{id} — one rule, with a broken exampleGET /api/pricing — tiers, quotas and prices, as the page reads themThese need no key. The rule pages are generated from the same data.
Pin labels and get told when a check changes — not because your template changed, but because ours did.
POST /api/suites · GET /api/suitesPOST /api/suites/{id}/labels — pin a label and take its baselinePOST /api/suites/{id}/check — re-check nowPUT /api/suites/{id}/schedule — run it hourly or daily and send a signed webhookA new finding or a changed rendered image is a regression. A resolved finding is deliberately not — we ship catalogue changes constantly, and alerting on those would page you on every release. See the catalogue and the alerting rules in the repository’s SUITES.md.
Send a label to a printer on your own network. The agent polls outbound, so nothing needs a firewall port opened.
POST /api/printers — register a printer, get an agent keyPOST /api/printers/{id}/jobs — queue a labelGET /api/jobs/{id} — what happened to itAlways send an Idempotency-Key header. A retried request without one queues a second physical label, and clients retry on timeouts and deploys.
There is no status called “printed”. A printer on raw TCP 9100 accepts bytes and says nothing back, so sent is the honest ceiling: the printer took the data. Whether a label came out — ribbon, media, head latch — is not something that connection can tell you, and an API that claimed otherwise would be reporting a guess as a fact.
Limits are per minute and per tier; see pricing. Over the limit returns 429 with a Retry-After header. A 402 or 403 names exactly what was exceeded and what it costs.
The labelproof CLI wraps this API for shells and CI, and a GitHub Action wraps the CLI. Nothing to install:
npx labelproof labels/ --dpi 203 --profile gs1_logistic
On npm, MIT, no dependencies. Exit code 2 means the check could not be completed — never treat it as a pass.