API Reference¶
Bambuddy provides a REST API for integration with external tools and automation.
Authentication¶
API Key Authentication¶
Include your API key in the X-API-Key header:
Getting an API Key¶
- Go to Settings > API Keys
- Click Create API Key
- Select permissions
- Copy the key (shown only once)
See API Keys & Webhooks for details.
Media Token¶
Thumbnails, plate previews, timelapses, print photos, QR codes, cover images and link icons are loaded by the browser as <img> and <video> sources, which cannot carry an Authorization header. Those routes therefore also accept a token in the query string:
curl -X POST -H "Authorization: Bearer <jwt>" \
http://localhost:8000/api/v1/auth/media-token
# => {"token": "..."}
curl "http://localhost:8000/api/v1/archives/42/thumbnail?token=..."
Any signed-in user can mint one; it is valid for 60 minutes and is not consumed, so one token serves every image on a page. It is authentication, not authorisation — each route still applies the permission and ownership rules of the resource it serves, so a media token reaches exactly the images its holder could already see listed.
Scripts and integrations do not need one
The media routes accept X-API-Key and Authorization: Bearer directly. The query token exists only for browser element loads.
Not the camera stream token
A camera_stream, camwall or overlay token is refused on these routes, and a media token is refused on the camera routes. Before Bambuddy 1.2.6 the media routes took the camera stream token, which made camera:view a prerequisite for seeing any image in the app; see Long-Lived Camera Tokens for what the camera scopes do reach.
Interactive API Browser¶
Bambuddy includes a built-in API browser for exploring and testing endpoints without external tools.
Accessing the API Browser¶
- Go to Settings > API Keys
- The API Browser appears in the right column
Features¶
| Feature | Description |
|---|---|
| OpenAPI Integration | Automatically loads all endpoints from the schema |
| Grouped by Category | Endpoints organized by printers, archives, settings, etc. |
| Parameter Inputs | Fill in path, query, and body parameters |
| Auto-examples | Request body pre-filled with schema examples |
| Live Execution | Execute requests and see real responses |
| Response Display | Formatted JSON with status code and timing |
| Search | Filter endpoints across all categories |
Testing with API Keys¶
- Paste your API key in the "API Key for Testing" input
- The key is sent as
X-API-Keyheader with each request - Test authenticated endpoints without external tools
Quick Setup
After creating a new API key, click "Use in API Browser" to automatically add it for testing.
Base URL¶
All endpoints are relative to this base URL.
Printers¶
List Printers¶
Response:
[
{
"id": 1,
"name": "Workshop X1C",
"ip_address": "192.168.1.100",
"serial_number": "01P00A000000001",
"model": "X1 Carbon",
"status": "idle"
}
]
Get Printer¶
Response:
{
"id": 1,
"name": "Workshop X1C",
"ip_address": "192.168.1.100",
"serial_number": "01P00A000000001",
"model": "X1 Carbon",
"status": "printing",
"current_print": {
"filename": "benchy.3mf",
"progress": 45,
"remaining_time": 3600
}
}
Get Printer Status¶
Response:
{
"id": 1,
"name": "X1C-Lab",
"connected": true,
"state": "RUNNING",
"progress": 45,
"remaining_time": 3600,
"layer_num": 120,
"total_layers": 267,
"temperatures": {
"nozzle": 220,
"nozzle_target": 220,
"bed": 60,
"bed_target": 60,
"chamber": 35
},
"hms_errors": [
{
"code": "0x8004",
"attr": 50364420,
"module": 3,
"severity": 2,
"actions": [],
"job_id": "1234567890",
"full_code": "03008004",
"description": "Filament ran out. Please load new filament."
}
],
"awaiting_plate_clear": false
}
layer_num is the current layer; total_layers is the layer count of the running job. temperatures carries a _target companion for the heaters that have one, and omits chamber entirely on models without a chamber sensor. state is the firmware's own value (IDLE, PREPARE, SLICING, RUNNING, PAUSE, FINISH, FAILED), not a lowercased Bambuddy label.
full_code identifies the fault: 16 hex characters for a fault from the printer's hms[] array (the four groups the printer screen shows), 8 for a print_error.
severity is Bambu's alert level: 1 error (the print was stopped), 2 warning (the print is paused), 3 notice (the print carries on), 0 when the printer set no valid level. For an hms[] fault it is the level the printer sends; a print_error carries no level, so it is taken from the first digit of the error number (4xxx → 1, 8xxx → 2, Cxxx → 3).
description is the text Bambu publishes for the fault on this printer model, taken from Bambu Studio's HMS files, so a client does not have to carry its own copy of the catalogue. It is English only and is not localized. It is null when Bambu publishes no text for the code. Treat null as "no text available", never as "no fault": the fault is fully reported either way, and full_code is what identifies it. Bambuddy's own UI counts a fault when it offers actions, or when it has a description and isn't an hms[] notice (severity 3 with a 16-character full_code). The same fields are on the printer_status WebSocket message.
awaiting_plate_clear is a Bambuddy-side gate, not printer telemetry. It goes true when a print reaches a terminal state and stays true until the plate is confirmed clear via Clear Plate; the queue will not dispatch the next job in the meantime. It survives restarts and Auto Off power cycles, so a printer that reports IDLE after a reboot can still be waiting. The same flag is pushed over the WebSocket printer_status message and over MQTT — including a dedicated retained topic, which is the better subscription for automations because it does not depend on the printer still being powered on.
Refresh Printer Status¶
Request a full status update from the printer via MQTT pushall command. Useful for getting fresh AMS data after swapping spools.
Response:
Errors:
404- Printer not found400- Printer not connected
Clear HMS Errors¶
Clear HMS/print errors on the printer. Sends a clean_print_error MQTT command and clears errors locally.
Response:
Errors:
404- Printer not found400- Printer not connected500- Failed to clear HMS errors
Permission: printers:control
Clear Plate¶
Acknowledge that the build plate has been cleared after a finished/failed print. Sets a plate-cleared flag so the scheduler can start the next queued print.
Response:
Acknowledgement is accepted whenever awaiting_plate_clear is true, whatever the printer currently reports — after an Auto Off power cycle it boots into IDLE with no memory of the finished print, and the gate still needs clearing. The reported state only matters as a fallback when the flag is not set.
The printer does not have to be online. Clearing the plate only mutates Bambuddy-side state — no command is sent to the printer — so it works on a machine Auto Power Off has switched off, which with that feature enabled is the normal end-of-print situation. The queue still waits for the printer to come back before dispatching; releasing the gate is what allows it to power the printer on again.
Errors:
404- Printer not found400- Printer is not awaiting acknowledgement and is not inFINISH/FAILEDstate
Permission: printers:clear_plate
Set Print Speed¶
Change the print speed preset during an active print.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
mode | int | Yes | Speed preset: 1 (Silent 50%), 2 (Standard 100%), 3 (Sport 124%), 4 (Ludicrous 166%) |
Response:
Errors:
404- Printer not found400- Printer not connected or no active print422- Invalid mode (must be 1-4)
Permission: printers:control
Add Printer¶
Request:
{
"name": "New Printer",
"ip_address": "192.168.1.101",
"access_code": "12345678",
"serial_number": "01P00A000000002"
}
Update Printer¶
Request:
Delete Printer¶
Download Multiple Printer Files¶
API clients can request a disk-backed ZIP containing files from printer storage:
POST /printers/{id}/files/download-zip
Content-Type: application/json
{
"paths": [
"/timelapse/video.mp4",
"/ipcam/ipcam-record.20260812.mp4"
],
"sizes": {
"/timelapse/video.mp4": 773468,
"/ipcam/ipcam-record.20260812.mp4": 250000000
}
}
The response is a ZIP attachment. sizes is an optional map of FTP-reported byte sizes. Supplying it lets Bambuddy reject an oversized selection or insufficient free space before FTP transfer begins; existing clients that send only paths, relative paths, or duplicate paths remain supported, and actual downloaded bytes are always capped. Up to 1,000 paths and 10 GiB total can be requested. Files that cannot be downloaded are skipped; response headers report requested, downloaded, and failed counts, and an all-failed request preserves the historical empty-ZIP response.
sizes is all-or-nothing: send it for every path or omit it entirely. A map covering only some of the selection is rejected, as is a negative size, and the keys must match the paths strings exactly — if paths are relative, the sizes keys have to be relative too.
| Status | Meaning |
|---|---|
400 | Empty selection |
413 | Selection exceeds 1,000 paths or 10 GiB |
422 | sizes does not cover exactly the selected paths, or a size is negative |
504 | The 30-minute preparation deadline passed |
507 | The app data volume cannot safely stage the selection |
Permission: printers:files
The web UI uses an asynchronous browser-native variant so neither large source files nor the result have to be buffered into a JavaScript Blob, and the initiating HTTP request does not occupy a proxy connection for the whole FTP transfer:
POST /printers/{id}/files/download-jobwith normal authentication andpaths,sizes,filename, andas_zip. The endpoint returns the job immediately. It validates more strictly thandownload-zipdoes: duplicate paths are rejected with400, andas_zip: falserequires exactly one path, because a native download has no container to put a second file in.- Poll
GET /printers/{id}/files/download-jobs/{job_id}. The body carriesjob_id,printer_id,state,requested,successful,failed,token,filename, andmessage, wherestateis one ofqueued,preparing,ready,failed, orcancelled.DELETEthe same URL to cancel; the FTP worker cooperatively stops and removes partial staging. - When
stateisready, the body'stokenfills in the nativeGET /printers/{id}/files/dl/{token}/{filename}URL. The token is short-lived, single-use, and bound to the printer ID. A spent or expired token answers403with a short text file rather than JSON, because the browser reaches this URL through a download click and saves whatever comes back.
All job and polling endpoints require printers:files, including the API key's optional printer_ids allowlist. Only the final /dl/ URL bypasses the gateway middleware, and it validates its resource-bound token itself. Staging lives under the configured archive data volume and is eligible for cleanup after one hour; cleanup runs at startup, before preparation, and every 15 minutes. Job state and cancellation are published as files, so any app worker can report on or cancel a job that another one is running. Preparations do not wait on each other: free space is re-checked against the bytes actually written throughout every transfer, so two jobs that start together each stop on their own when the volume runs low.
Archives¶
List Archives¶
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
printer_id | int | Filter by printer |
status | string | success, failed, stopped |
start_date | date | Filter from date |
end_date | date | Filter to date |
search | string | Full-text search |
project_id | int | Filter by project |
limit | int | Max results (default: 50) |
offset | int | Pagination offset |
Response:
{
"total": 1234,
"archives": [
{
"id": 1,
"name": "Benchy",
"filename": "benchy.3mf",
"printer_id": 1,
"printer_name": "Workshop X1C",
"created_at": "2024-01-15T14:30:00Z",
"duration": 8100,
"status": "success",
"filament_used": 45.2,
"filament_type": "PLA"
}
]
}
Get Archive¶
Find Archive Videos¶
Returns an attached timelapse, matching printer-side timelapse, and IP-camera chunks whose timestamps overlap the print window. Directory inspection is read-only; no printer file is downloaded until a separate download request is made. Callers without printers:files still receive an attached local timelapse, plus a printer_files_forbidden warning, while printer discovery is skipped.
A printer-side timelapse is only looked for when nothing is attached to the archive yet, so local_timelapse and a "kind": "timelapse" entry in remote_files do not appear together. When a copy is attached, local_timelapse is {"name": ..., "size": ...}; otherwise it is null.
{
"archive_id": 42,
"printer_id": 1,
"local_timelapse": null,
"remote_files": [
{
"name": "video_2026-08-12_14-38-46.mp4",
"path": "/timelapse/video_2026-08-12_14-38-46.mp4",
"size": 773468,
"mtime": "2026-08-12T14:40:12",
"kind": "timelapse"
}
],
"warnings": []
}
warnings distinguishes the reasons a listing came back short, so a caller can tell an unreachable printer from one that simply has no footage:
| Warning | Meaning |
|---|---|
printer_files_forbidden | The caller lacks printers:files, so only the attached copy was considered |
printer_missing | The archive names a printer that no longer exists |
timelapse_unavailable | No timelapse directory could be read — the printer is off, unreachable, or in the FTPS handshake cool-off |
ipcam_unavailable | /ipcam could not be read, for the same set of reasons |
An archive with no printer or no recorded start time returns empty lists and no warning, because there is nothing to look for rather than something that failed.
Permissions: archives:read_all or ownership through archives:read_own; printers:files is additionally required for the printer-side portion of the response
Download an Attached Timelapse¶
Mints a single-use token bound to this archive's attached timelapse and returns it with the file's name:
Then fetch the file itself, which needs no other credential:
The token expires after five minutes and is consumed on first use. Because a browser reaches this URL through a download click and saves whatever comes back, a spent token, a missing attachment, or a file gone from disk answers with a short text file explaining the failure rather than a JSON error body.
Permissions: archives:read_all or ownership through archives:read_own. Printer access is not required, and neither is camera:view — the attached copy is an archive asset. 404 if this archive has no attached timelapse.
Printer-side files found by the endpoint above are fetched through the printer download job endpoints instead, which require printers:files.
Update Archive¶
Request:
{
"name": "Updated Name",
"notes": "Great print",
"tags": ["functional", "gift"],
"filament_used_grams": 46.16
}
filament_used_grams is accepted between 0 and 100000 and is written to the archive's most recent run as well, so the filament totals on the Projects page and in the Prometheus metrics — which sum the runs, not the archives — agree with the card. A run that measured its own weight through spool tracking keeps that measurement; only a run with no figure, or one that inherited the archive's, is updated. Nothing is deducted from Spoolman or from internal inventory. It exists for a print that archived without its 3MF, where nothing else can supply a figure: the rescan endpoint reads the figure out of the 3MF, and such an archive has no file to read. On an archive that does have its 3MF, a rescan overwrites a hand-typed figure with the sliced one.
Delete Archive¶
Download 3MF¶
Returns the 3MF file as download.
Export Archives¶
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
format | string | csv or xlsx |
| (others) | Same filters as list |
Projects¶
List Projects¶
Get Project¶
Create Project¶
Request:
{
"name": "Voron Build",
"description": "Building a Voron 2.4",
"color": "#4caf50",
"target_count": 100
}
Update Project¶
Delete Project¶
Print Queue¶
Get Queue¶
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
printer_id | int | Filter by printer |
status | string | pending, printing, completed |
Add to Queue¶
Request:
Remove from Queue¶
Reorder Queue¶
Request:
List Batches¶
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
status | string | active, completed, or cancelled |
Batches with neither queue items nor per-plate targets are omitted — see Batch Orders. Fetching one by id (GET /queue/batches/{batch_id}) returns it regardless.
Create Batch or Order¶
Without plates this creates a plain grouping: pass item_ids to group existing pending items, or omit them and pass the returned id as batch_id on later POST /queue calls. With plates it becomes an order that records how many runs of each plate are wanted, so a failed run still counts as owed.
Request:
{
"name": "Bracket run",
"library_file_id": 42,
"plates": [
{ "plate_id": 1, "quantity_target": 1 },
{ "plate_id": 2, "quantity_target": 2 },
{ "plate_id": 3, "quantity_target": 3 }
],
"due_date": "2026-09-01T12:00:00Z",
"notes": "Rush job"
}
plate_id is null for a single-plate file. A quantity_target of 0 is allowed — a plate that is not required yet keeps its row so the target can be raised later — but an order in which every target is 0 is rejected.
Update an Order¶
Every field is optional. Supplying plates replaces the whole target set, so a plate left out of the list has its target row removed. Lowering a target below what has already been dispatched is allowed and simply leaves nothing owed; queued items are never cancelled implicitly.
Request:
Dispatch Remaining Runs¶
Creates queue items for the runs the order still owes. Each is copied from the most recent item for that plate, inheriting its printer or model target, AMS mapping, filament overrides and print options, and is appended to the end of the relevant printer's queue.
Request:
| Field | Type | Description |
|---|---|---|
plate_id | int | null | Which plate to dispatch. Only read when only_plate is true |
only_plate | bool | Restrict to the single plate named above. Default false — every plate with work outstanding |
limit | int | Cap on items created across all plates. Omit to queue everything owed |
Returns 400 when a plate owes runs but has never been queued, since there is no existing item to copy settings from, and when the batch has been cancelled.
Ungroup a Batch¶
Clears batch_id from every member the caller owns. The batch row is deleted once no members remain.
Cancel a Batch¶
Cancels the batch's pending items and marks the batch cancelled. Items that have already run are untouched.
Scheduled Drying Sessions¶
Get Scheduled Drying Sessions¶
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
printer_id | int | Filter by printer |
Returns sessions that are pending, running, or failed, earliest start time first, with sessions that have no start time ahead of the rest. Completed and cancelled sessions are not returned.
Permission: printers:read
Create Scheduled Drying Session¶
Request:
{
"printer_id": 1,
"ams_id": 2,
"temp": 45,
"duration_hours": 12,
"filament": "PLA",
"rotate_tray": true,
"start_after": "2026-07-26T18:00:00Z"
}
start_after is the earliest start, and it is optional: omit it (or send null) and the session runs as soon as the printer is idle and the AMS is ready. The drying popover has no equivalent — its Now option starts drying immediately through POST /printers/{id}/drying/start instead — so this is an API-only way to say "next time the printer is free".
Permission: printers:control
Cancel or Dismiss Scheduled Drying Session¶
Cancels a pending or running session and responds with {"status": "cancelled"}. A running session is also sent a stop command. On a failed session the record is deleted instead, and the response is {"status": "dismissed"}.
Permission: printers:control
Spool Inventory¶
List Spools¶
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
include_archived | bool | Include archived spools (default false) |
Get Spool¶
Find Spool by Tag¶
Look up a single spool by its NFC tag identifiers without listing the whole inventory. This is intended for NFC inventory integrations that scan a Bambu Lab spool tag and need to check whether it already exists before creating or updating it.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
tray_uuid | string | Bambu Lab spool UUID from the tag — the same value the AMS reports over MQTT |
tag_uid | string | RFID tag UID |
include_archived | bool | Include archived spools (default false) |
At least one of tray_uuid or tag_uid must be supplied. Values are normalised (case-insensitive, non-hex separators ignored). tray_uuid is matched first and tag_uid is used as a fallback. Returns the single matching spool.
Response:
{
"id": 42,
"material": "PLA",
"brand": "Bambu",
"color_name": "Red",
"tray_uuid": "AABBCCDDEEFF0011AABBCCDDEEFF0011",
"tag_uid": "04A1B2C3"
}
Errors:
400- Neithertray_uuidnortag_uidwas provided404- No matching spool found
Required scope
This endpoint accepts inventory read or inventory update access — for API keys, either the Read Status scope or the Manage Inventory scope. This lets a key that can already create, update, and delete spools look one up to dedupe an NFC scan. (Listing spools and fetching a spool by id still require Read Status.)
Statistics¶
Get Statistics¶
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
date_from | date | Period start, YYYY-MM-DD (inclusive) |
date_to | date | Period end, YYYY-MM-DD (inclusive) |
created_by_id | int | Only prints by this user (-1: prints with no user) |
Response (excerpt):
{
"total_prints": 1234,
"successful_prints": 1100,
"failed_prints": 100,
"cancelled_prints": 34,
"total_print_time_hours": 1000.5,
"total_filament_grams": 15000.5,
"total_cost": 350.0,
"total_energy_kwh": 120.4,
"total_energy_cost": 36.1,
"prints_by_filament_type": {"PLA": 900, "PETG": 334},
"prints_by_printer": {"1": 800, "2": 434}
}
Export Statistics¶
| Parameter | Type | Description |
|---|---|---|
format | string | csv (default) or xlsx |
days | int | How many days back (default 30) |
printer_id | int | Filter by printer |
project_id | int | Filter by project |
created_by_id | int | Only prints by this user (-1: prints with no user) |
Camera¶
An <img> or <video> tag cannot send an Authorization header, so the stream and snapshot endpoints also accept a token in the query string. Pass either a 60-minute browser token (POST /printers/camera/stream-token) or a long-lived camera token — see Long-Lived Camera Tokens.
Stream (MJPEG)¶
Returns MJPEG stream. Query params:
| Parameter | Type | Description |
|---|---|---|
fps | int | Frames per second (1-30) |
token | string | Camera token, when auth is enabled |
Snapshot¶
Returns single JPEG image. Accepts the same token query param.
Stop Stream¶
Terminates active streams for printer.
Cam Wall¶
Wall Feed¶
Every printer plus the handful of status fields a Cam Wall tile draws. One call for the whole wall — a kiosk display polls this on a fixed interval with no WebSocket to invalidate it.
| Parameter | Type | Description |
|---|---|---|
token | string | A Cam Wall-scoped camera token, when auth is enabled |
Authenticated only by a camwall-scoped token. A camera_stream token is refused here — it was minted to hand out video, not to enumerate a fleet by name.
[
{
"id": 1,
"name": "X1C-Lab",
"camera_rotation": 0,
"connected": true,
"state": "RUNNING",
"progress": 42.0,
"remaining_time": 33,
"layer_num": 120,
"total_layers": 300,
"hms_errors": []
}
]
That list is the entire payload. It deliberately carries no serial_number, no ip_address, no access_code, and no print filename: the token travels in a URL displayed on a screen, so the feed behind it must not disclose more than the camera picture already does.
Streaming Overlay¶
Overlay Status¶
Everything the streaming overlay draws for one printer — name, model, camera rotation, live print state, temperatures, and the one display setting it reads. A token-authenticated sibling of the printer status endpoint, so an OBS browser source with no login session can back the overlay.
| Parameter | Type | Description |
|---|---|---|
token | string | A Streaming Overlay-scoped camera token, when auth is enabled |
Authenticated only by an overlay-scoped token. A camwall or camera_stream token is refused here — unlike the Cam Wall feed this names the file being printed, so it sits behind its own scope.
{
"id": 1,
"name": "X1C-Lab",
"model": "X1C",
"camera_rotation": 0,
"connected": true,
"state": "RUNNING",
"current_print": "Benchy.gcode.3mf",
"gcode_file": "Metadata/plate_1.gcode",
"progress": 42.0,
"remaining_time": 33,
"layer_num": 120,
"total_layers": 300,
"stg_cur_name": null,
"temperatures": {"nozzle": 219.7, "nozzle_target": 220.0, "bed": 60.0, "bed_target": 60.0},
"time_format": "system"
}
model is null when the printer has none stored. temperatures holds only the readings the printer reports and is {} when it reports none; chamber readings are left out on models without a real chamber sensor.
That object is the entire payload. Like the Cam Wall feed it carries no serial_number, ip_address, or access_code — but it does carry the print filename, which is why the overlay scope is distinct from camwall.
Overlay branding logo¶
All paths below are relative to /api/v1. The logo is shared by the installation.
| Method | Path | Purpose | Permission when login is enabled |
|---|---|---|---|
POST | /settings/overlay-logo | Upload multipart file | settings:update |
GET | /settings/overlay-logo | Read PNG for settings preview | settings:read |
DELETE | /settings/overlay-logo | Remove saved logo | settings:update |
GET | /overlay-branding/logo?token=... | Read PNG for OBS | Valid overlay token |
Uploads accept static PNG and WebP images up to 2 MiB and 4 million pixels. Images are decoded, resized to fit 512 × 512, and stored as PNG with transparency. Invalid images return 400; oversized uploads return 413. Reading a missing logo returns 404. Reads use Cache-Control: no-store. Upload and delete return {"status":"ok"}. Deleting an absent logo succeeds.
System¶
System Info¶
Response:
Health Check¶
Response:
Error Responses¶
Error Format¶
HTTP Status Codes¶
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request |
| 401 | Unauthorized (no/invalid API key) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not Found |
| 429 | Rate Limited |
| 500 | Server Error |
Rate Limits¶
| Endpoint Type | Limit |
|---|---|
| Read | 100/minute |
| Write | 30/minute |
| Control | 10/minute |
Rate Limit Headers¶
Content Types¶
Request¶
Response¶
Except for file downloads (application/octet-stream) and images (image/jpeg).
Webhooks¶
Bambuddy can send webhooks for events. Configure in Settings > Notifications.
Webhook Payload¶
{
"event": "print_complete",
"timestamp": "2024-01-15T14:30:00Z",
"data": {
"printer_id": 1,
"printer_name": "Workshop X1C",
"archive_id": 123,
"filename": "benchy.3mf",
"duration": 8100,
"status": "success"
}
}
Event Types¶
| Event | Trigger |
|---|---|
print_started | Print begins |
print_progress | Progress milestone |
print_complete | Print finishes |
print_failed | Print fails |
printer_offline | Connection lost |
printer_error | HMS error |