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/Playbook/Run a print device

    Run a print device

    Everything a device integrator (or the operator supporting one) needs: enrolment, the polling loop, what to send at each state, and how every failure mode resolves without a database console.

    1 · Enrol the device, bind its credential

    bash
    # Mint a key with aidc:write, then enrol the device and bind the key to it
    curl -X POST "$BASE/api/v2/devices" \
      -H "Authorization: Bearer $ADMIN_KEY" -H "Content-Type: application/json" \
      -d '{ "label": "PEX-2000-LINE-3", "kind": "label_printer",
            "connectMode": "raw_9100", "model": "TSC PEX-2000",
            "apiKeyId": "key_01H…" }'

    The bound key IS the device's identity: claims, heartbeats and results made with it are attributed to this device (actor_kind: "device", D20), and revoking or disabling the device cuts that credential out of the loop within one lease TTL. An unbound aidc:write key can still issue jobs and report as a human operator — it just cannot claim.

    2 · The loop a client runs

    flow
    loop:
      POST /aidc/jobs/claim { "limit": 1, "lease_seconds": 120 }
      ── no jobs? sleep, poll again (an empty claim is a 200, not an error)
      ── got a job:
         1. VERIFY the envelope signature (eddsa-jcs-2022) BEFORE printing
         2. render          → POST …/result { status: RENDERED,        evidence: render_manifest,  attempt_id }
         3. spool           → POST …/result { status: SPOOL_SUBMITTED, evidence: spool_receipt,    attempt_id }
         4. paper came out  → POST …/result { status: PRINTED_ATTESTED,                            attempt_id }
         5. scan the label  → POST …/result { status: SCAN_VERIFIED,   evidence: scan_result,      attempt_id }
         6. seal            → POST …/result { status: SEALED,          evidence: evidence_bundle,  attempt_id }
      long run? POST …/heartbeat { attempt_id, lease_seconds } before the lease expires
      anything broke? POST …/result { status: FAILED, error_message, attempt_id }

    Always send Idempotency-Key on results. Retrying after a timeout is then always safe — the server replays the completed transition instead of double-applying it ("replayed": true in the response).

    3 · The error catalogue, and what each one means you should do

    RefusalMeaningDo
    403 DEVICE_NOT_ENROLLEDThe key is not bound to a device.Enrol the device / bind api_key_id (§1).
    403 DEVICE_DISABLEDKill switch — an operator disabled or retired this device.Stop the client; resolve with the operator; re-enable via POST /devices/{id}/status.
    409 STALE_ATTEMPTThe lease expired and another claim superseded yours.Drop the job locally — the work now belongs to the new attempt. Never re-report.
    410 ENVELOPE_EXPIREDThe envelope's TTL passed before the result arrived.Re-claim; a fresh envelope carries a fresh expiry.
    409 LEASE_NOT_HELDHeartbeat for a lease you no longer hold.Same as stale attempt — re-claim.
    422 MISSING_EVIDENCEThe target state requires its artifact (render_manifest, spool_receipt, scan_result, evidence_bundle).Send the evidence — the state is unreachable without it, by design.
    403 ACTOR_NOT_ALLOWEDPRINTED_ATTESTED asserted by something that is not a device or a human.Only an authenticated device (ODV) or operator may claim paper came out.
    409 ILLEGAL_TRANSITIONThe FSM forbids that edge (the message lists what is legal).Fix the client's state model.
    422 SERIAL_NOT_ALLOCATED / PASSPORT_NOT_READYCreation gates (issuer side).Allocate via carriers/batch; anchor a validation + publish (F6).

    4 · Resolving a stuck job

    • Device crashed mid-job? Nothing to do. When the lease expires the job re-enters the claim pool; the next poll picks it up with a fresh attempt_id and the attempt budget increments.
    • Job keeps failing? After max_attempts the reaper parks it in DEAD_LETTER and the serial lands in GET /carriers/export?printState=failed — the re-print file. Query dead-lettered jobs with GET /aidc/jobs?status=DEAD_LETTER; last_error carries the final failure.
    • Need to stop a device NOW? POST /devices/{id}/status { "status": "disabled" } — it cannot claim again, and in-flight work is reclaimable after its lease.
    • Wrong label in the world? A SEALED job is terminal and its serial is printed on the spine. Recovery runs through the void/reissue axis on the serialization record, never by mutating the job.
    ◆
    Reference client. scripts/agent/print-agent.mjs in the platform repo is the executable definition of this contract — claim, lease, heartbeat, evidence chain, idempotent reporting. It is reference and test software, not a supported production deployable.
    Was this page helpful?
    Thanks — noted.Feedback goes to the docs team by email.
    ← PreviousWebhook receiver guideNext →Deviations & gotchas
    On this page
    1 · Enrol the device, bind its credential2 · The loop a client runs3 · The error catalogue, and what each one means you should do4 · Resolving a stuck job
    Norruva DPP API · sandbox developer docsGenerated 2026-07-29 · PRD-aligned (TSC roadmap rev 2) · statuses reflect E2E-verified sandbox behaviour — not marketing