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": "Industrial label printer",
"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
| Refusal | Meaning | Do |
|---|---|---|
403 DEVICE_NOT_ENROLLED | The key is not bound to a device. | Enrol the device / bind api_key_id (§1). |
403 DEVICE_DISABLED | Kill switch, an operator disabled or retired this device. | Stop the client; resolve with the operator; re-enable via POST /devices/{id}/status. |
409 STALE_ATTEMPT | The lease expired and another claim superseded yours. | Drop the job locally, the work now belongs to the new attempt. Never re-report. |
410 ENVELOPE_EXPIRED | The envelope's TTL passed before the result arrived. | Re-claim; a fresh envelope carries a fresh expiry. |
409 LEASE_NOT_HELD | Heartbeat for a lease you no longer hold. | Same as stale attempt, re-claim. |
422 MISSING_EVIDENCE | The 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_ALLOWED | PRINTED_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_TRANSITION | The FSM forbids that edge (the message lists what is legal). | Fix the client's state model. |
422 SERIAL_NOT_ALLOCATED / PASSPORT_NOT_READY | Creation 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_idand the attempt budget increments. - Job keeps failing? After
max_attemptsthe reaper parks it inDEAD_LETTERand the serial lands inGET /carriers/export?printState=failed, the re-print file. Query dead-lettered jobs withGET /aidc/jobs?status=DEAD_LETTER;last_errorcarries 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
printedon the spine. Recovery runs through the void/reissue axis on the serialization record, never by mutating the job.
Build against this contract. Norruva tests the claim, lease, heartbeat, evidence chain and idempotent reporting described here with an internal test client. That client is not distributed and is not a supported production deployable, so build your device integration directly against the contract on this page.