Skip to main content
    Skip to content
    NorruvaDeveloper Docs
    Sandbox · verified 2026-07-29
    This page
    Whole docs
    Machine formats

    PlaygroundGet API keys
    IntroductionQuickstartAuthenticationErrors & conventionsSandbox & environments
    Entity modelProducts & categoriesPassports & versionsDigital Link & resolutionCompliance & regulationsWebhooks & eventsAutoID print loopImport jobsObservability & audit
    OverviewAuth & API keysProductsCompliance & regulationsPassportsResolver & publicWebhooksPrint jobs & devicesImport / bulkObservabilityBeyond happy path
    Integration playbookWebhook receiver guideRun a print deviceDeviations & gotchas
    EN 18222 API methodsAnnex ZA — ESPR correspondence
    Docs/Concepts/AutoID print loop

    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 (the TSC key) 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 $TSC_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 issues a signed job for one allocated serial, an enrolled device claims it under a lease, 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 signed envelope            (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

    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 TSC 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.
    ← PreviousWebhooks & eventsNext →Import jobs
    On this page
    The loopThe endpointsSemantics that matterReport outcomes — exampleThe re-print fileTier 2 — orchestrated print jobs (AIDC)The job lifecycle — states are evidence-gatedThe four client guarantees
    Norruva DPP API · sandbox developer docsGenerated 2026-07-29 · PRD-aligned (TSC roadmap rev 2) · statuses reflect E2E-verified sandbox behaviour — not marketing