Spoolr Connector API Reference
The Spoolr Connector API allows printer connectors (and integrators or AI agents) to register with a Spoolr account, push telemetry, respond to remote commands, serve webcam images on demand, and upload backups.
Base URL: /api/v1
All request and response bodies are JSON unless noted (binary uploads are the exception). Set Content-Type: application/json on all JSON requests.
Authentication
Registration (unauthenticated)
The POST /api/v1/connectors/register endpoint is public. It exchanges a one-time pairing token — generated in the Spoolr web UI with a 10-minute expiry and single-use policy — for a long-lived connector id and secret.
The plaintext secret is returned exactly once at registration. Store it immediately; it cannot be retrieved again. The server stores only a SHA-256 digest.
All other endpoints: two required headers
| Header | Value |
|---|---|
Authorization |
Bearer <secret> |
X-Connector-Id |
<connector_id> |
The server locates the connector by X-Connector-Id, then verifies the bearer token against the stored digest using a constant-time comparison.
Backup upload: signed token (exception)
POST|PUT /api/v1/backups/:id/upload skips the bearer scheme entirely. It uses a Rails MessageVerifier-signed token passed either as:
- Query parameter:
?token=<signed_token> - Header:
X-Upload-Token: <signed_token>
The token encodes the backup ID and an expiry timestamp. Generating this token is handled server-side when a backup job is dispatched.
Connector Lifecycle
A typical connector session follows this sequence:
- Register — exchange a pairing token for credentials and printer IDs.
- Heartbeat loop —
POST /api/v1/connectors/:id/heartbeatevery 30–60 seconds to mark the connector online. - Poll commands —
GET /api/v1/connectors/:connector_id/commandseverypolling.commands_seconds(default: 3 s). Returned commands are automatically markedrunning. - Complete commands —
POST /api/v1/commands/:id/completeafter each command finishes, reportingsucceededorfailed. - Push snapshot batches —
POST /api/v1/snapshots/batcheverypolling.snapshots_seconds(default: 30 s) with telemetry for one or more printers. - Serve webcam requests on demand — poll
GET /api/v1/connectors/:id/webcam_requests, capture JPEG frames, and upload each viaPUT /api/v1/webcam_requests/:id/upload. - Upload backups on demand — when instructed (via a
create_backupcommand), stream the archive toPOST|PUT /api/v1/backups/:id/uploadusing the signed token.
Endpoints
Endpoint Summary
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/v1/connectors/register |
None | Register a connector |
POST |
/api/v1/connectors/:id/heartbeat |
Bearer | Heartbeat / keep-alive |
GET |
/api/v1/connectors/:connector_id/commands |
Bearer | Poll pending commands |
POST |
/api/v1/commands/:id/complete |
Bearer | Mark a command complete |
GET |
/api/v1/connectors/:id/webcam_requests |
Bearer | Poll pending webcam requests |
PUT |
/api/v1/webcam_requests/:id/upload |
Bearer | Upload a webcam frame |
GET |
/api/v1/connectors/:id/webcam_stream |
Bearer | Poll printers a viewer is live-watching |
PUT |
/api/v1/printers/:id/webcam_stream_frame |
Bearer | Push a live MJPEG stream frame |
POST |
/api/v1/snapshots/batch |
Bearer | Push telemetry snapshots |
POST / PUT |
/api/v1/backups/:id/upload |
Signed token | Upload a backup archive |
1. Register Connector
POST /api/v1/connectors/register
Authentication: None (public endpoint).
Exchanges a pairing token for connector credentials and printer IDs. The pairing token must not be expired or previously used.
Request body
{
"pairing_token": "abc123...",
"site_name": "My Workshop",
"device": {
"hostname": "K1Max-1814",
"arch": "mipsle",
"os": "linux",
"version": "0.1.0",
"ip": "192.0.2.50",
"ui_port": 4409
},
"printers": [
{
"name": "K1 Max",
"host": "192.0.2.60",
"moonraker_port": 7125,
"ui_port": 4409
}
]
}
| Field | Type | Required | Notes |
|---|---|---|---|
pairing_token |
string | Yes | One-time token from the web UI |
site_name |
string | No | Falls back to the token's site name |
device |
object | No | Connector host information |
device.hostname |
string | No | |
device.arch |
string | No | e.g. mipsle, arm64, amd64 |
device.os |
string | No | e.g. linux |
device.version |
string | No | Connector firmware version |
device.ip |
string | No | Used as fallback printer host |
device.ui_port |
integer | No | Connector UI port |
printers |
array | No | Omit to auto-create one placeholder printer |
printers[].name |
string | No | Defaults to Printer #N |
printers[].host |
string | No | Printer LAN host; falls back to device.ip |
printers[].moonraker_port |
integer | No | Defaults to 7125 |
printers[].ui_port |
integer | No | Defaults to device.ui_port or 80 |
Success response — 201 Created
{
"connector": {
"id": 42,
"site_name": "My Workshop"
},
"credentials": {
"secret": "spoolr_live_xxxxxxxxxxxxxxxxxxxxxxxx"
},
"printers": [
{ "id": 17, "name": "K1 Max" }
],
"polling": {
"commands_seconds": 3,
"snapshots_seconds": 30
}
}
Store
credentials.secretimmediately — it is shown only once.
Error responses
| Status | Condition |
|---|---|
400 Bad Request |
pairing_token is missing or blank |
401 Unauthorized |
Token not found, already used, or expired |
422 Unprocessable Entity |
Printer record creation failed (validation error) |
curl example
curl -s -X POST https://your-spoolr-host/api/v1/connectors/register \
-H "Content-Type: application/json" \
-d '{
"pairing_token": "your-pairing-token",
"site_name": "My Workshop",
"device": {
"hostname": "my-connector",
"arch": "amd64",
"os": "linux",
"version": "0.1.0",
"ip": "192.0.2.50",
"ui_port": 4409
},
"printers": [
{ "name": "Ender 3", "host": "192.0.2.61", "moonraker_port": 7125 }
]
}'
2. Heartbeat
POST /api/v1/connectors/:id/heartbeat
Authentication: Bearer + X-Connector-Id.
Marks the connector as active and updates its device metadata. Call every 30–60 seconds.
The :id in the path must match the authenticated connector's own ID; mismatches return 401.
Request body
{
"hostname": "K1Max-1814",
"arch": "mipsle",
"os": "linux",
"version": "0.1.1"
}
All fields are optional.
Success response — 200 OK
{ "ok": true }
Error responses
| Status | Condition |
|---|---|
401 Unauthorized |
Missing/invalid credentials, or :id does not match the authenticated connector |
422 Unprocessable Entity |
Validation failure on the connector record |
curl example
curl -s -X POST https://your-spoolr-host/api/v1/connectors/$CONNECTOR_ID/heartbeat \
-H "Authorization: Bearer $SPOOLR_SECRET" \
-H "X-Connector-Id: $CONNECTOR_ID" \
-H "Content-Type: application/json" \
-d '{ "hostname": "my-connector", "version": "0.1.1" }'
3. Poll Commands
GET /api/v1/connectors/:connector_id/commands
Authentication: Bearer + X-Connector-Id.
Returns up to 20 queued commands for this connector. Side effect: all returned commands are immediately transitioned to running status.
Query parameters
| Parameter | Default | Notes |
|---|---|---|
limit |
20 |
Max 20; values ≤ 0 reset to 20 |
Success response — 200 OK
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"printer_id": 17,
"action": "start_print",
"params": { "filename": "benchy.gcode" }
}
]
An empty array [] means no commands are pending.
Action enum
| Action | Description |
|---|---|
pause |
Pause the active print |
resume |
Resume a paused print |
cancel |
Cancel the active print |
start_print |
Start printing a file; params include filename |
sync_files |
Sync the file list from the printer |
upload_file |
Upload a G-code file to the printer |
delete_file |
Delete a file from the printer |
create_backup |
Create a printer configuration backup |
homing |
Home the printer axes |
import_history |
Import print history from the printer |
curl example
curl -s https://your-spoolr-host/api/v1/connectors/$CONNECTOR_ID/commands \
-H "Authorization: Bearer $SPOOLR_SECRET" \
-H "X-Connector-Id: $CONNECTOR_ID"
4. Complete a Command
POST /api/v1/commands/:id/complete
Authentication: Bearer + X-Connector-Id.
Reports the outcome of a command. The :id is the command UUID returned by the poll endpoint.
If the command does not belong to the authenticated connector, the server returns
404rather than403— this is intentional to avoid existence disclosure.
Request body — succeeded
{
"status": "succeeded",
"result": {
"files_synced": 12
}
}
Request body — failed
{
"status": "failed",
"error_message": "Printer unreachable"
}
| Field | Type | Required | Notes |
|---|---|---|---|
status |
string | Yes | "succeeded" or "failed" |
result |
object | No | Arbitrary result data (on success) |
error_message |
string | No | Human-readable error detail (on failure) |
Success response — 200 OK
{ "ok": true }
Error responses
| Status | Condition |
|---|---|
404 Not Found |
Command not found or belongs to a different connector |
422 Unprocessable Entity |
status is not "succeeded" or "failed" |
curl example
curl -s -X POST https://your-spoolr-host/api/v1/commands/$COMMAND_ID/complete \
-H "Authorization: Bearer $SPOOLR_SECRET" \
-H "X-Connector-Id: $CONNECTOR_ID" \
-H "Content-Type: application/json" \
-d '{ "status": "succeeded", "result": { "files_synced": 5 } }'
5. Poll Webcam Requests
GET /api/v1/connectors/:id/webcam_requests
Authentication: Bearer + X-Connector-Id.
Returns up to 10 pending webcam requests for this connector. Each entry represents a Spoolr user waiting for a live snapshot from a specific printer.
Success response — 200 OK
[
{
"id": 99,
"printer_id": 17,
"status": "pending",
"requested_at": "2024-11-01T14:32:00.000Z"
}
]
Results are ordered oldest-first so the connector can serve them in FIFO order.
curl example
curl -s https://your-spoolr-host/api/v1/connectors/$CONNECTOR_ID/webcam_requests \
-H "Authorization: Bearer $SPOOLR_SECRET" \
-H "X-Connector-Id: $CONNECTOR_ID"
6. Upload Webcam Frame
PUT /api/v1/webcam_requests/:id/upload
Authentication: Bearer + X-Connector-Id.
Uploads a JPEG frame for a pending webcam request. The request body must be the raw image bytes.
Request
- Content-Type:
image/jpeg - Body: raw JPEG bytes
Success response — 200 OK
{
"status": "success",
"image_url": "https://your-spoolr-host/rails/active_storage/blobs/..."
}
Error responses
| Status | Condition |
|---|---|
403 Forbidden |
The webcam request belongs to a different connector |
404 Not Found |
Webcam request not found |
422 Unprocessable Entity |
Upload or record update failed |
curl example
curl -s -X PUT https://your-spoolr-host/api/v1/webcam_requests/$REQUEST_ID/upload \
-H "Authorization: Bearer $SPOOLR_SECRET" \
-H "X-Connector-Id: $CONNECTOR_ID" \
-H "Content-Type: image/jpeg" \
--data-binary @/tmp/snapshot.jpg
6a. Live Webcam Stream (smooth feed)
The webcam-request flow above delivers one frame per request — fine for a periodic snapshot, but a slideshow for live viewing. For a near-real-time feed, the connector relays the printer's MJPEG stream while a browser is actively watching.
Poll who is watching:
GET /api/v1/connectors/:id/webcam_stream — Bearer + X-Connector-Id.
Returns the connector's own printers that currently have an active viewer. Empty (the common case) when no one is watching, so polling it frequently is cheap.
[ { "printer_id": 12, "expires_in_ms": 8000 } ]
For each printer listed, open its MJPEG stream (e.g. mjpg-streamer's
http://<printer>:8080/?action=stream) and relay frames until expires_in_ms
elapses. A subsequent poll that still lists the printer extends the window.
Push a stream frame:
PUT /api/v1/printers/:id/webcam_stream_frame — Bearer + X-Connector-Id.
Body is the raw JPEG bytes of one frame (Content-Type: image/jpeg). Only the
latest frame is kept (in cache, not durable storage); push at a capped cadence
(~8fps is plenty). Returns 204 No Content on success, 403 if the printer
isn't owned by this connector, 422 on an empty body.
curl -s -X PUT https://your-spoolr-host/api/v1/printers/$PRINTER_ID/webcam_stream_frame \
-H "Authorization: Bearer $SPOOLR_SECRET" \
-H "X-Connector-Id: $CONNECTOR_ID" \
-H "Content-Type: image/jpeg" \
--data-binary @/tmp/frame.jpg
7. Push Snapshot Batch
POST /api/v1/snapshots/batch
Authentication: Bearer + X-Connector-Id.
Submits a batch of telemetry snapshots for one or more printers. Each printer in the batch is marked online and its last_seen_at, last_snapshot_at, and (optionally) moonraker_latency_ms are updated.
All printers in the batch must belong to this connector's user. captured_at is required on every snapshot.
Request body
{
"snapshots": [
{
"printer_id": 17,
"captured_at": "2024-11-01T14:32:00.000Z",
"payload": {
"result": {
"status": {
"extruder": { "temperature": 215.3, "target": 215.0 },
"heater_bed": { "temperature": 60.1, "target": 60.0 }
}
},
"normalized": {
"nozzle_temp": 215.3,
"bed_temp": 60.1,
"nozzle_target": 215.0,
"bed_target": 60.0,
"print_progress": 0.42,
"state": "printing"
},
"latency_ms": 18
}
}
]
}
| Field | Type | Required | Notes |
|---|---|---|---|
snapshots |
array | Yes | Must be a non-empty array |
snapshots[].printer_id |
integer | Yes | Must belong to this connector's user |
snapshots[].captured_at |
string (ISO 8601) | Yes | RFC 3339 timestamp from the connector |
snapshots[].payload |
object | No | Arbitrary telemetry; structure below |
payload.result.status |
object | No | Raw Moonraker status by sensor name |
payload.result.status.<sensor>.temperature |
number | No | Current temperature in °C |
payload.normalized |
object | No | Canonical cross-protocol telemetry fields |
payload.normalized.nozzle_temp |
number | No | Nozzle temperature in °C |
payload.normalized.bed_temp |
number | No | Bed temperature in °C |
payload.normalized.nozzle_target |
number | No | Nozzle target temperature |
payload.normalized.bed_target |
number | No | Bed target temperature |
payload.normalized.print_progress |
number | No | 0.0 – 1.0 |
payload.normalized.state |
string | No | Printer state string |
payload.latency_ms |
integer | No | Round-trip latency to the printer |
Success response — 201 Created
{ "inserted": 1 }
Error responses
| Status | Condition |
|---|---|
400 Bad Request |
snapshots key missing or not an array |
422 Unprocessable Entity |
Any snapshot missing captured_at, or a printer_id not owned by this connector's user |
curl example
curl -s -X POST https://your-spoolr-host/api/v1/snapshots/batch \
-H "Authorization: Bearer $SPOOLR_SECRET" \
-H "X-Connector-Id: $CONNECTOR_ID" \
-H "Content-Type: application/json" \
-d '{
"snapshots": [
{
"printer_id": 17,
"captured_at": "2024-11-01T14:32:00Z",
"payload": {
"normalized": { "nozzle_temp": 215.3, "bed_temp": 60.1, "state": "printing" },
"latency_ms": 18
}
}
]
}'
8. Upload Backup Archive
POST /api/v1/backups/:id/upload
PUT /api/v1/backups/:id/upload
Authentication: Signed token — NOT the bearer scheme.
Accepts a tar.gz archive for the identified backup record. The backup must currently be in queued or running state; any other state returns 422.
The signed token is a Rails MessageVerifier token containing the backup_id and an expires_at Unix timestamp. The server generates it when dispatching a backup command. Pass it as either:
- Query parameter:
?token=<signed_token> - Header:
X-Upload-Token: <signed_token>
The request body must be the raw tar.gz bytes (no multipart encoding).
Request
- Content-Type:
application/gzip - Body: raw
tar.gzbytes
Success response — 200 OK
{
"ok": true,
"backup_id": 7,
"status": "available"
}
Error responses
| Status | Condition |
|---|---|
400 Bad Request |
No file data in the request body |
401 Unauthorized |
Token missing, invalid signature, expired, or does not match the backup ID |
404 Not Found |
Backup record not found |
422 Unprocessable Entity |
Backup is not in an uploadable state (queued or running) |
500 Internal Server Error |
Unexpected error during storage |
curl example
curl -s -X POST \
"https://your-spoolr-host/api/v1/backups/$BACKUP_ID/upload?token=$UPLOAD_TOKEN" \
-H "Content-Type: application/gzip" \
--data-binary @/tmp/printer_backup.tar.gz
Error Format
All error responses follow one of two envelopes.
Single error:
{ "error": "message describing what went wrong" }
Validation errors:
{
"errors": {
"field_name": ["error message one", "error message two"]
}
}
Common status codes
| Code | Meaning |
|---|---|
200 OK |
Request succeeded |
201 Created |
Resource created successfully |
400 Bad Request |
Required parameter missing or malformed input |
401 Unauthorized |
Missing, invalid, or expired credentials |
403 Forbidden |
Authenticated but not permitted to act on this resource |
404 Not Found |
Resource not found (also used instead of 403 in some cases to avoid existence disclosure) |
422 Unprocessable Entity |
Validation failed or resource is in an invalid state for the operation |
500 Internal Server Error |
Unexpected server-side error |
Reference: Enum Values
Connector status
| Value | Description |
|---|---|
online |
Connector has sent a heartbeat recently |
offline |
Connector has not sent a heartbeat within the expected window |
Printer status
| Value | Description |
|---|---|
online |
Printer has sent a snapshot recently |
offline |
Printer has not sent data within the expected window |
unknown |
Initial state after pairing; no data received yet |
error |
Printer is reporting an error condition |
Command status
| Value | Description |
|---|---|
queued |
Command is waiting to be picked up by the connector |
running |
Command has been polled and is in progress |
succeeded |
Connector reported successful completion |
failed |
Connector reported a failure |
Webcam request status
| Value | Description |
|---|---|
pending |
Waiting for the connector to upload a frame |
completed |
Frame uploaded successfully |
failed |
Upload failed |
expired |
Request timed out before a frame was received |
Machine-Readable Access
This reference is also available as raw Markdown for tools and AI agents.
Append .md to this URL: /docs/api.md
The raw Markdown is served directly from the source file without any HTML processing, making it suitable for ingestion by language models, documentation generators, and automated integration tests.