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
| 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.
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.