API · v1
Raw Markdown ↗

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:

  1. Register — exchange a pairing token for credentials and printer IDs.
  2. Heartbeat loopPOST /api/v1/connectors/:id/heartbeat every 30–60 seconds to mark the connector online.
  3. Poll commandsGET /api/v1/connectors/:connector_id/commands every polling.commands_seconds (default: 3 s). Returned commands are automatically marked running.
  4. Complete commandsPOST /api/v1/commands/:id/complete after each command finishes, reporting succeeded or failed.
  5. Push snapshot batchesPOST /api/v1/snapshots/batch every polling.snapshots_seconds (default: 30 s) with telemetry for one or more printers.
  6. Serve webcam requests on demand — poll GET /api/v1/connectors/:id/webcam_requests, capture JPEG frames, and upload each via PUT /api/v1/webcam_requests/:id/upload.
  7. Upload backups on demand — when instructed (via a create_backup command), stream the archive to POST|PUT /api/v1/backups/:id/upload using 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.secret immediately — 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 404 rather than 403 — 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.gz bytes

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.