Creality (stock Creality OS) β LAN Protocol¶
Status: π‘ source-read (the stock
:9999wire is read from Creality's own device-manager frontend, but no stock device has been bench-validated here) Β· Firmware: Klipper-based "Creality OS" (proprietary LAN service on top) Β· Models: Ender-3 V3 / V3 KE / V3 Plus, K1 / K1C / K1 SE / K1 Max, K2 Plus / K2 Pro, Hi, CR-10 SE"Creality" is three protocols wearing one brand, split by firmware, not model line. This paper documents the stock bucket: an unauthenticated plain WebSocket at
ws://<ip>:9999carrying a flat{"method","params"}envelope with unsolicited state pushes, plus a no-auth:80/uploadfor files. The other two buckets cross-link out.
The three buckets (route by live service, not by model name)¶
| Bucket | Firmware / LAN | Representative models | Where documented |
|---|---|---|---|
| A β Marlin/USB | Marlin, USB/SD, no WiFi | Ender-3 V3 SE, Ender-3 V2/S1, CR-10 classic | marlin-serial.md |
| B β stock Creality OS | Klipper-based, WiFi, proprietary ws://:9999 |
Ender-3 V3 KE/V3/Plus, K1/K1C/K1 SE/K1 Max, K2 Plus/Pro, Hi, CR-10 SE | this paper |
| C β rooted β Moonraker | any bucket-B model after rooting β standard Moonraker :7125 |
" (after the community root script) | klipper-moonraker.md |
β οΈ SE vs KE are one letter apart and opposite buckets β Ender-3 V3 SE is Marlin/no-WiFi (bucket A), KE is Klipper/WiFi (bucket B). Never infer the bucket from the model string; fingerprint the live service (see Discovery) and re-fingerprint on reconnect, since a firmware update can revert a rooted printer back to stock. π‘
At a glance¶
- Transport: plain WebSocket
ws://<ip>:9999(text frames, no TLS, no auth) + plain-HTTP:80/uploadfor files. Not JSON-RPC, not SDCP, not MQTT β the simplest transport in the orchard. - Discovery: manual IP always works; the vendor slicer also uses a UDP broadcast. Identity via
GET http://<ip>/info. - Auth / credential: none on the LAN socket (LAN-trust). Nothing for the user to obtain. (The separate, account-bound Creality Cloud path is a different transport β out of scope here.)
- Read / status: push-hybrid β the printer pushes flat state objects unsolicited; a client merges each into one
running dict and pokes a
geton a timer as backstop. - File transfer:
POST http://<ip>:80/upload/<name>(multipart / raw body, no auth). - Print launch: two-step β HTTP upload, then a WebSocket
setwithopGcodeFile: "printprt:"+<path>. - Feeders / multi-material: CFS (Creality Filament System) β up to 4 units Γ 4 slots; rich (per-slot % + RFID + dry-box temp/humidity).
- β οΈ The load-bearing gotcha: the same brand speaks three different protocols; and stock progress is a percent but the OS is Klipper-based, so treat it as file-byte position, not time until a capture proves otherwise.
Transport & connection¶
One long-lived plain WebSocket at ws://<ip>:9999, no path suffix, no subprotocol argument, no TLS. The socket
is unauthenticated β no token, header, or password (LAN-trust). One JSON object per text frame. π‘
Message envelope β two verbs, flat, un-correlated: π‘
// the client SENDS:
{"method":"set","params":{ <paramKey>:<value>, ... }} // control a setting / issue an action
{"method":"get","params":{ <reqKey>:1, ... }} // ask the printer to (re)emit a data block
method wrapper, just the field bag
(e.g. {"nozzleTemp":210.4,"printProgress":42,...}), streamed unsolicited while connected. There is no response
correlation: a set/get is fire-and-forget, and you observe its effect in the next pushed object. A client
merges every inbound object into one running state dict. π‘
Keepalive & reconnect. Independent community clients describe an in-band heartbeat β the printer sends
{"ModeCode":"heart_beat"} and the client replies the literal string "ok", with a get-poke after ~10 s of silence.
Neither the heartbeat string nor the advertised wsslicer subprotocol appears in the vendor's own frontend β treat
both as community lore: implement the heartbeat defensively, but key liveness on the transport connection, not on it.
Reconnect with backoff. π΅
Auxiliary services on the same host: MJPEG camera http://<ip>:8080/?action=stream (K1 / V3 / Hi); WebRTC
http://<ip>:8000/call/webrtc_local (K2 family and newer); file downloads under http://<ip>/downloads/{gcode,video,
humbnail}/β¦ (the humbnail spelling is the vendor's, not a typo here). The older Halot resin line uses a
different port (:18188) and is out of FDM scope. π΅
Discovery & identity¶
Manual IP is the guaranteed path. The vendor slicer additionally discovers over UDP broadcast: it broadcasts a
prefixed probe and printers reply with a small JSON {"answer":β¦,"machineIp":β¦}; a classifier regex splits a rooted
Moonraker machine (a machine-type field of "00") from a stock one. A community forum note mentions a possible
secondary beacon on UDP :5353. π‘ (vendor slicer) / π΅ (the :5353 note)
Identity probe: GET http://<ip>/info returns a small JSON carrying model and mac (some firmware also vtIp).
It is cheap β read it before committing to a session. π‘
Fingerprint (which bucket?):
if standard Moonraker answers on tcp:7125 β ROOTED (bucket C) β see klipper-moonraker.md
elif tcp:9999 accepts a plain WS upgrade β STOCK (bucket B) β this paper
else β not LAN-networked (bucket A / offline)
:4408 /
:4409), not LAN-reachable. Route on LAN :7125 reachability, never on "Moonraker exists." Rooting rebinds
Moonraker to the LAN. Re-fingerprint on every reconnect (an OTA can revert root). π‘
See ../patterns/discovery-and-credentials.md for the cross-vendor
discovery/LAN-mode shapes.
Credentials / auth¶
The stock LAN :9999 socket is unauthenticated β there is no access code, token, or client certificate to
obtain, and none to ship. This is a pure LAN-trust model; an implementer connects directly. That is the whole story for
the local path documented here.
A separate Creality Cloud service exists for remote access. It is a different transport (an account-bound MQTT
relay), it requires the owner to bind the device to their account and hold a per-user cloud token (obtained through
Creality's app/site β a mechanism, never a value), and it is region-bound and firmware-fragile (a firmware update
can unbind it). It reaches the same printers, but the LAN path needs none of it and it is out of scope for this
paper. Crucially, the cloud user token is not any kind of LAN credential β do not conflate them.
See ../patterns/discovery-and-credentials.md Β§1β2.
Reading state¶
Push-hybrid. The printer streams flat state objects unsolicited; a client merges each inbound object into one
running dict and additionally pokes a get on a timer (~2β5 s) as a backstop, plus a boxsInfo poll only when a
feeder is present (cfsConnect). This sits between pure-push (Moonraker) and pure-poll β closer to a gentle poll with
live pushes during a print. See ../patterns/discovery-and-credentials.md
Β§5.
The report is a flat field bag. Field spellings drift across models/firmware, so an implementer needs an alias map (some targets arrive under two names). Key fields (exact wire keys): π‘
- Temperatures (plain Β°C): nozzle
nozzleTemp(current) /nozzleTemp2(target; some firmwaretargetNozzleTemp) /maxNozzleTemp; bedbedTemp|bedTemp0/bedTemp2(target) /maxBedTemp(indexed for multi-bed); chamberboxTemp/boxTemp2. A structuredtemperature.{nozzle,bed,box}.{value,target,max}also appears. - Progress:
printProgress(0β100 int; alsodProgress). β οΈ A percent, but the OS is Klipper-based β very likely FILE-BYTE position, not time. Do not extrapolate an ETA from it until a capture proves it is time-based. See../patterns/timing-normalization.mdΒ§2. π‘/βͺ - Times:
printLeftTime(remaining),printJobTime(elapsed),printStartTime. Source-read as seconds (unlike the minutes some brands use), but the tick unit still wants a capture-confirm β log the raw value before any math. See../patterns/timing-normalization.mdΒ§3. π‘ - Job:
printFileName,layer,TotalLayer. - Fans / light / speed:
modelFanPct/caseFanPct/auxiliaryFanPct(+ aliasesfan/fanCase/fanAuxiliary);lightSw(0/1);curFeedratePct/speedMode. - Feed / runout:
feedState,FilamentStatus,materialStatus(1= runout);cfsConnectgates the feeder poll. - Error:
err:{errcode,key,value,errLevel}.err.keyindexes a 139-entry message dictionary the vendor frontend carries; the neutral fault test iserrcode != 0(orkey β {0, 30000}), then look upkeyfor the text.
State (native state int β normalized). Read from the vendor frontend's own write-gate logic, which groups the
edges: π‘ (grouping) / βͺ (fine labels for 6/7/8)
state |
native meaning | normalized |
|---|---|---|
| 0 | idle / processing | standby |
| 1 | printing | printing |
| 2 | complete | complete |
| 3 | failed | error |
| 4 | abort / stopped | cancelled |
| 5 | paused | paused |
| 6 | pausing / paused-variant | paused (busy edge) |
| 7 | busy (stopping / homing?) | busy |
| 8 | printing-variant (recovery?) | printing (busy edge) |
The frontend treats state β {1,5,8} as "printing in progress" and {5,6} as "paused", which pins 6βpaused-side,
8βprinting-side, 7βbusy β enough to map all of 0β8 safely; only the fine label per edge wants a live capture.
β οΈ deviceState is a separate online/health integer (-1 offline / 0 online-idle / 1 busy) β do not conflate
it with state. For an "is it safe to write?" gate, derive from the composite read (non-empty printFileName +
printProgress + err.errcode + selfTestStep) plus the busy set {1,5,6,7,8}, not a single field. π‘
Writing / control¶
All control is {"method":"set","params":{<key>:<val>}}, fire-and-forget β no ACK; confirm by watching the next
pushed state. The command shapes below are read from the vendor's own frontend (first-party source), but the
runtime behavior is unvalidated on hardware β every write drives a hot, moving machine, so gate writes behind an
explicit "enable" and validate against your own device before trusting them.
| Intent | params payload |
|---|---|
| Print-start | {"opGcodeFile":"printprt:"+<on-device path>, "enableSelfTest":0} (1 = start with self-test / auto-calibration) |
| Delete file | {"opGcodeFile":"deleteprt:"+<path>} |
| Pause / Resume | {"pause":1} / {"pause":0} |
| Stop / cancel | {"stop":1} |
| Nozzle temp | {"nozzleTempControl":<Β°C>} (multi-nozzle: {"setNozzleTemp":{"id":<n>,"temp":<Β°C>}}) |
| Bed temp | {"bedTempControl":{"num":<idx>,"val":<Β°C>}} |
| Chamber temp | {"boxTempControl":<Β°C>} |
| Speed % | {"setFeedratePct":<int>} ({"speedMode":1} = silent) |
| Light | {"lightSw":1\|0} |
| Fan on/off (part/aux/case) | {"fan":1\|0} / {"fanAuxiliary":1\|0} / {"fanCase":1\|0} |
| Fan speed % | raw {"gcodeCmd":"M106 P<ch> S<0-255>"} (P0 model, P1 case, P2 side) |
| Home | {"autohome":"X Y"} then {"autohome":"Z"} |
| Jog axis | {"setPosition":"X<mm> F3000"} (Z uses F600) |
| Error handling | {"errorHandling":0\|1} / {"cleanErr":1} / {"repoPlrStatus":0\|1} (retry / clear / power-loss-recovery) |
| Arbitrary G-code | {"gcodeCmd":"<gcode>"} β a raw escape hatch |
| E-stop | no dedicated verb β {"stop":1} (cancel) or {"gcodeCmd":"M112"} (βͺ inferred) |
β οΈ Two corrections worth internalizing: (1) LAN light is
lightSw, notledβ theledkey is the cloud path (an HTTP POST to a cloud RPC route), not the:9999socket. (2) There is no flow-ratesetkey on the LAN; flow, if settable, goes viagcodeCmd(M221). ThegcodeCmdpassthrough is the highest-risk surface (fan, homing, extrude,M112all reachable) β gate it hardest, and validate a no-op (M105) before ever trustingM112. Contrast a typed emergency-stop verb on documented stacks.
Print launch (two-step): π‘
- Upload β
POST http://<ip>:80/upload/<url-encoded name>(multipart or raw body), no auth,200/201= ok, accepts.gcode/.gcode.3mf. (Legacy machines FTP-push instead; a:81 /server/files/uploadalternate is also present in vendor source β probe on hardware.) Upload β start. - Launch β the client polls device-side file readiness ("under decompression"), then sends the print-start
setframe above over:9999. The<on-device path>is the printer-side location of the just-uploaded file (files live under agcodes/prefix); an absolute-path form sometimes cited is βͺ unconfirmed.
File format. Creality Print v5.0.0+ is an OrcaSlicer fork, so its .gcode / .gcode.3mf carries Prusa/Orca-
style embedded base64 PNG thumbnails (; thumbnail begin / ; thumbnail end) and ; key = value metadata comments β
reuse a Prusa/Orca 3MF parser, not a Cura one (pre-5.0 Creality Print was Cura-based, with different markers). π‘
Multi-material / feeders (CFS)¶
CFS (Creality Filament System) is Creality's AMS-class unit β a bank of slots feeding one hotend, up to 4 units Γ 4
slots = 16. Read via {"method":"get","params":{"boxsInfo":1}}, gated on cfsConnect. It sits on the rich end of
the feeder spectrum: per-slot remaining % and RFID and per-box dry-box telemetry. See
../patterns/multi-material-feeders.md for the neutral model. π‘
Read shape β boxsInfo.materialBoxs[], each unit carrying:
box_typeβ β οΈ three-valued:0= normal 4-slot CFS,1= extra box (variable slots),2= mini / single- slot CFS. Do not hardcode 4 slots β key the slot count offbox_type.cfsName(a per-unit label), and per-boxtemp+humidity(dry-box telemetry).materials[]slots, each:material_id(slot index; letter AβD =material_id % 4),name/type/vendor,color(#RRGGBB),percent(remaining),minTemp/maxTemp,rfid,state(int; meanings uncaptured),selected(active slot).
Normalize color to an uppercase RRGGBB at the boundary; keep the slot key opaque per unit (e.g. a
box:slot token), not a global index; treat rfid as opt-in/internal (privacy). β οΈ Wire spelling varies β reads use
id/type, the vendor desktop names them box_id/box_type; alias both.
Write frames (source-read; gate as experimental until hardware-validated): colorβslot map
{"colorMatch":{"path":<path>,"list":<mapping>}}; feed/retract {"feedInOrOut":{"boxId":<b>,"materialId":<s>,
"isFeed":0|1}}; slot edit {"modifyMaterial":{β¦}}; {"refreshBox":{"boxId":<b>,"materialId":<s>}}; dry-box control
{"dryBox":{β¦}}. The print-time colorβslot map rides the print-start (colorMatchInfo) β see the pattern's
print-time-slot-map section. π‘
Quirks & gotchas¶
- Three protocols, one brand β route by live service, re-fingerprint on reconnect (an OTA can revert root); SE (Marlin/A) vs KE (Klipper/B) are one letter apart and opposite buckets.
- Stock runs Moonraker on loopback behind nginx β route on LAN
:7125reachability, not on "Moonraker exists." - No response correlation β
set/getis fire-and-forget; confirm via the next pushed object, never by an ACK. - Progress is a percent but the OS is Klipper-based β treat it as file-byte position, not time (the classic absurd-ETA bug). Prefer a firmware-reported remaining-time field.
lightSw, notled, for LAN light; no flow-rate key (usegcodeCmd/M221); no dedicated e-stop.gcodeCmdis a raw passthrough β the highest-risk write; gate hardest.- Vendor spellings on the wire β a read field
nozzleMaterailStatusand a download pathhumbnailare as-shipped; match them exactly. wsslicersubprotocol and theheart_beatβ"ok"keepalive are community lore, absent from the vendor frontend β implement defensively, don't depend on them.- Single-source model quirks: a K2-base has been reported to emit a spurious
targetBoxTemp:0; whether the Ender-3 V3 KE accepts a CFS kit is ambiguous β don't advertise a feeder for a KE by default. - Firmware drift β every fact here is "as of mid-2026 firmware"; Creality OTAs frequently and can move fields.
Confidence & validation¶
- π‘ Source-read (first-party): the entire stock
:9999wire β transport, envelope, command set, print-start, the read field-set, thestateenum grouping, theerrdictionary, and the CFS read/write frames β is read from Creality's own committed device-manager frontend (the LAN client shipped inside their open-source slicer). That is the strongest form of source-read (it is the vendor's own client, not third-party RE), but it is shape, not behavior: no stock Creality has been bench-validated here. The paper is source-only overall, hardware-unvalidated β validate live values, and especially every write, before shipping. - π΅ Community only: the
wsslicersubprotocol and theheart_beatβ"ok"keepalive (from independent LAN-client integrations; not in the vendor frontend). - βͺ Inferred: an
M112e-stop viagcodeCmd, the absolute on-device path form, and the CFS slotstate-int meanings. - Bucket C (rooted) is high-confidence by inheritance β it is literally standard Moonraker
:7125(klipper-moonraker.md). Bucket A (Marlin/USB) ismarlin-serial.md.
Open gaps β the capture that closes each (a hardware-owner's highest-value contribution):
- Does a real K1/KE accept the print-start frame, and how does the file-readiness transition sequence? β capture an upload β launch on a stock K1/KE.
- Fine labels for
state6/7/8 β capture thestate/deviceStatestream across idle β print β pause β complete β fail. - Write-echo behavior β issue each
set(light/temp/pause/stop), confirm in the next push; validategcodeCmdwith a no-opM105beforeM112. - CFS slot
state-int meanings + K2-native vs K1-kit parity + whether the KE takes a CFS kit βboxsInfoon a real CFS. - Timing units β confirm whether
printProgressis file-byte, and whether the time fields are truly seconds. - Connection exclusivity β does a second
:9999socket (alongside the vendor app) destabilize the printer? - File-format markers β confirm the Orca/3MF thumbnail markers + metadata keys on a real Creality Print export (hardware-free).
Sources¶
Clean-room, facts-only. The stock :9999 wire is documented from Creality's own open-source slicer (Creality Print,
an OrcaSlicer fork, GPL-3.0): its committed device-manager frontend (the LAN control client) for the WebSocket wire,
print-start, command set, error dictionary, and CFS frames; its send-to-printer code for the HTTP/FTP upload
transport; and its client code for UDP discovery. These are uncopyrightable interface facts (ports, JSON field
names, envelope shape, the error dictionary's structure), described in my own words β no slicer, plugin, or firmware
source was copied. Corroborated by β₯4 independent community LAN-client integrations (licenses range from AGPL-3.0 to
no-license to MIT β read for facts only, never copied) and by Creality's own AGPL cloud plugin, which echoes the same
{"method","params"} field vocabulary from the cloud side. The rooted path is standard Moonraker; the
modelβfirmware boundary is anchored by Creality's own GPL Klipper firmware forks (consumed only as facts proving the
boundary β never vendored). No certificates, keys, tokens, cloud provisioning secrets, or real device identifiers appear
(the LAN path is unauthenticated). Confidence-tagged per ../CONFIDENCE.md; source-read wire β
hardware-validated. Passed ../CLEANROOM-CHECKLIST.md.