Skip to content

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.

Authorization: Bearer <factory-token>

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:

{
  "batchId": "01912345-...",
  "serial": "SN2026001",
  "sku": "INK-800-BW-001",
  "hwRevision": "v1.2"
}
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 OKDeviceView 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 CreatedCredentialView object


POST /internal/factory/devices:nfc-files-batch-generate

Requires Supervisor

Batch-generate credentials for all nfcStatus=pending devices in a batch.

Request Body:

{ "batchId": "01912345-..." }

Response: 200 OK

{ "generated": 48, "skipped": 2 }

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:

{ "reason": "tag damaged — rewrite required" }

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:

{ "batchId": "01912345-..." }

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:

{ "reason": "not authorized for this batch" }

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

{
  "registered": 1000,
  "credentialGenerated": 980,
  "downloaded": 950,
  "bound": 800
}

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:

{ "error": "description of what went wrong" }
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