Developer Docs
Sandbox · API v2PlaygroundGet API keys

AutoID print loop

Norruva creates the identity; the print layer (e.g. TSC) prints and encodes it; the outcome comes back per serial. Four legs, webhooks both ways, print execution stays the printer partner's job, Norruva stays the system of record for what was (and wasn't) printed.

The loop

flow
POST /api/v2/carriers/batch          { "productId": "…", "count": 1000 }
     → 201 { allocated, serials[] }  → webhook carrier.generated
                                       ("codes ready", eventData carries exportPath; no polling)
GET  /api/v2/carriers/export?productId=…
     → the print CSV: gtin, serial, digital_link, sgtin96_epc
       (digital_link IS the QR payload; sgtin96_epc feeds the RFID encoder)

…the print layer prints…

POST /api/v2/carriers/print-status
{ "results": [ { "serial": "…", "status": "printed", "deviceId": "PEX-2000-01" },
               { "serial": "…", "status": "failed",  "reason": "ribbon jam" } ] }
     → 200 { updated[], unknown[], printed[], failed[], reportedAt, events[] }
     → webhooks print.confirmed / print.failed

The endpoints

CallPurposeAuth
POST /api/v2/carriers/batchAllocate up to 10k numeric, SGTIN-96-safe serials for a product. Idempotent via Idempotency-Key; fires carrier.generated so subscribers pull instead of poll.KEY production:create
GET /api/v2/carriers/exportThe print contract (CSV or format=json). Optional printState=printed|failed|pending narrows to the reported outcome, printState=failed is exactly the re-print file; an unknown value refuses with 422.KEY production:view (identifiers:read satisfies it)
POST /api/v2/carriers/print-statusPer-serial outcomes from the print layer, ≤1000 per call: printed or failed + optional reason / deviceId.KEY epcis:capture or production:create

Semantics that matter

  • Idempotent by construction. Outcomes land on the serialization record (metadata.print: status, reason, deviceId, reportedAt) and are replaced wholesale, a later re-print legitimately overwrites an earlier failure (last write wins, also within one request).
  • A retry replays; it does not re-report. Because print controllers rarely send an Idempotency-Key, the endpoint derives one from the batch itself. Re-posting an identical batch returns the first response and fires no further webhooks, so a timed-out report is safe to retry without duplicating print.confirmed / print.failed for subscribers. The events[] counts in a replayed response describe the original call. Change any outcome in the batch and it hashes differently, so genuinely new results always execute.
  • Unknown serials are reported, never dropped. Anything not found for your tenant comes back in unknown[] with a 200, the known outcomes still commit.
  • Aggregate events, not per-serial spam. One print.confirmed and/or one print.failed delivery per request, each carrying the serial list (+ per-serial failures[] reasons on the failed event), a 10k-label run is 2 deliveries, not 10k.
  • Webhook dispatch failures never fail the call. The outcome is already durably recorded; events[] in the response tells you what was actually notified.

Report outcomes, example

bash
curl -X POST "$BASE/api/v2/carriers/print-status" \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{ "results": [
        { "serial": "100001", "status": "printed", "deviceId": "PEX-2000-01" },
        { "serial": "100002", "status": "failed",  "reason": "ribbon jam" } ] }'
JSON
{ "updated": ["100001", "100002"], "unknown": [],
  "printed": ["100001"], "failed": ["100002"],
  "reportedAt": "2026-07-22T17:20:00.000Z",
  "events": [ { "type": "print.confirmed", "webhooksNotified": 1 },
              { "type": "print.failed",    "webhooksNotified": 1 } ] }

The re-print file

After outcomes are in, GET /carriers/export?productId=…&printState=failed returns exactly the serials that need another pass, same CSV columns, so the same file feeds the same print pipeline. printState=pending answers "what has the print layer not reported yet?" for reconciliation.

Tier 2, orchestrated print jobs (AIDC)

