Skip to content

QR labels and binding

The printed QR

Every device leaves the factory with a QR sticker containing a URL:

https://web.linter-iot-stg.icent.digital/v/<payload>

The payload has eight segments separated by dots:

order.mac.client.icc.partNumber.subsidiary.distributor.cif
#SegmentExampleNotes
1orderPED-2026-04-1832Manufacturing order / purchase order
2maca4cf12bb73d2Normalized to lowercase → deviceId
3clientAutocolorInformational label only: it does not assign the device
4icc8934…ICC of the SIM (empty when the device is WiFi)
5partNumberEJETMust exist in the device-type catalog
6subsidiaryESCode of the subsidiary the device is sold through. Required on new labels.
7distributorPCCode of the distributor, optional. It must belong to that subsidiary.
8cifB12345678Fiscal ID (CIF) of the end client, optional, when it is already known

For example PED-2026-04-1832.a4cf12bb73d2.Autocolor..EJET.ES.PC is a WiFi device (empty ICC) for subsidiary ES and distributor PC, with no end client yet. The payload travels URL-encoded as a single path segment.

  • Empty segments stay empty between the dots, and empty trailing segments are left out. QRs printed with the earlier five-segment layout (order.mac.client.icc.partNumber) are still valid: they simply carry no commercial chain.
  • No value can contain a dot, because the dot is the separator. The label generator removes dots from the free fields (order, client, ICC) and rejects codes, CIFs and part numbers that contain one.
  • Codes are matched exactly first and then ignoring case (only when a single record matches); CIFs exactly, then in upper case, then ignoring case. The label always prints them as they are stored.
  • The API and the web never return the commercial chain (names or codes) to client roles or on the public pre-login page. The codes themselves are still readable in the printed QR.

The API accepts the QR both as the URL and as the raw payload in every binding endpoint.

Generating labels

Labels are generated by GET /v1/labels (ROOT only). The web application’s Labels page uses it with the user’s session; a factory can call it directly with a ROOT API token, either in the key query parameter (the Generation URL (for the factory) shown on that page) or as Authorization: Bearer lit_….

Terminal window
curl -o label.svg "https://api.linter-iot-stg.icent.digital/v1/labels?mac=a4cf12bb73d2&partNumber=EJET&filial=ES&distribuidor=PC&pedido=PED-2026-04-1832&key=lit_…"
ParameterRequiredNotes
macYesMAC of the device (4–32 characters; separators are removed)
partNumberYesModel part number (up to 40)
filialYesSubsidiary code (up to 40)
distribuidorDistributor code of that subsidiary (up to 40)
cifFiscal ID of the end client (up to 40)
pedidoOrder (up to 60)
clienteClient text printed in the QR (up to 60); informational
iccICC of the SIM (up to 30)
register1 or true: also register the device (below). 0/false or absent: label only.
textText printed next to the QR (up to 220; \n breaks lines)
baseWeb URL for the QR (default: the platform’s web address)
formatsvg (default) or png
widthPNG width in pixels, 200–4000 (default 1000)
variantLabel layout (1)
keyROOT API token, as an alternative to the Authorization header

The response is the image (image/svg+xml or image/png). Errors:

StatuscodeWhen
401 / 403unauthorized / forbiddenNo valid credential, or not ROOT
422subsidiary_code_requiredfilial is missing
422code_has_dotA code, CIF or part number contains a dot
422subsidiary_code_unknownNo subsidiary with that code
422distributor_code_unknownNo distributor with that code
422chain_incoherentThe distributor belongs to another subsidiary
422client_fiscal_id_unknownNo client with that CIF

Factory registration

With register=1 the same call also registers the device under the printed chain, so it appears in the lists and the tree of its subsidiary, distributor or client before anyone scans it. The web’s preview never registers anything; its download does when Register the device on the platform when downloading is ticked (the default).

  • New MAC → the device is created in REGISTERED state with the model, the order, the client text, the ICC and the chain.
  • Existing device without a client, or with the same client → its chain and factory data are updated (a label without a distributor removes the current one). Its status does not change.
  • Existing device bound to another client (or a label without a client for a device that has one) → 409 already_bound: reassign it from the device page instead.
  • With a client, that client is also added to the chain’s portfolio.

