Factory Console API¶
The factory console API handles device registration, NFC credential issuance, file downloads with an approval workflow, and audit logging. It is mounted at /internal/factory/* and exposed through a dedicated factory-api ingress, fully isolated from the user-facing API: a user JWT cannot reach it, and a factory token cannot open anything else.
Authentication¶
The factory API uses static Bearer Tokens configured via the FACTORY_PRINCIPALS environment variable — not user JWTs. Each principal carries a unique ID, role, and batch/SKU access scope.
Roles¶
| Role | Capabilities |
|---|---|
operator |
Register devices, generate credentials, download .nfc.json files (first time per credential) |
supervisor |
All operator capabilities, plus: approve repeat-download requests, revoke and reissue credentials, create batches, batch-import devices, batch-generate files |
auditor |
Read-only: view all devices, batches, stats, and audit logs |
Scope Restrictions¶
Each principal's batches and skus fields restrict which production orders and products it can touch. ["*"] means unrestricted; [] means no access at all (not a wildcard).
Batches¶
A batch corresponds to one purchase order (PO). Devices must be associated with a batch before registration.
GET /internal/factory/batches¶
List all batches accessible to the current principal.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status (open, closed) |
Response: 200 OK
{
"items": [
{
"id": "01912345-...",
"orderNo": "PO-2026-001",
"sku": "INK-800-BW-001",
"hwRevision": "v1.2",
"plannedQuantity": 500,
"factoryId": "factory-shenzhen",
"status": "open",
"createdAt": "2026-07-01T00:00:00Z",
"createdBy": "admin"
}
],
"total": 1,
"limit": 1,
"offset": 0
}
POST /internal/factory/batches¶
Requires Supervisor
Create a new batch.
Request Body:
{
"orderNo": "PO-2026-001",
"sku": "INK-800-BW-001",
"hwRevision": "v1.2",
"plannedQuantity": 500,
"factoryId": "factory-shenzhen"
}
| Field | Type | Required | Description |
|---|---|---|---|
orderNo |
string | Yes | Purchase order number, globally unique |
sku |
string | Yes | Product SKU |
hwRevision |
string | No | Hardware revision |
plannedQuantity |
integer | No | Planned unit count |
factoryId |
string | No | Factory identifier |
Response: 201 Created — returns the created batch object
GET /internal/factory/batches/{batchId}¶
Get a single batch by ID.
Response: 200 OK — batch object
GET /internal/factory/batches/{batchId}/stats¶
Get registration and download statistics for a batch.
Response: 200 OK
{
"batch": { ... },
"counts": {
"registered": 50,
"credentialGenerated": 45,
"downloaded": 40,
"bound": 30,
"plannedQuantity": 500
}
}
Devices¶
GET /internal/factory/devices¶
List devices with pagination and filtering.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
batchId |
UUID | Filter by batch |
lifecycleStatus |
string | registered, provisioned |
nfcStatus |
string | pending, generated, downloaded, revoked |
bound |
boolean | Filter by bind status |
q |
string | Keyword search on serial or hwId |
limit |
integer | Items per page (default 50, max 200) |
offset |
integer | Pagination offset |
Response: 200 OK — list of DeviceView objects (see model below)
POST /internal/factory/devices¶
Requires write access (operator or supervisor)
Register a single device. hwId and thingName are allocated by the backend — callers must not supply them.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
batchId |
UUID | Yes | Batch ID |
serial |
string | No | Factory serial number; batchId + serial is unique |
sku |
string | No | Overrides the batch SKU |
hwRevision |
string | No | Overrides the batch hardware revision |
Request Headers (optional):
| Header | Description |
|---|---|
Idempotency-Key |
Safe retry key for batchId + serial duplicates |
Response: 201 Created (new device) or 200 OK (idempotent hit)
{
"id": "01912345-...",
"hwId": "a1b2c3d4e5f60708090a0b0c0d0e0f10",
"thingName": "inklet-a1b2c3d4e5f60708",
"serial": "SN2026001",
"batchId": "01912345-...",
"lifecycleStatus": "registered",
"nfcStatus": "pending",
"credentialVersion": 0,
"bound": false,
"createdAt": "2026-07-01T10:00:00Z"
}
POST /internal/factory/devices:batch-import¶
Requires Supervisor
Bulk-register devices from a CSV file. The CSV must include a serial column (required) and may include sku and hwRevision. The batchId is passed as a query parameter.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
batchId |
UUID | Target batch (required) |
Request Body: multipart/form-data with field file — CSV file (max 4 MiB)
Response: 200 OK
{
"ok": 48,
"idempotent": 2,
"errors": [
{ "row": 3, "serial": "SN2026003", "error": "duplicate serial" }
]
}
Error rows do not block valid rows; partial success is a normal outcome.
GET /internal/factory/devices/{deviceId}¶
Get details for a single device, including its current credential view (proof is masked).
Response: 200 OK — DeviceView object
POST /internal/factory/devices/{deviceId}/nfc-files¶
Requires write access
Generate an NFC credential for a device. A device can have at most one active credential per credentialVersion. Calling again returns 409 — generation is not repeatable. To re-download an existing file, use the download endpoint (which may require an approval request).
Response: 201 Created — CredentialView object
POST /internal/factory/devices:nfc-files-batch-generate¶
Requires Supervisor
Batch-generate credentials for all nfcStatus=pending devices in a batch.
Request Body:
Response: 200 OK
POST /internal/factory/devices/{deviceId}/nfc-credentials:revoke-and-reissue¶
Requires Supervisor
Revoke the current credential and issue a new one (credentialVersion + 1). The old credential is invalidated immediately; any App that previously bound using the old credential will be rejected on a re-bind attempt.
Request Body:
Response: 200 OK — new CredentialView object
NFC Files¶
The .nfc.json file is the programming file handed to the production line. It contains device identity and the NFC URI payload. Downloading is a permissioned and audited action separate from generation.
.nfc.json File Schema¶
{
"schemaVersion": 1,
"type": "inklet-nfc-bind",
"serial": "SN2026001",
"hwId": "a1b2c3d4e5f60708090a0b0c0d0e0f10",
"thingName": "inklet-a1b2c3d4e5f60708",
"batchId": "01912345-...",
"sku": "INK-800-BW-001",
"hwRevision": "v1.2",
"keyId": "k1",
"credentialVersion": 1,
"nfcPayload": "inklet://bind?v=2&kid=k1&hw=a1b2c3d4...&cv=1&p=AAAA...",
"payloadSha256": "abcdef0123456789...",
"issuedAt": "2026-07-01T10:00:00Z",
"manifestKeyId": "manifest-key-1",
"manifestSignature": "base64-ed25519-sig"
}
| Field | Description |
|---|---|
nfcPayload |
Full URI to write to the NFC tag (production line uses this field directly) |
payloadSha256 |
SHA-256 of the payload, for read-back verification |
manifestSignature |
Ed25519 signature over the file, for production line integrity check |
Credential sensitivity
The .nfc.json file contains the full NFC URI including the proof — it is a live binding credential. Distribute only to authorized stations. A leaked file compromises exactly the one device it names; revoke-and-reissue invalidates it immediately.
GET /internal/factory/nfc-files/{credentialId}/download¶
Requires write access · Rate-limited
Download the .nfc.json file for a device.
- First download: returned immediately.
- Repeat download: an operator must first submit a download request (
POST .../download-requests) and wait for supervisor approval. Supervisors can repeat-download directly.
Response: 200 OK, Content-Type: application/json, filename {serial}.nfc.json
Errors:
| Code | Reason |
|---|---|
403 |
Repeat download not approved, or insufficient role |
404 |
Credential does not exist or has been revoked |
POST /internal/factory/nfc-files/{credentialId}/download-requests¶
Requires write access
Submit a repeat-download request awaiting supervisor approval. Only one pending request per credential is allowed at a time.
Response: 201 Created
{
"id": "request-uuid",
"credentialId": "cred-uuid",
"requestedBy": "operator-1",
"status": "pending",
"createdAt": "2026-07-01T11:00:00Z"
}
POST /internal/factory/nfc-files:download-zip¶
Requires Supervisor · Rate-limited
Bulk-download all .nfc.json files for a batch as a ZIP archive.
Request Body:
Response: 200 OK, Content-Type: application/zip
Download Requests¶
GET /internal/factory/download-requests¶
List download approval requests visible to the current principal.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | pending, approved, rejected |
batchId |
UUID | Filter by batch |
Response: 200 OK — list of request objects
POST /internal/factory/download-requests/{requestId}:approve¶
Requires Supervisor
Approve a download request, allowing the operator to download once.
Response: 200 OK
POST /internal/factory/download-requests/{requestId}:reject¶
Requires Supervisor
Reject a download request with a reason.
Request Body:
Response: 200 OK
Stats and Audit¶
GET /internal/factory/stats¶
Get global or batch-level statistics.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
batchId |
UUID | Scope to a specific batch (omit for global) |
Response: 200 OK
GET /internal/factory/audit-logs¶
Query the audit log. All write operations (register, generate, download, revoke, approve) are automatically recorded.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
batchId |
UUID | Filter by batch |
deviceId |
UUID | Filter by device |
action |
string | Filter by action type |
from |
RFC3339 | Start time |
to |
RFC3339 | End time |
limit |
integer | Items per page (default 50) |
offset |
integer | Pagination offset |
Response: 200 OK
{
"items": [
{
"id": "log-uuid",
"action": "credential_download",
"result": "ok",
"principalId": "operator-1",
"principalName": "Line A Station 3",
"batchId": "01912345-...",
"deviceId": "01912345-...",
"detail": "first download",
"createdAt": "2026-07-01T10:05:00Z"
}
],
"total": 150,
"limit": 50,
"offset": 0
}
GET /internal/factory/manifest-keys¶
List the active manifest signing public keys, for production line verification of .nfc.json file integrity.
Response: 200 OK
[
{
"id": "manifest-key-1",
"publicKey": "base64-ed25519-public-key",
"algorithm": "ed25519",
"createdAt": "2026-07-01T00:00:00Z"
}
]
GET /internal/factory/whoami¶
Return the current principal's identity.
Response: 200 OK
{
"id": "operator-1",
"name": "Line A Station 3",
"role": "operator",
"stationId": "station-a3",
"factoryId": "factory-shenzhen",
"batches": ["*"],
"skus": ["INK-800-BW-001"]
}
DeviceView Model¶
{
"id": "01912345-...",
"serial": "SN2026001",
"hwId": "a1b2c3d4e5f60708090a0b0c0d0e0f10",
"thingName": "inklet-a1b2c3d4e5f60708",
"batchId": "01912345-...",
"orderNo": "PO-2026-001",
"sku": "INK-800-BW-001",
"hwRevision": "v1.2",
"lifecycleStatus": "registered",
"nfcStatus": "downloaded",
"credentialVersion": 1,
"nfcKeyId": "k1",
"bound": false,
"online": false,
"createdAt": "2026-07-01T10:00:00Z",
"credential": {
"id": "cred-uuid",
"keyId": "k1",
"credentialVersion": 1,
"status": "downloaded",
"payloadSha256": "abcdef...",
"payloadPreview": "inklet://bind?v=2&kid=k1&hw=a1b2...&cv=1&p=****",
"issuedBy": "operator-1",
"issuedAt": "2026-07-01T10:00:00Z",
"downloadCount": 1,
"requiresApproval": true
}
}
NFC status values:
nfcStatus |
Description |
|---|---|
pending |
No credential generated yet |
generated |
Credential generated, not yet downloaded |
downloaded |
Downloaded at least once |
revoked |
Revoked, awaiting reissue |
Note
payloadPreview always masks the proof as ****. The full proof is only accessible through the download endpoint.
Error Responses¶
All errors use a consistent format:
| Code | Description |
|---|---|
400 |
Invalid request body or parameters |
401 |
Missing or invalid Bearer Token |
403 |
Insufficient role or scope |
404 |
Resource not found |
409 |
Conflict (e.g., duplicate serial, or credential already generated) |
429 |
Rate limit exceeded |