The CSV loop above trusts the print layer to manage its own work. The print-job tier (/api/v2/aidc/*, F15) makes each label an orchestrated, evidence-carrying unit instead: Norruva creates a job for one allocated serial, an enrolled device claims it under a lease and receives it inside a signed envelope, reports each state transition with its evidence artifact, and the terminal outcome lands on the same metadata.print spine the CSV loop uses, so the export, the re-print file and the webhooks stay one truth whichever tier printed the label.

flow
POST /api/v2/aidc/jobs                { "gtin": "…", "serial": "…" }   ← serial must be allocated
     → 201 job resource               (fails 422 unless the passport is readyToPrint)
POST /api/v2/aidc/jobs/claim          ← THE device pulls (no inbound port into the plant)
     → signed envelope + attempt_id + lease
POST /api/v2/aidc/jobs/{ref}/heartbeat   ← long run? extend the lease
POST /api/v2/aidc/jobs/{ref}/result   { "status": "RENDERED", "evidence": {…}, "attempt_id": "…" }
     …repeat per state…               SPOOL_SUBMITTED → PRINTED_ATTESTED → SCAN_VERIFIED → SEALED
     → SEALED: metadata.print=printed + EPCIS commissioning + print.confirmed
     → DEAD_LETTER (retries exhausted): metadata.print=failed → the re-print file

Vocabulary note for printer-vendor integrations: on this platform a print job (print_job, identified by job_ref) is the shared lifecycle object that survives retries, and a print attempt (print_attempt) is one physical try, reissued on each retry. A printer vendor's own "print job" (one command stream sent to one printer) maps to our print_attempt; a vendor's "DPP job" maps to our print_job. There is deliberately no dpp_job object or alias: one object, one name, and the two-noun mapping keeps the one-to-many relation between a lifecycle and its attempts visible.

The job lifecycle, states are evidence-gated

StateMeaningRequired evidenceWho may assert it
QUEUEDCreated from an allocated serial; waiting for a claim.-issuer
CLAIMEDA device holds it under a lease (fresh attempt_id per claim).-device (via claim)
RENDEREDLabel content produced.render_manifestdevice / operator
SPOOL_SUBMITTEDA spooler accepted the job, machine-observable, nothing more.spool_receiptdevice / operator
PRINTED_ATTESTEDPaper came out. Actor-attested only, a spool submission can never imply it (the honesty constraint).-device or human only, never system
SCAN_VERIFIEDThe printed symbol decoded back to the expected payload.scan_resultdevice / operator
SEALEDTerminal success, evidence bundle sealed. Propagates printed to the spine + EPCIS commissioning.evidence_bundledevice / operator
FAILEDRetryable up to max_attempts, re-enters the claim pool.-any
DEAD_LETTERTerminal failure, retry budget exhausted (reaper). Propagates failed → the re-print file.-system (reaper)
CANCELLEDTerminal, before physical work, no print outcome to report.-issuer

The four client guarantees

  • Lease. A crashed device's job is reclaimable the moment its lease expires, no human, no reaper on the happy path. Heartbeat to keep a long run.
  • Attempt id. Every claim mints and PERSISTS a fresh attempt_id; a result from a superseded attempt is a 409 STALE_ATTEMPT, an expired envelope a 410.
  • Idempotency. Send Idempotency-Key on results, the key is claimed inside the transition transaction, so a retry after a timeout replays instead of double-applying.
  • Atomic outcomes. A terminal transition and the spine write (metadata.print) commit in ONE transaction, there is no window where the job says printed and the export says pending.

Two creation gates, both typed 422s: SERIAL_NOT_ALLOCATED (allocate via carriers/batch first, a job can never mint a serial) and PASSPORT_NOT_READY (the F6 readyToPrint gate: ≥ 1 anchored validation ∧ published; anchor via POST /passports/{id}/validations). Full endpoint reference: Print jobs & devices; operator walkthrough: Run a print device.

What this loop does NOT cover. Label verification (scanner/RFID read-back matching expected data) is the EPCIS capture surface (POST /epcis/capture with the same key), and a dedicated verify-label endpoint is not shipped, see Deviations. Subscribe to the trio on one endpoint via Webhooks & events.
Was this page helpful?
Thanks, noted.Feedback goes to the docs team by email.