The result travels in the response headers, which are readable from the browser (CORS):

HeaderValue
x-device-registrationcreated, updated or unchanged
x-device-idThe device’s _id (as in GET /v1/devices/:id)
Terminal window
curl -s -D - -o label.png "https://api.linter-iot-stg.icent.digital/v1/labels?mac=a4cf12bb73d2&partNumber=EJET&filial=ES&register=1&format=png&width=2000&key=lit_…" \
| grep -i '^x-device'
# x-device-registration: created
# x-device-id: 6718a0c2e4b0a1b2c3d4e5f6

Besides the label errors above, registration can answer 422 mac_invalid (the MAC must be 12 hexadecimal characters), 422 part_number_unknown and 409 already_bound. The label is rendered first, so a failed request registers nothing. Each registration that creates or changes a device is recorded in the audit log (device.registered).

To reprint the label of an existing device, GET /v1/devices/:id returns its subsidiaryCode, distributorCode and clientFiscalId (not to client roles).

Flow

  1. Scan — the end client (or the installer) opens the QR URL:
    • Without a session: the public page offers sign in or create an account (registration creates a client + user in PENDING state).
    • With a session: straight to the wizard with the device preloaded.
  2. Request — POST /v1/bindings/scan { qr } creates an Assignment in PENDING_REVIEW. If notification rules are configured, Linter receives an email with the ICC to activate the SIM.
    • Commercial roles (subsidiary, distributor, installer) also designate the end client in the same call: client: { email, companyName?, fiscalId?, description? }. With an already-registered email the device is bound to that client; with a new one the client is created in PENDING (deduplicated by fiscal ID) with that email as contact. When they send no client and the QR carries the CIF of a client in their portfolio, that client is used.
    • The request takes its commercial chain from the device when it is already registered under a subsidiary (and is not moving to another client); otherwise from the chain printed in the QR (codes that do not resolve are ignored, they never block the scan); otherwise from the client’s commercial links (or, without links, the scanner’s own scope).
    • For every role except client roles, POST /v1/bindings/preview also returns that chain (chain: { subsidiaryName, distributorName? }) and the QR’s client when it is in their portfolio (qrClient: { _id, name }).
  3. Approval — ROOT reviews under Bindings and approves (POST /v1/assignments/:id/approve): the Device is materialized in ACTIVE state with the request’s whole chain and, if the requester came from self-service registration, their account and their client are activated at the same time. If the request was filed by a commercial role, the client is additionally linked to their chain (visible in their portfolio). The requester receives an email.
  4. Reinstallation — binding a MAC to a different client deactivates the previous assignment (history is never deleted) and requires explicit confirmation.

An administrator can also bind directly (POST /v1/bindings, auto-approved). The distributor must belong to the subsidiary: when only the distributor is given its subsidiary is filled in, and a mismatch is 422 chain_incoherent.

EndpointUse
GET /v1/labelsLabel with the QR (SVG/PNG), optionally registering the device (ROOT)
POST /v1/public/qr-previewCatalog identity of the QR (public, no commercial data)
POST /v1/bindings/previewIdentity + binding state (authenticated); commercial chain for every role except client roles
POST /v1/bindings/scanCreate the request (PENDING_REVIEW); commercial roles add client
POST /v1/bindings/client-lookupDoes the email belong to a registered client? (commercial roles)
POST /v1/bindingsDirect registration by an administrator (auto-approved)
GET /v1/bindings/optionsSubsidiaries and distributors with their code, clients with their fiscalId
GET /v1/assignmentsBinding queue for the scope
POST /v1/assignments/:id/approve / …/rejectROOT’s decision