OctoPrint (host controller) β LAN Protocol¶
Status: π‘ source-read (official API docs + AGPL server source; validatable hardware-free via the official Docker image + bundled Virtual Printer β no printer hardware needed) Β· Firmware: n/a β OctoPrint is a host controller, not a printer protocol; it fronts the printer's own firmware over USB/serial Β· Models: any USB/serial FDM printer behind an OctoPrint host (and, from 2.0, a Moonraker/Bambu connector)
OctoPrint is a computer in front of a USB printer. A client talks to OctoPrint's documented HTTP REST API + SockJS push channel and never to the firmware underneath β the physical printer is just metadata. One API key authenticates everything; the state model is push-primary with a poll backstop.
At a glance¶
- Transport: HTTP REST rooted at
/api/*on:5000(plain http) + a SockJS push channel at/sockjs.https, a URL path prefix, and HTTP Basic-Auth appear only when the operator fronts it with a reverse proxy. - Discovery:
_octoprint._tcpmDNS is the reliable channel (generic SSDPBasic:1is best-effort); manual IP/URL is the guaranteed path. - Auth / credential: one API key (
X-Api-Key:header, orAuthorization: Bearer). Recommended onboarding is the interactive Application Keys approval handshake; the user can also paste a key from their own instance. - Read / status: push-primary over SockJS (
current/historyframes), with a non-zero REST poll backstop (GET /api/printer+GET /api/job). Parse state offstate.flagsbooleans. - File transfer:
multipart/form-data POST /api/files/{local|sdcard}(form partfile+ inlineselect/printflags) β201. - Print launch: upload β (
select) βPOST /api/job {command:"start"}; or upload withprint=truein one shot. - Feeders / multi-material: none in core β OctoPrint exposes multi-tool (
extruder.count,tool{n}), which is a toolhead concept, not a filament feeder. Filament tracking is plugin territory. - β οΈ The load-bearing gotcha:
progress.completionis a fraction0.0β1.0(Γ100 for a bar) and it's file-byte position, not time β two separate traps in one field. See Reading state.
Transport & connection¶
The chain is client β OctoPrint instance β USB/serial (or a 2.0 connector) β printer. OctoPrint is a host
controller; you drive its API and treat the machine underneath as secondary metadata. π‘
- REST: plain HTTP on
:5000by default, rooted at/api/*, plus a few bundled-plugin routes under/plugin/*(e.g./plugin/appkeys/*for onboarding). Bodies areapplication/json(UTF-8). π‘ - Reverse-proxy shape:
https, a path prefix (e.g. an instance served under/octoprint/), and HTTP Basic-Auth only appear when an operator puts a proxy in front. When a path prefix is present it must be prepended to every/api/*call. π‘ - CSRF: OctoPrint's double-submit-cookie CSRF protection applies only to non-
GETrequests that rely on cookie auth. A header-key client bypasses CSRF entirely β no token needed. π‘ - Push: live state rides SockJS, mounted at
/sockjs. SockJS is a transport with its own handshake and framing over an underlying WebSocket/XHR β not a raw WebSocket JSON-RPC channel, so a client needs the SockJS handshake, not just a bare socket. The exact sub-URL and frame envelope need a live capture to pin. π‘ (mount) / βͺ (exact sub-URL + envelope)
Discovery & identity¶
mDNS / ZeroConf advertises two services: the generic _http._tcp and the OctoPrint-specific _octoprint._tcp
β key on the latter. Its TXT record carries path, u, p, version, api, model, vendor. π‘
β οΈ The TXT
u/pare a reverse-proxy Basic-Auth convenience, not the OctoPrint API key and not a security boundary β never treat them as auth, never persist them in a capture.
SSDP / UPnP announces the instance as a generic urn:schemas-upnp-org:device:Basic:1 device β that identifies
"a UPnP Basic device," not OctoPrint specifically, so it's best-effort; confirm any SSDP hit with /api/version.
π‘ Build the base URL as http://[u[:p]@]host:port[path] (https when useSsl is set); manual IP/URL entry is the
first-class, always-works path. See ../patterns/discovery-and-credentials.md.
Identity endpoints:
GET /api/versionβ{ api, server, text }.serveris the OctoPrint semver (e.g.1.11.8);textcarries the literal"OctoPrint <version>"β the"OctoPrint "prefix is the positive genuineness tell. π‘GET /api/serverβ{ version, safemode }(present from β₯1.5.0;safemodenames the reason it booted into safe mode, elsenull). π‘
The genuineness fingerprint (important). Other hosts expose an OctoPrint-compatible /api/* subset (PrusaLink,
notably), so "answers /api/version" does not imply genuine OctoPrint. Use a two-sided discriminator:
positive = the "OctoPrint " text prefix (corroborated by /api/server answering and/or _octoprint._tcp
mDNS); negative = it must not answer a /api/v1 info/status with a Prusa serial. Preserve the raw
text/server strings so a mis-fingerprint is diagnosable. π‘ (positive tell) / βͺ (the composite two-sided rule)
Credentials / auth¶
One credential β an API key, a user secret obtained from the user's own OctoPrint install. Transmit it as
X-Api-Key: <key> (primary) or Authorization: Bearer <key>; a ?apikey= query param exists but is testing-only.
Missing/invalid β 403 Forbidden when access control is on (OctoPrint's default). This orchard documents the
mechanism, never a value β prompt the user, store it encrypted, never bundle one. π‘
Application Keys β the recommended interactive onboarding (an approval handshake, not a static paste):
| Step | Request | Result / codes |
|---|---|---|
| Probe | GET /plugin/appkeys/probe |
204 β supported β else fall back to manual paste |
| Request | POST /plugin/appkeys/request β body { "app": "<name>", "user": <optional> } (app required, case-insensitive) |
201 + polling URL in the Location header (+ an app token) |
| Poll | GET /plugin/appkeys/request/<app_token> every ~1 s |
202 pending Β· 200 { "api_key": "<key>" } granted Β· 404 denied | expired |
| (user side) | the owner approves/denies in the OctoPrint web UI (POST /plugin/appkeys/decision/<user_token> β 204) |
β |
β οΈ Hard constraint: a pending request is considered stale and deleted internally if its polling endpoint isn't called for more than 5 s. Poll every ~1 s and do not back off while pending, or the grant is lost and onboarding restarts. π‘
The granted key is app-specific (least privilege) β prefer it over the global/user key. Manage/revoke lives at
GET/POST /api/plugin/appkeys. Keys are permission-scoped: a key granted only STATUS can drive the read/poll
backstop but not writes (which need CONTROL/PRINT). See
../patterns/discovery-and-credentials.md.
Reading state¶
Push-primary, poll-backstop. SockJS delivers live deltas; the REST poll is the fallback when the socket is down or the key is authed for status only.
SockJS handshake: call GET /api/login?passive=true (carrying the key) to obtain a session, then send an
{"auth": "<userid>:<session>"} frame over the socket. This is required before any status message arrives (the
permission system withholds them from a socket lacking STATUS). π‘ (mechanism) / βͺ (exact login body + pairing β
needs a capture)
Message-type keys β each push is a JSON object whose single top-level key names the type; ignore unknown
keys: connected, reauthRequired, current, history, event, slicingProgress, plugin. current and
history share a shape β { state:{text,flags}, job, progress, currentZ, offsets, temps, logs, messages, resends,
plugins }; history is the one-time backlog on connect, current is the live delta. reauthRequired means re-send
the auth frame. π‘
Poll surface (same data as the pushes):
GET /api/printerβ{ temperature:{ tool0:{actual,target,offset}, tool1β¦, bed, chamber, history? }, sd:{ready}, state:{ text, flags, error? } }. Returns409when the printer is not operational β treat that as a valid "not connected" signal, not a transport failure. π‘GET /api/jobβ{ job:{ file, estimatedPrintTime, averagePrintTime, lastPrintTime, filament:{tool0:{length,volume}} }, progress:{ completion, filepos, printTime, printTimeLeft, printTimeLeftOrigin }, state, error? }. π‘
State enum β parse state.flags booleans, never the human state.text. The 9 documented flags are
operational, printing, paused, pausing, cancelling, sdReady, error, ready, closedOrError. The server source emits
11, adding resuming and finishing; prefer those two flags when present, otherwise derive them from the
state string (a resuming/finishing printer otherwise just reports printing:true). π‘ (9 documented) / βͺ (the 11-flag
set β confirm which a live instance emits)
| Native signal (flags first) | Normalized | Notes |
|---|---|---|
closedOrError && !error / text Offline |
offline | not connected to the printer |
text Openingβ¦/Detectingβ¦/Connecting (no flag) |
connecting | transient; string-derived |
operational && !printing && !paused (Operational) |
idle / ready | the "ready for a job" state |
printing (+ finishing hint) β text Starting/Printing/Finishing |
printing | 3 printing-variant strings ("Printing", "Printing from SD", "Sending file to SD") β don't string-match them |
resuming flag or text Resuming |
printing (resuming) | prefer the flag; else string-derive |
pausing (Pausing) |
pausing | |
paused (Paused) |
paused | |
cancelling (Cancelling) |
cancelling | |
error / closedOrError && error (Error/Offline after error) |
error | carries the message |
β οΈ
Operationalis ambiguous β it is both "idle/ready" and the base state under which the printing/paused flags ride. Readprinting/paused/pausing/cancellingbefore concluding idle. βͺ
Temperatures: temperature.{tool0β¦N, bed, chamber}.{actual, target, offset}, Β°C floats. actual with no
target = read-only monitoring; target: 0 = heater off; offset is the user's manual temp offset. chamber is
present only if the profile has a heated chamber. π‘
Progress & timing (the load-bearing traps β see ../patterns/timing-normalization.md):
progress.completionis a fraction0.0β1.0(a real docs example is0.2298β¦) β multiply by 100 for a percent bar; do not treat it as already-percent. One live-confirm remains open. π‘- It is file-byte position (
filepos/size), not time-based β extrapolating an ETA from it is systematically wrong near the end of a print. For "remaining," trust the firmware-reportedprogress.printTimeLeftinstead. π‘ - All times are in SECONDS (
printTime,printTimeLeft,estimatedPrintTime,averagePrintTime,lastPrintTime);filepos/sizeare bytes.printTimeLeftmay benullearly in a print. π‘ printTimeLeftOriginqualifies ETA quality βlinear,analysis,estimate,average,mixed-analysis,mixed-average.estimate/linearare coarse guesses;analysis/averageare file-analysis-backed. Surface the ETA with a caveat (or hide it) when the origin is a bareestimate/linear. π‘
Missing-field rule: everything except the state signal degrades gracefully β chamber absent without a heated
chamber, temperature partial when not operational, progress.* null/absent with no active job. A missing
temp/progress/file must not be read as 0. βͺ
Writing / control¶
OctoPrint exposes structured, typed endpoints for the whole core surface β no raw-G-code passthrough is required
for normal control. All control POSTs return 204 No Content with an empty body; read the effect from the next
SockJS current push or a follow-up GET, never from the POST body.
| Neutral intent | Endpoint | Body |
|---|---|---|
| Print start / cancel / restart | POST /api/job |
{command:"start"\|"cancel"\|"restart"} (restart needs a paused job) |
| Pause / resume | POST /api/job |
{command:"pause", action:"pause"\|"resume"} β explicit, never toggle |
| Set tool temp | POST /api/printer/tool |
{command:"target", targets:{tool0:N}} β Β°C map, 0=off |
| Set bed / chamber temp | POST /api/printer/{bed,chamber} |
{command:"target", target:N} β gate on heatedBed/heatedChamber |
| Jog / home / feedrate | POST /api/printer/printhead |
{command:"jog", x,y,z, speed, absolute} Β· {command:"home", axes:[β¦]} Β· {command:"feedrate", factor:105} |
| Tool select / extrude / flowrate | POST /api/printer/tool |
{command:"select", tool:"tool1"} Β· {command:"extrude", amount, speed} Β· {command:"flowrate", factor:0.95} |
Connect / disconnect / fake_ack |
POST /api/connection |
host-link management, not printing β treat as admin/dangerous |
| SD init/refresh/release | POST /api/printer/sd |
{command:"init"\|"refresh"\|"release"} |
| Raw G-code | POST /api/printer/command |
{commands:[β¦]} β can interrupt/stop a print; keep gated off by default |
β οΈ
feedrate.factoris a PERCENT (105= 105 %), butflowrate.factoris a FRACTION (0.95= 95 %). This asymmetry is documented and a real footgun β don't conflate them. π‘β οΈ The
pauseaction defaults totogglewhen omitted β always send an explicitpause/resume, or you race the true state. π‘
409 is a precondition, not a fault. Control POSTs return 409 when: jog/home β not operational or currently
printing; tool commands (except target) β not operational; SD β card not initialized; bed/chamber β the profile
lacks that heated component. Gate bed/chamber on heatedBed/heatedChamber and jog/home on
operational && !printing before sending, so a 409 becomes a rare race rather than a routine outcome. π‘
β οΈ Control writes drive a hot, moving machine β validate them against your own instance and gate them behind an explicit "enable writes" in any client.
fake_ack(an emergency action for a stalled serial line) and connect/disconnect are host-management operations, not a normal workflow β keep them admin-gated.
Print launch & file transfer. The Files API is rooted at /api/files; location β local (OctoPrint's uploads
folder) | sdcard (the printer's SD). Upload is multipart/form-data POST /api/files/{location} with the form
part file plus flag fields: path (subfolder), select (bool, default false), print (bool, default
false), userdata (a JSON string; invalid β 400), and foldername (an alternative to file that creates a
folder β mutually exclusive with it). π‘
Response is 201 { files:{ local:{β¦}, sdcard? }, folder?, done, effectiveSelect, effectivePrint }:
doneisfalsewhile an SD stream is still in progress (final completion arrives via SockJS).effectiveSelect/effectivePrintecho what actually happened β aprint=truethe printer could not honor comes backfalse. Always read them back. π‘- Codes:
201success Β·400(nofile/foldername, or baduserdata) Β·404(bad location) Β·409(would interrupt an active print, or SD busy) Β·415(extension not an accepted machinecode type β.gcode/.gco/.g; model types are slicer-plugin dependent) Β·500.
Two launch flows:
- Upload-and-hold (recommended): upload with
select=false, print=falseβ201; later selectPOST /api/files/{location}/{path} {command:"select", print:false}β204; thenPOST /api/job {command:"start"}β204. This lets a client verify the stored file and keep its own job record authoritative. - Upload-and-print (one-shot): upload with
print=trueβ201, then read backeffectivePrintto confirm it actually started.
OctoPrint is the notable case where launch intent can ride inline in the upload (the
select/201+done:true; a dropped transfer means re-POST the whole multipart (no resume protocol). π‘
Other Files ops: POST /api/files/{location}/{path} {command: select|unselect|copy|move|slice};
DELETE /api/files/{location}/{path} β 204 (409 if it's the active print). sdcard files expose only
name/path/origin/size β no date, no gcodeAnalysis, no download. A machinecode file also carries gcodeAnalysis
(estimatedPrintTime, filament:{length,volume}, dimensions) for additive enrichment; thumbnails are
plugin-dependent, not core. π‘
β οΈ Security floor β OctoPrint β₯ 1.11.8. The upload endpoints carried a High-severity file-exfiltration flaw (CVE-2026-54134, an incomplete-fix follow-up to CVE-2025-48067; patched in 1.11.8). A client should send only the documented public form fields (
file,path,select,userdata,foldername) β never OctoPrint's reserved internal upload fields β and warn when a connected server reports< 1.11.8while upload is enabled. π΅ (advisory-sourced)
Multi-material / feeders¶
Core OctoPrint has no multi-material / spool / filament-slot model β no per-slot type/color/material, no
active-slot, no load/unload, no per-slot runout anywhere in the documented /api surface. π‘
What it does expose is multi-tool: extruder.count and per-tool tool{n} temperature channels + tool-select
({command:"select", tool:"tool1"}). That is a toolhead concept, not a filament feeder β keep them separate
(see ../patterns/multi-material-feeders.md, which treats a toolchanger as
orthogonal to a feeder). extruder.sharedNozzle distinguishes a switching-nozzle / IDEX-style setup from truly
independent hotends (a metadata hint). π‘
Filament tracking in the OctoPrint world is plugin territory (Filament Manager, SpoolManager, β¦), each adding its
own plugin API. An implementer should contribute presence-only to a neutral feeder model (at most a hint from
extruder.count > 1 / sharedNozzle) and never fabricate slot data; keep any plugin-sourced filament data
plugin-local rather than promoting it into the neutral model. βͺ
Capabilities & build volume come from GET /api/printerprofiles β { profiles:{<id>:{β¦}} }. Each profile carries
model, heatedBed, heatedChamber, volume{ formFactor(rectangular|circular), origin, width, depth, height },
axes{x,y,z,e:{speed,inverted}}, and extruder{ count, offsets[[x,y]], nozzleDiameter, sharedNozzle }. Treat these as
hints, not ground truth β they are user-configured and can be wrong or default; allow the user to override display
metadata, gate bed/chamber control on the heated* flags, and read a circular form-factor's diameter from width. π‘
Quirks & gotchas¶
- It's a host, not a printer. You never see the firmware; the machine underneath is metadata only (
profile.model, best-effort). Never route a dialect off it. - Fingerprint before trusting
/api/*. Other hosts expose an OctoPrint-compatible subset β require the"OctoPrint "textprefix (and not a Prusa/api/v1). progress.completionis a fraction0.0β1.0(Γ100) and file-byte position, not time β two traps in one field.feedrate= percent,flowrate= fraction β don't conflate.pausedefaults totoggleβ always send explicitpause/resume.- 9 documented state flags vs 11 in source (
resuming/finishing) β prefer the flags, else derive from the string; and never string-match the 3 printing-variant literals. 409is a signal, not an error βGET /api/printer409= not operational; upload/job409= would interrupt / SD busy. Gate preconditions before sending.- SockJS β raw WebSocket β it needs the SockJS handshake + framing, plus a passive-login
sessionand an{"auth":"userid:session"}frame before any status arrives. - Reverse-proxy shape β prepend the path prefix to every
/api/*call;https/Basic-Auth appear only when proxied; the mDNSu/pare not the API key. - Version: target the 1.11.x stable line (floor 1.11.8). 2.0 was RC-only as of mid-2026; it moves comms into
pluggable connectors (Serial / Moonraker / Bambu) but keeps the same
/api/*+ SockJS surface, adds anX-OctoPrint-Api-Versionrequest header for API pinning, and adds native thumbnails + multi-storage β so an implementer stays 2.0-ready by not hardcoding "onelocal+ onesdcard" and instead reading the storage list. π΅
Confidence & validation¶
Overall π‘ source-read β nothing here is hardware-validated in this pass. The whole surface is closeable
hardware-free: the official octoprint/octoprint Docker image + bundled Virtual Printer reproduce
idle β printing β paused β cancelled β error β comm-error on a developer's own machine, no printer required.
- π‘ documented first-party: the
/api/*endpoints, form/JSON field names, status codes,state.flagsbooleans, the SockJS message-type keys, the Application-Keys handshake + the 5 s poll-staleness rule, the temperature and progress shapes, and theprinterprofilesmodel β all from the official API docs, read-confirmed against the server source for exact strings. - βͺ open gaps (a Docker + Virtual-Printer capture closes each):
- the exact
/sockjssub-URL + frame envelope, and whether a plain WebSocket client carries SockJS framing directly or a SockJS handshake shim is required; - the passive-login
POST /api/login?passive=truebody and the{"auth":"userid:session"}pairing; - the appkeys
201/202/200/404body shapes, and whether/api/version+/api/serveranswer unauthenticated (a pre-auth fingerprint); - a live confirm that
progress.completionis a fraction on a real 1.11.x instance; - multi-tool temp keys (
tool1,tool2) + tool-select on a 2-extruder virtual profile; - whether the
409bodies carry a machine-readable reason (not-operational vs profile-lacks-component vs printing); - whether 1.11.x silently ignores
X-OctoPrint-Api-Version(safe to always send); - real
_octoprint._tcpTXT contents (aremodel/vendorpopulated?); - whether a commonly-bundled filament plugin leaks per-slot state into
current/pluginframes before finalizing "no feeder."
Sources¶
Clean-room, facts-only. Built from OctoPrint's official REST / Application-Keys / discovery API documentation
(endpoint paths, JSON field names, enum/state strings, header names, status codes β interface facts), read-confirmed
against the AGPL-3.0 OctoPrint/OctoPrint server repo for the exact state strings, the SockJS mount, and the SSDP
URN without copying code, and cross-checked against two stale MIT community clients for field-name sanity only.
No AGPL code was copied or vendored β neither the server nor the same-license in-repo JS client (whose SockJS
parsing is a copy risk, not a safe reference). Version/CVE facts are from the OctoPrint release blog and GitHub security
advisories. No API keys, Basic-Auth values, hosts, or serials appear here (the API key is the user's own, obtained from
their own instance). Hardware-unvalidated in this pass β validatable hardware-free via the official Docker image +
bundled Virtual Printer. Passed ../CLEANROOM-CHECKLIST.md.