The machine interface to microfactory HQ.
Every gateway on a microfactory floor speaks this HTTP surface — five endpoints on https://microfactoryhq.com. The mfagent implements it for you; this page is for anyone building a bridge by hand, or pointing an AI agent at the floor's wire protocol.
Payload shapes are not documented here — they are served live, as JSON Schema, at https://microfactoryhq.com/api/devices/contract.
Five endpoints, one loop
Claim once, then heartbeat, drain scans, pull commands, and ack — the whole device loop. All five live on the public origin.
| Endpoint | Auth | What it does |
|---|---|---|
| POST /api/devices/claim | None — the claim code is the credential | Redeem a single-use claim code for a device token. The token is returned exactly once; HQ stores only its hash. |
| POST /api/devices/heartbeat | Bearer | Liveness + peripheral roster. Drives adopting → online; the ack carries the current cadence (30s) and an agent-version compatibility nudge. |
| POST /api/devices/scans | Bearer | Batch scan upload — the gateway's store-and-forward drain, with a per-item result for every scan. |
| GET /api/devices/commands | Bearer | Pull the queued, not-yet-expired commands for this device (remote print / rescan). Immediate return — poll it each heartbeat cycle. |
| POST /api/devices/commands/ack | Bearer | Acknowledge a pulled command with its outcome (applied, failed, or dropped). |
One token per device, minted at claim
No accounts, no API keys to manage — a device's credential is born when its claim code is redeemed.
Bearer token
The claim response carries the device token exactly once — HQ stores only its SHA-256 hash. Send it on every other call as authorization: Bearer <token>. A missing or revoked token answers 401 with www-authenticate: Bearer.
Claim is unauthenticated
The single-use claim code (minted in HQ under Devices → Adopt) is itself the credential, so the endpoint carries no auth header — and is throttled per client IP (below) to keep codes brute-force-proof. Unknown and already-used codes fail with one opaque answer.
Budgets that tell you when to come back
Every 429 self-describes its retry point in both channels — the retry-after header and retryAfterSecs in the body. Waiting one full window always yields a fresh budget.
| Scope | Budget | Applies to |
|---|---|---|
| Per client IP | 10 / 60s | POST /api/devices/claim — the one unauthenticated endpoint. |
| Per device | 120 / 60s | All four bearer endpoints, sharing one budget keyed on the token's device — a healthy 30s heartbeat + poll + ack cycle fits with ample headroom. |
One envelope, an append-only code registry
Every error response carries the same additive envelope, so a client branches on stable slugs — never on prose or overloaded HTTP statuses. Published codes never change meaning.
{
"error": "invalid input",
"code": "invalid_input",
"requestId": "1c9e37a2-…",
"param": "scans.0.clientId"
}- error
- Human-readable prose — stable for clients that just print it.
- code
- A stable machine slug from the append-only registry below.
- requestId
- Minted per response and logged server-side, so a report and a log line correlate.
- param?
- The offending input field (dotted path) on validation failures.
- retryAfterSecs?
- On 429s — mirrors the `retry-after` header in the body.
| Code | Status | Meaning |
|---|---|---|
| invalid_json | 400 | The request body is not valid JSON. |
| invalid_input | 400 / 413 | The body failed schema validation — `param` names the offending field. |
| invalid_claim | 404 | Unknown or already-used claim code. |
| unauthorized | 401 | Missing or invalid bearer token. The response advertises `www-authenticate: Bearer`. |
| gateway_mismatch | 403 | The body's gatewayId does not match the device the token authenticates. |
| not_found | 404 | The target does not exist — a deleted/revoked device, an unknown command id, or an unknown station on a scan item. |
| command_already_terminal | 404 | Duplicate ack — the command already reached a terminal outcome. Benign under at-least-once delivery. |
| rate_limited | 429 | Over budget — retry after `retryAfterSecs` (also in the `retry-after` header). |
| server_error | 500 | Unexpected failure. Quote the `requestId` when reporting it. |
| demo_readonly | 403 | Writes are rejected on the read-only demo host — its data resets anyway. |
Built for lossy floors
Factory networks drop responses. Every write path is safe to retry — dropped genealogy is unrecoverable, so the API is designed to never force a client to guess.
Claim retry
A gateway that redeemed a claim code but lost the response may re-present the burned code from the same gatewayHardwareId — while the device is still adopting (no heartbeat yet) and within 15 minutes of redemption — and gets a fresh token (the orphaned one is invalidated). A NAT timeout on first power-on never bricks the box.
Scan dedupe
Recording is idempotent per (gatewayId, clientId): a re-upload comes back `duplicate` and stays durably stored. Results are per item — recorded, duplicate, or rejected with a code — so one poisoned scan never rejects its batch.
Ack replay
A duplicate ack answers 404 with code `command_already_terminal` — normal under at-least-once delivery, safe to ignore. An unknown command id answers 404 with `not_found` instead, which is worth escalating.
The contract, and the agent that speaks it
The payload shapes are one GET away, and the reference client is one command.
Live wire contract
The full JSON Schema for every payload on this page — Rust-owned, drift-gated in CI, unauthenticated, served as application/schema+json:
The mf gateway agent
The Rust CLI that ships with every microfactory gateway drives this whole loop — claim, heartbeat, spool, drain, ack — from one command:
mf gateway --hq-url https://microfactoryhq.com--claim-code <code>