microfactory
Docs · Device API

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.

Endpoints

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.

EndpointAuthWhat it does
POST /api/devices/claimNone — the claim code is the credentialRedeem a single-use claim code for a device token. The token is returned exactly once; HQ stores only its hash.
POST /api/devices/heartbeatBearerLiveness + peripheral roster. Drives adopting → online; the ack carries the current cadence (30s) and an agent-version compatibility nudge.
POST /api/devices/scansBearerBatch scan upload — the gateway's store-and-forward drain, with a per-item result for every scan.
GET /api/devices/commandsBearerPull the queued, not-yet-expired commands for this device (remote print / rescan). Immediate return — poll it each heartbeat cycle.
POST /api/devices/commands/ackBearerAcknowledge a pulled command with its outcome (applied, failed, or dropped).
Auth

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.

Rate limits

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.

ScopeBudgetApplies to
Per client IP10 / 60sPOST /api/devices/claim — the one unauthenticated endpoint.
Per device120 / 60sAll four bearer endpoints, sharing one budget keyed on the token's device — a healthy 30s heartbeat + poll + ack cycle fits with ample headroom.
Errors

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.

Example 400
{
  "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.
CodeStatusMeaning
invalid_json400The request body is not valid JSON.
invalid_input400 / 413The body failed schema validation — `param` names the offending field.
invalid_claim404Unknown or already-used claim code.
unauthorized401Missing or invalid bearer token. The response advertises `www-authenticate: Bearer`.
gateway_mismatch403The body's gatewayId does not match the device the token authenticates.
not_found404The target does not exist — a deleted/revoked device, an unknown command id, or an unknown station on a scan item.
command_already_terminal404Duplicate ack — the command already reached a terminal outcome. Benign under at-least-once delivery.
rate_limited429Over budget — retry after `retryAfterSecs` (also in the `retry-after` header).
server_error500Unexpected failure. Quote the `requestId` when reporting it.
demo_readonly403Writes are rejected on the read-only demo host — its data resets anyway.
Idempotency

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.

Go deeper

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:

GET https://microfactoryhq.com/api/devices/contract

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>