Klipper / Moonraker (generic) β LAN Protocol¶
Status: π‘ source-read (transcribed from Moonraker + Klipper's own docs/source; ecosystem-corroborated, no first-party hardware capture in this paper) Β· Firmware: Klipper (motion) + Moonraker (API host), both open source Β· Models: any Moonraker host β DIY Voron / RatRig / Sovol + a large cluster of vendor Klipper forks (Qidi, Elegoo Neptune, rooted Creality, Snapmaker U1β¦)
Moonraker is the HTTP + WebSocket JSON-RPC API that sits in front of Klipper on
:7125. It is the de-facto LAN backbone for a whole class of printers, so "support Moonraker" really means "support a family." The one thing to internalize: there is no fixed schema β a printer exposes only the objects its config defines, so you enumerate, you don't assume.
At a glance¶
- Transport: JSON-RPC 2.0 over HTTP + WebSocket on
:7125(also a Unix socket; Moonraker can itself be an MQTT client). File ops are HTTP-only; status subscriptions are WebSocket-only. - Discovery: manual IP is the baseline (what Mainsail/Fluidd assume). Optional mDNS
_moonraker._tcpand SSDP exist but are off unless configured. - Auth / credential: often none on a trusted LAN (
trusted_clients); otherwise an API key (X-Api-Key) or a JWT. The user reads/creates their own on their own host β nothing is bundled. See../patterns/discovery-and-credentials.md. - Read / status: subscribe (WS) returns a full snapshot then sparse deltas; or one-shot query.
Capabilities are presence-based (
printer.objects.list). - File transfer: multipart
POST /server/files/upload(optional SHA256checksum). - Print launch: upload, then
printer.print.start{filename}(orupload?print=trueto couple them). - Feeders / multi-material: none natively β a software provider (Happy Hare, AFC, native TradRack) layers it on Klipper. Detect the provider, not the hardware.
- β οΈ The load-bearing gotcha:
progressis file-byte position, not time.virtual_sdcard.progress/display_status.progresstrack bytes streamed, so naiveelapsed/progressETA is systematically wrong near the end. (And the object model is per-install: assume a fixed set of objects and you break on the next config.)
Transport & connection¶
Everything is JSON-RPC 2.0. A request is {"jsonrpc":"2.0","method":"<method>","params":{β¦},"id":<n>}; the
response echoes the matching id; errors come back as a JSON-RPC error object with a code + message. Most
methods are reachable over both HTTP (POST /printer/..., GET /server/...) and the WebSocket, but the mapping is
not 1:1 β file upload/download is HTTP-only and printer.objects.subscribe is WebSocket/Unix-socket only.
π‘
The WebSocket lives at ws://<host>:7125/websocket. After it opens, a client should identify itself via
server.connection.identify (fields: client_name, version, type β web|mobile|desktop|display|bot|agent|other,
url, plus optional api_key / access_token); the reply carries a per-connection connection_id. π‘
Notifications are unsolicited JSON-RPC messages (no id), method + optional params array: π‘
notify_status_updateβparamsis a 2-element array:[ { <object>: { <changed fields only> } }, <eventtime> ]. Element[0]is a sparse delta;[1]is Klipper's float event timestamp.notify_gcode_responseβ[ "<response text>" ].notify_klippy_ready/notify_klippy_shutdown/notify_klippy_disconnectedβ noparams(lifecycle).- plus
notify_proc_stat_update,notify_history_changed, and others.
Reconnect posture: retry the WebSocket on a timer until it connects, then run the readiness handshake (query
server.info until klippy_state == "ready", or wait for notify_klippy_ready); watch for
notify_klippy_disconnected and repeat the handshake when it fires. The docs prescribe the loop but not a backoff
curve β an implementer picks one (e.g. ~1 s start, ~30 s cap). βͺ Two more standing reminders: subscriptions live on
the WS and are not restored for you β re-subscribe on every reconnect; and key liveness on the transport +
klippy_state, not on a heartbeat.
Discovery & identity¶
Manual IP is the guaranteed path β Mainsail and Fluidd normally connect to a hand-entered host or the reverse-proxy origin they are served from, not auto-discovery. When present, discovery is a bonus, not the assumed path: π‘
- mDNS: service type
_moonraker._tcp.local.(a Moonraker-specific type, not generic_http._tcp); the TXT record advertisesuuid,version,route_prefix. Loaded only if[zeroconf]is in the config β a stock Moonraker advertises nothing. - SSDP: device type
urn:arksine.github.io:device:Moonraker:1on239.255.255.250:1900; off unless enabled.
Identity probes (read these before committing to a session):
GET /server/infoβklippy_connected,klippy_state(disconnected|startup|ready|error|shutdown),moonraker_version,api_version,components[]. π‘GET /printer/infoβ only answers when Klippy is connected:state,hostname,klipper_path,config_file,log_file, and asoftware_versionin Klipper git-describe form (e.g.v0.12.0-85-gd785b396). A successful, internally-consistentprinter.infois the strongest "this is a real Klippy host" signal β the endpoint is gated on an actual Klippy connection. π‘GET /machine/system_infoβ host detail:cpu_info(incl. a sometimes-presentserial_number),network.<iface>. mac_address,distribution,sd_info,instance_ids(systemd unit names). None of these is both universal and tamper-resistant, so they corroborate identity rather than define it. π‘
Durable ID: Moonraker mints a persistent instance_uuid (a random uuid4) stored in .moonraker.uuid in its data
dir and in its database (moonraker:instance_id); it survives reboots and IP/hostname changes, and is advertised in the
zeroconf TXT uuid. Caveat: it is not listed in the documented HTTP response bodies of server.info / printer.
info / machine.system_info (source-read from Moonraker's server.py/database.py/zeroconf.py), and a synthetic
host could fabricate one β treat it as the preferred correlator but pair it with a corroborating fingerprint. π‘ If
you also expose a Moonraker-compatible surface yourself, reject a target whose uuid equals your own to avoid a
self-registration loop. βͺ
Credentials / auth¶
The local credential is the user's own, obtained from their own host β never bundled (see the
credentials pattern). Modes, from the [authorization] config: π‘
trusted_clientsβ IPs / CIDR ranges / FQDNs that bypass authentication entirely (no key or token needed). Documented constraint: an IPv4 range's last octet must be0(e.g.192.0.2.0/24). On an open LAN this is why a Moonraker printer frequently needs no credential at all.- API key β a static key supplied as the
X-Api-Keyheader (HTTP) or theapi_keyparam toserver.connection.identify(WS). The user retrieves it from Moonraker (access.get_api_key) or their host UI. - JWT β
Authorization: Bearer <token>(HTTP) /access_tokenon identify (WS); the access token expires after ~1 hour, refreshed from a longer-lived refresh token (~weeks, derived from example timestamps βͺ). - Oneshot token β a single-use
?token=β¦query param that expires in ~5 seconds, for contexts that can't set headers (the WS upgrade, browser limits). force_logins: truemakes login mandatory once any user exists, overridingtrusted_clients.
cors_domains governs browser cross-origin access only β it is not an auth layer and is largely irrelevant to a
non-browser client. For a headless integration the real gate is trusted_clients vs. key/JWT.
Reading state¶
Subscribe (the primary path). printer.objects.subscribe with params.objects = a map of { <object>: null }
(all fields) or { <object>: ["field", β¦] } (a subset). The response is the full initial snapshot β
{ eventtime, status: { β¦ } } β explicitly usable to seed local state without a separate query. Thereafter,
notify_status_update delivers only the changed fields; deep-merge each delta into local state field-by-field
and order by eventtime. printer.objects.query is the one-shot equivalent (same shape, no ongoing updates). π‘
Capabilities are presence-based β this is the core discipline. An object exists only if its config section does,
and named objects are keyed by "<section> <user-name>" (e.g. heater_generic chamber, fan_generic exhaust,
temperature_sensor mcu). So: enumerate printer.objects.list; resolve named objects by prefix (the suffix is
user-chosen and arbitrary); and treat a missing object as "not capable" / null, never an error. Tolerate unknown
enum strings by passing them through as unknown. π‘
Core objects and their load-bearing fields: π‘
| Object | Fields you'll use | Notes |
|---|---|---|
print_stats |
filename, state, print_duration, total_duration, filament_used, info.current_layer/total_layer, message |
durations in seconds; filament_used in mm |
virtual_sdcard |
progress (0.0β1.0), file_position, file_size, is_active |
β οΈ progress = file-byte position |
display_status |
progress (0.0β1.0), message |
also file-byte-derived |
toolhead |
position, homed_axes, axis_minimum/axis_maximum |
homed-axes + travel limits gate motion |
extruder / heater_bed |
temperature, target, power, can_extrude |
plain Β°C |
fan |
speed (0.0β1.0), rpm |
rpm only if a tachometer_pin is set |
idle_timeout |
state (Idle/Printing/Ready) |
|
pause_resume |
is_paused |
|
gcode_move |
speed_factor, extrude_factor, homing_origin |
live speed/flow multipliers |
Optional objects follow the same rule and simply appear when configured: heater_generic *, temperature_sensor *,
temperature_fan */controller_fan */heater_fan *, bed_mesh, exclude_object, probe/bltouch, filament_switch_
sensor */filament_motion_sensor *, TMC driver objects, mcu/mcu <name>, output_pin *, and more. Absence is the
normal case. π‘
State (native β normalized). print_stats.state has six documented values (the enum comes from Klipper source,
not the prose docs): standbyβstandby/idle; printingβprinting; pausedβpaused; completeβcomplete;
cancelledβcancelled; errorβerror. Map any other string to unknown. π‘
Progress & timing β the trap. Both progress fields are file-byte position (bytes streamed Γ· file size), not
elapsed-time fraction, so extrapolating an ETA from them is wrong near the end of a print. Prefer a
firmware/metadata time remaining, or estimated_time β elapsed from file metadata, and fall back to time-based
extrapolation only where progress is time-based (Klipper's isn't). print_duration/total_duration are plain
seconds (Γ1) β copying a minutes-based parser from another brand here is exactly how the "11-minute print shows 112
hours" 60Γ bug spreads. filament_used is mm β convert to grams volumetrically. This is the canonical case in
../patterns/timing-normalization.md (Β§2 file-byte, Β§3 seconds). π‘
Writing / control¶
Structured methods (each returns "ok"): printer.print.start{filename}, printer.print.pause, printer.print.
resume, printer.print.cancel, and printer.emergency_stop. π‘
β οΈ Emergency stop must use
printer.emergency_stopβ an out-of-band administrative shutdown. Never renderM112throughprinter.gcode.script: a queued M112 sits behind the g-code queue and won't fire immediately, defeating the whole point.
Everything else β temperatures, fans, speed/flow factor, homing, motion β has no structured endpoint and travels
through printer.gcode.script{script}. The safe posture is that an implementer owns typed templates and validates/
clamps every parameter, rather than passing user- or plugin-supplied raw g-code down this path. Recommended
templates (typed params only): π‘
- Temperatures (no-wait):
SET_HEATER_TEMPERATURE HEATER=<name> TARGET=<int>β the extended command takes an explicit heater name (unambiguous acrossextruder,heater_bed,heater_generic chamber, β¦), which is why it is preferred overM104/M140(implicit tool index). The no-wait form doesn't stall the queue;M109/M190block and are deliberately not used for a fire-and-forget "set temp". - Fans:
M106 S<0..255>/M107for the single primary part-cooling fan (no name needed);SET_FAN_SPEED FAN=<name> SPEED=<0.0..1.0>for any named fan. - Speed / flow:
M220 S<percent>(speed factor),M221 S<percent>(extrude factor). - Motion (gate carefully):
G28/G28 X|Y|Z(home) andG0/G1with an explicitG90/G91mode line are state-dependent β a move requires homed axes and is unsafe mid-print. Gate behind a confirmed homed state, a not-printing check, and travel-limit validation; always emit the mode line immediately before the move.
{name} in any template must be validated against printer.objects.list, never trusted as free text β that
blocks both typos and injection of arbitrary object names. Clamp temps to a per-heater max, fan bytes to 0..255,
percentages to sane bounds.
File transfer & launch. Multipart POST /server/files/upload: file field file; optional root (gcodes
default; gcodes/config are the only writable roots), path (subfolder, auto-created), checksum (SHA256 hex β a
mismatch returns 422, free integrity checking), and print ("true" starts the print after upload). Success is
HTTP 201 with a Location header and a body carrying item{path,root,modified,size,permissions},
print_started/print_queued, and action:"create_file". Read /server/files/roots for per-root permissions
(config may be read-only) rather than hardcoding. π‘
Two launch strategies: upload?print=true is atomic but couples upload and start and hides a started-vs-queued
decision inside Moonraker; separate calls (upload with print=false, then printer.print.start) give an
implementer control over conflict policy, metadata (/server/files/metadata, forced via metascan), and start-now
vs. enqueue (/server/job_queue, a FIFO with ready|loading|starting|paused). Note two documentation gaps: the exact
condition that yields print_queued vs print_started, and upload filename-conflict behavior (overwrite vs.
error) β both undocumented, so check existence client-side before uploading. βͺ
β οΈ Control writes drive a hot, moving machine. Validate against your own device and gate them behind an explicit "enable writes" in any client.
Multi-material / feeders¶
Klipper/Moonraker has no native feeder concept β multi-material is provided by a software layer on top of
Klipper, and the right move is to detect the provider by object presence, never by hardware brand. The full neutral
model (one opaque slot atom, an opaque per-adapter slot key, the print-time colorβslot map) lives in
../patterns/multi-material-feeders.md β this section is just the Klipper-side
detection map. π΅
- Happy Hare β the dominant provider. One object,
printer.mmu, fronts most open MMUs at once (ERCF, Box Turtle-on-HH, Night Owl, TradRack-on-HH, QuattroBox, 3MSβ¦), so reading it covers the family. Rich per-slot data:gate/tool(active slot / logical tool),gate_status[](0 empty / 1 available / 2 from-buffer),gate_material[]/gate_color[]/gate_color_rgb[]/gate_spool_id[],ttg_map(toolβgate), a nozzle-sidefilamentload state,action,endless_spool_groups, plus an optionalmmu.encodersub-object (flow_rate,headroom) on encoder-equipped units. User ops areMMU_*macros /Tn. - AFC (Armored Turtle) β Box Turtle's native, Happy-Hare-incompatible stack. Crucially, some AFC inventory is
published through the Moonraker database (
GET /server/database/item?namespace=lane_data) in addition to live Klipper status β so a reader must consult both surfaces to reconstruct full per-lane inventory (material, color, weight, spool id, PREP/LOAD sensor state, buffer). - Native TradRack β its own
printer.trad_rackobject (curr_lane,active_lane,next_lane,tool_map,selector_homed) with sparse inventory (no native material/color) β the neutral "remaining/material" fields must degrade tonull. (A TradRack run under Happy Hare instead appears asprinter.mmu.) - Vendor MMUs on forks β e.g. Creality's CFS surfaces through a vendor
[box]Klipper module read viaprinter.objects.query, not the[mmu]shape. Detect the vendor object, surface read-only, and don't assume Happy-Hare command semantics.
Vocabulary note: Happy Hare's gate and AFC/TradRack's lane are the same neutral slot; a tool (Tn) is a
logical color index resolved to a physical slot via a toolβslot map β it is not a toolchanger toolhead. Keep the
namespaces distinct. Detection rule (presence-based, no brand strings): mmu in the object list β Happy Hare (covers
its many kits); else trad_rack β native TradRack; else an AFC object / the lane_data DB namespace β AFC; else no
feeder. π΅
(Toolchangers β multiple docking toolheads with per-tool offsets, e.g. inferred from several extruder/extruder1β¦
objects plus Tn macros β are a separate, orthogonal concern from feeders; don't collapse the two.)
Vendor variants¶
Generic Moonraker is the backbone; a large cluster of printers reuse it with deltas. The headline risk is a
device that "claims Klipper" but ships a restricted or forked Moonraker β so never assume a current version, the
standard port, root access, open LAN trust, or standard MMU objects. Probe server.info / printer.info /
printer.objects.list first and degrade gracefully. π΅
Open / reference tier β a standard client just works. Generic upstream Klipper+Moonraker (KIAUH / Mainsail / Fluidd
on a Pi/SBC), Voron 2.4 / Trident / Switchwire, RatRig (RatOS = stock Moonraker + macros), VzBot, Sovol
SV08 / SV08 Max (vanilla Klipper, open LAN), and Ender/Prusa Klipper conversions. Stock port :7125, current
Moonraker, open LAN trust, standard objects. These are the validation baseline. π΅
Vendor-locked forks β feature-detect + degrade. The Moonraker contract is reachable but modified:
| Family | Delta an implementer must handle |
|---|---|
| Qidi (Q1 Pro / Plus4 / X-Plus 3 / X-Max 3) | Pinned-old Moonraker missing newer endpoints; MKS board. Feature-detect the older API; don't assume the current endpoint set. |
| Sovol | SV08 line is effectively open (above); other models track vanilla Klipper. |
| Rooted / stock Creality | K1 / K1C / K1 Max: nginx-reverse-proxied, root historically via helper script (2025 firmware removed the root menu). K2 / K2 Plus / K2 Pro: Fluidd+Moonraker on :4408 with reserved-path / permission-locked dirs, plus the CFS vendor [box] MMU. Discover the actual port; expect restricted paths. |
| Elegoo Neptune 4 / 4 Pro / 4 Plus / 4 Max | Vendor-modified, pinned-old Moonraker on an MKS board; Mainsail/Fluidd present. Closest of Elegoo's to stock β feature-detect the old Moonraker. |
| Snapmaker U1 | A Klipper+Moonraker+Fluidd fork with a :8100 auth-wrapper bootstrap and 4-toolhead toolchange via per-extruder objects + T0βT3 macros (no confirmed [toolchanger] object). Its bootstrap + toolchanger specifics get their own paper β see snapmaker.md. |
Not Moonraker at all (out of scope here). The Elegoo Centauri Carbon is marketed as Klipper-inspired but speaks a proprietary SDCP WebSocket protocol with UDP discovery β it exposes no Moonraker. A Moonraker client should classify it as "Klipper-marketed, non-Moonraker" and route it to the SDCP paper, not attempt to connect. π΅
Quirks & gotchas¶
- No fixed schema. Enumerate
printer.objects.list, resolve named objects by prefix (the suffix is user-chosen), and map missing β not-capable, never error. progressis file-byte position (virtual_sdcard/display_status), not time β ETA from it is wrong near the end. Time fields are seconds (Γ1) β beware the 60Γ copy-paste bug. (Both in the timing pattern.)- E-stop is the structured method, never a queued
M112. - Subscriptions are WS-only and are not restored on reconnect β re-subscribe every time. File ops are HTTP-only.
- Discovery is off by default β don't assume mDNS/SSDP will find anything; manual host entry is the baseline.
- The durable
instance_uuidisn't in the documented HTTP bodies and is forgeable β corroborate identity with a realprinter.info(git-describesoftware_version+ consistent filesystem paths); that same check guards against accidentally self-registering another Moonraker-compatible endpoint. - Upload overwrite behavior is undocumented β check existence client-side; and the camera stream bypasses
Moonraker auth (nginx proxies
/webcam/straight to ustreamer/camera-streamer), so API credentials don't gate it. - Vendor forks bite in four predictable ways: pinned-old Moonraker (missing endpoints), non-standard ports,
reserved-path/permission restrictions, and vendor-specific objects (CFS
[box], fork toolchanger macros).
Confidence & validation¶
- π‘ The generic protocol is source-read, not hardware-validated in this paper. Transport, JSON-RPC, the
subscribe-snapshot-then-delta model, auth modes, the file API, core object fields, and the
print_stats.stateenum are transcribed from Moonraker's official docs + source and Klipper's docs + source (reference point: Moonraker ~v0.10.0/8e948f1a, Klipper ~v0.13.0/c707dd19, 2026-06-19). It is heavily ecosystem-corroborated β every Mainsail/Fluidd/KlipperScreen client speaks exactly this β but the "latest" docs are a rolling channel; re-verify optional-object shapes against a target host's actual version. - π΅ Community-sourced: Happy Hare / AFC / TradRack field sets (their wikis; drift between releases and selector
variants), Creality CFS
[box], and every vendor-fork "openness" cell β those are snapshots that shift with OTA firmware. - βͺ Reported-but-unverified here: the reconnect backoff curve; the refresh-token exact lifetime; the upload
filename-conflict behavior; and the
print_queuedtrigger. Vendor-fork ports/versions are community-sourced π΅ (e.g. Creality K2:4408) and drift with OTA firmware β probe them, never hardcode. - Open gaps and the capture that closes each: delta behavior over a long print (ordering/
eventtimemonotonicity, how a removed field is signalled); object-absence on minimal configs; reconnect/Klippy-restart recovery; the upload conflict + queued-vs-started branch; live MMU object shapes (Happy Hare vs AFClane_data); whetherinstance_uuidis HTTP-readable on a real host; and the exact ports/versions of each vendor fork. A single multi-host field rig closes most of them. - Validation is accretive, never complete. Klipper's config space is effectively unbounded (user configs, MMU variants, vendor forks), so a hardware pass can only ever claim "validated on these configs," never "all Klipper printers."
- Fixtures: none ship for this family yet. The
fixtures/layout is defined (see../fixtures/README.md); a sanitized status/delta capture set β idle, printing, MMU, toolchanger, TradRack, plus temperature/MMU deltas β is the highest-value contribution to close the gaps above, and a real π’ hardware trace most of all.
Sources¶
Clean-room, facts-only. Generic protocol transcribed from Moonraker's own documentation (external API:
introduction, server, printer, machine, authorization, file_manager, job_queue, webcams, jsonrpc_notifications; plus
configuration) and source (server.py, components/database.py, components/zeroconf.py for the instance_uuid +
discovery facts); Klipper documentation (Status_Reference, Config_Reference, G-Codes) with the print_stats.state
enum taken from Klipper source (klippy/extras/print_stats.py). Multi-material from the Happy Hare and AFC
(Armored Turtle) docs/wikis and the TradRack repo. Vendor-variant deltas from community reports and setup docs
(Voron/RatRig/Sovol/KIAUH/Mainsail/Fluidd; Qidi, Creality K1/K2, Elegoo Neptune/Centauri, and Snapmaker U1
communities). No code was copied; all JSON shapes are described or shown as synthetic/sanitized examples; no
certificates, keys, tokens, access codes, or real IPs appear. Passed
../CLEANROOM-CHECKLIST.md.