Skip to content
On this page

WebSocket API

Contract: bedd.asyncapi.yaml (spec/, AsyncAPI 3.0; Studio).

Connecting

Discovery file %LOCALAPPDATA%\BeDD\run\<pid>.json (root BEDD_HOME), present while listening; skip dead pids. BeDD.exe ps --json lists the same.

json
{"protocol": 2, "kind": "client", "pid": 23116, "url": "ws://127.0.0.1:53021/v1", "token": "9c1f…(64 hex)",
 "instance": "MyInstance-2", "instanceDir": "…\\instances\\MyInstance-2", "label": "smoke test", "modVersion": "0.4.0", "startedAt": "2026-11-13T09:41:07Z"}
Item
kindclient (game) or server (server build)
Portconfig wsPort (default 0: free port per start); read it from the file
Token32 random bytes, new per start. Authorization: Bearer <token> or ?token=<token>; wrong/missing: HTTP 401
Originhandshake with Origin → HTTP 403 unless in config allowOrigins (default empty): browsers cannot connect
Limits127.0.0.1 only; wsMaxClients connections (8); UTF-8 JSON text messages ≤ 16 MiB; no compression

Node.js 24 (its WebSocket sets no headers):

js
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const dir = join(process.env.BEDD_HOME ?? join(process.env.LOCALAPPDATA, 'BeDD'), 'run');
const game = readdirSync(dir).map((f) => JSON.parse(readFileSync(join(dir, f), 'utf8')))
  .find((e) => e.kind === 'client'); // and its pid alive
const ws = new WebSocket(`${game.url}?token=${game.token}`);
ws.onopen = () => ws.send(JSON.stringify({ id: 1, op: 'world' }));
ws.onmessage = (e) => { console.log(JSON.parse(e.data)); ws.close(); };

Requests and replies

json
{"id": 7, "op": "look", "yaw": 90, "pitch": 0}
{"id": 7, "ok": true, "yaw": 90, "pitch": 0, "tick": 5182, "turns": 1}
  • id: any JSON value, echoed. Failure: {id, ok:false, error, code?, key?}; match code (Errors).
  • Each connection is served in order; use more connections for parallel requests.
  • Probe: an op without arguments (read-only for bedboo ops): unknown op = absent, disabled = off, ok = present.

Choosing a mode

bedboo ops first, then ui, then input ops (why). ping lists them:

json
{"ok": true, "protocol": 2, "pid": 23116, "kind": "client", "defaultMode": "bedboo",
 "modes": {"bedboo": ["world","look","input","slot","action","button","settings","send","stack","timescale","join","tap"],
           "ui": ["ui","oreui"], "input": ["key","text","mouse","raw","release","flush","focus","window","screenshot"]},
 "hwnd": 4589122, "managed": true, "client": {"w": 1280, "h": 720}, "cursor": {"x": 640, "y": 360}, "realForeground": false}

bedboo ops

Config key false → disabled. Implementation.

opkeyprobe replyaction fields (default)
worldgameHooksinLevel, levelEpoch (+1 per world), topScreen, mouseGrabbed, showingMenu, inputMode mouse|touch|gamepad|undefined, player{runtimeId, eye{x,y,z}, rot{yaw,pitch}, slot} (null: no world), ticknone
lookgameHooksinLevel, yaw, pitch, tickyaw, pitch, timeoutMs (1000*, ≤60000)
inputgameHooksactive, via hooks|keys, hooks{raw,state}, tick, applied, ticksLeftmove{x,z}, jump, sneak, sprint, ttlMs (300) or ttlTicks
slotgameHooksslot (null: no world)slot 0..8, timeoutMs (1000*)
actiongameHooksnamesname drop|dropAll|inventory|perspective|pause|swing; hand main|off (swing); timeoutMs (1000*)
buttongameHooksheldbutton attack|use|pick; action down|up|click (click); holdTicks (click: 2); releaseOn blockChange; timeoutMs (1000*)
settingsgameHooksrenderDistance (chunks; null before the first world), inLevelrenderDistance (chunks, 1..255)
sendallowSendallow (sendAllow ids; "*" spelled out), allowAllbuf, timeoutMs (2000*, ≤10000)
stackallowStackenabled, via:"raw", stats{sent, answered, rejected, failed, resyncs}buf, timeoutMs (2000*); or view:true
taptapenabled, attached, link{state up|down, type remote|local, address}, journal{frames, bytes, complete}, lastReplay, replay, counters{in, out, dropped, sent, readers}, hooksaction:"subscribe" → tap stream
timescalenonebelowscale, paused, timeoutMs (2000)
joinnonemode, connection none|local|remote, host, port, inLevel, joining, last, allowHosts (reachable off the PC; "*" without loopbackOnly), anyHosthost (null: leave the world), port (19132), wait, timeoutMs (60000)

* At ≥20 ticks/s; slower, defaults scale by tick interval / 50 ms (up to the max). not_in_level: no client tick within 20 tick intervals (≥1 s).

opBehaviourErrors
lookabsolute; replies yaw, pitch, tick, turns once both within 0.5°. Yaw 0 = +Z, 90 = -X; pitch -90 = up, clamped ±89.9not_in_level, timeout (with current yaw/pitch), busy (newer look), bad_args
inputmove: x = left-right, z = forward-back, clamped [-1,1], diagonal normalized. Ends ttlMs (≤60000, longer cut) after the request (refresh to keep moving), on ttlMs:0 or release. ttlTicks (≤1200) counts client ticks, also ends after 5 s without ticks. Outlives its connection. via:"hooks": only with the mouse grabbed and no screen; applied = ticks written. via:"keys" (hook missing): W/A/S/D, SPACE, LSHIFT, LCTRL held like key (reach screens, break on rebinding)bad_args (ttlTicks with ttlMs), not_in_level (ttlTicks, not ticking)
slot, action, buttonanswered after the next client tick applied them (tick). A paused singleplayer world does not tick: resume with key ESCnot_in_level, busy (screen open; button up still works), timeout, bad_args, refused
slothotbar key 1..9; replies once held (slot, via keys|none)
actionkey released next tick: drop Q, dropAll Ctrl+Q, inventory E, pause Escape (default keymap); perspective: game call. swing: LocalPlayer::swing, arm swing with no attack, the client sends animate; works with a screen open; hand:"off" shows on this client only. Replies name, hand
buttonvirtual GameInput left/right/middle; released after holdTicks, or (releaseOn:"blockChange") when the block targeted on down changes, turns to air or is untargeted. up at once. Never blocks. Replies held, block{x,y,z}. On disconnect only an endless down is released; others finish, releaseOn at most 1200 more ticksrefused: no block targeted
settingsrenderDistance sets Render Distance of every renderer (normal, Vibrant Visuals, ray tracing; not saved to options.txt) next client tick, then ClientInstance::updateChunkRadius: the client sends request_chunk_radius; the server's grant arrives as chunk_radius_update (tap). Replies renderDistance (rendered now), requested, ticknot_in_level, timeout, bad_args, unavailable
sendbuf: base64 serverbound packet (varint header + body, bedrock-protocol), sent by the local player; replies packetId, name. Stats: state.stats.send{sent, refused, failed, lastError}bad_args (unreadable, not sent), refused (not in sendAllow)
joinfrom title or a world (left first: left); answers "Online play is not rated" (Proceed) and "Trust this server?" (Trust and Join). started:true once connecting; wait:true → joined:true. Off-PC hosts need allowHosts (Remote mode); with allowHosts "*" the host is resolved and added (not_allowed if it does not resolve). host: null leaves the world (Save & Quit / Disconnect): left:true (wait: once saved and back in the menus; else started:true), left:false in no worldjoin_failed, timeout (screen), busy, not_allowed, stopped, no_client, unavailable, bad_args

stack: item stack requests through the client

FormReply
{op:"stack", buf}{via:"raw", requestIds, map:{"<caller id>": client id}} once sent
{op:"stack", view:true}{inventory, ui, open?:{containerId, type, items}}, entries {slot, item, count, stackId}
  • buf: one item_stack_request packet (id 147); other ids or unreadable bytes: bad_args.
  • Each request gets a client id: odd, negative, down from -0x40000001. Stack ids naming caller ids are renamed (own and the last 256 earlier). map keys: caller ids as strings.
  • The item_stack_response (tap) may precede the reply: hold it.
  • No client prediction. Accepted request: the client sends inventory_transaction {type: inventory_mismatch}, the server resends the inventory. Rejected: nothing changes.
  • The needed screen (inventory, chest) must be open: action inventory, key E, or use the block.

timescale: game speed

Speed of a world this game hosts (singleplayer, LAN host): client and local server together. A request (scale, paused) replies the probe once both sides run at it; probe also has local, inLevel.

Field
scale0.05..20, clamped (1 = 20 ticks/s)
pausedstops/resumes both sides, like the pause menu
sourcedefault, config (timeScale, on entering a world), local (this API), server (a BeDD server)
tps{client, server}ticks/s over the last 2 s; 0 paused; server null on a remote server. Measured local: 0.05→1, 0.5→10, 2→40, 3→60, 5→100, 10→200, 20→400
ticksPerSecondclient timer base rate (20)
effective{client, server}{scale, paused, ticksPerSecond} last applied per side (null before)

Leaving a world resets to 1; entering applies timeScale (1). Errors: not_in_level, remote (not hosted here), server_led (a BeDD server sets it: change it there), timeout (still applies later), refused, bad_args. A BeDD server sends level_event 3007 (SimTimeScale, pos 0,0,0, data = float bits as int32, 0 = paused) on change and on join; the client follows (0 pauses, keeping the scale).

Who sends what (bedboo transport bedd)

The client sends every serverbound packet. bedboo's lib/bedd/authority.js drops client-owned packets, maps others to ops (input, look, slot, button, action, settings, stack, join, key ESC), sends text and command_request with send (fallback T/SLASH, text, ENTER), works Leave Bed and the sign editor through ui, sends other sendAllow ids as is with send, and throws ERR_BEDD_UNSUPPORTED (BeddUnsupportedError) for the rest.

ui ops

ui reads the screens as text with refs and acts on a ref; oreui returns the raw Ore UI DOM; mouse takes a ref. Config key ui covers all three. Fields, replies and errors: UI tree.

input ops

No config key. Reply after the game thread dispatched the input (timeoutMs 5000).

opfields (default)
keykey or vk (1..254); action press|down|up (press); holdMs (60)letters, digits, ESC, ENTER, BACK, TAB, SPACE, SHIFT, CTRL, ALT, LEFT/UP/RIGHT/DOWN, HOME, END, PGUP, PGDN, INSERT, DELETE, F1..F24, SLASH, GRAVE, MINUS, EQUALS
texttext, delayMs (0)one WM_CHAR per UTF-16 unit
mouseaction move|down|up|click|wheel (move); x,y (client px) or rx,ry (fractions) or ref; button left|right|middle (left); notches (1); holdMs (80, ≥50); settleMs (50, hover first); afterMs (50)moves the virtual cursor, then GameInput buttons/wheel
rawas mouse, with dx,dy (counts), steps (1, spread over frames)camera while the mouse is grabbed
releasereleases all held keys/buttons (anyone's) and the input override
focusre-sends "focus gained"
windowclientWidth, clientHeightresizes; replies result + window fields; never activates
flushwaits until all posted input is dispatched
screenshotformat png|jpeg, quality, maxWidth (960), region{x,y,w,h}{width, height, format, data} (base64)

System ops

opreply
pingexample; hwnd 0 before the window, managed = manager attached, audio = sound on, grabbed = the game holds the mouse
statewindow fields, stats{synthetic, swallowedInput, swallowedFocus, passed, realForegroundSeen, api, gameInput, loopbackRebinds, serverQueriesRefused, audio, tapLoopback, send, journal, sendsRefused}, clients, held[{vk, client}], hooks{name: installed}, mode ClientOnly|ClientServer|Remote, connection{connection none|local|remote, inLevel, host, port}
manageraction:"attach": this connection is the manager; window kept off screen, taskbar, Alt+Tab. Config manager (true): game closes if none attaches within 30 s of its window or the attached one drops; false: hidden, never self-closes. Label: env BEDD_LABEL. Reply has audio. Attached manager only: action:"audio", on true|false (the game's sound on this PC; off again when the manager drops); action:"input", events[] of {t:"key", vk, down}, {t:"char", c}, {t:"move", x, y}, {t:"raw", dx, dy}, {t:"button", button, down} (left|right|middle|x1|x2), {t:"wheel", notches}, {t:"release"}: the user's input while they control the game in the manager window, nothing waits; replies {grabbed, audio}
close{ok}, then the game closes

Errors

codemeaning
disabledconfig key false (key), or unloading
not_in_levelnot in a world (server: not started)
refusedthe game refused (id not in sendAllow, no block for releaseOn)
timeoutnot done in timeoutMs (may still)
bad_argsinvalid field values or unreadable bytes
bad_requestthe op cannot act on it: binary message; tap/manager unknown action or no connection; ui action without ref, unknown action, setText without text
internala ui action's sub-op reply was unreadable (a bug)
busyscreen open, newer look, join running, tap taken
stoppedunloading
join_failedjoin gave up (the client shows why)
not_allowedoff-PC host, loopbackOnly, not in allowHosts (or, with "*", does not resolve)
unavailablea needed hook did not install (game log)
no_clientno client instance yet
remoteworld on a server not hosted here
server_leda BeDD server leads the speed
stale_ref, occluded, not_ready, unsupportedUI tree errors

No code: unknown op, not a JSON object, input not dispatched in time, invalid names (key, button).

Several clients

Window-input ops are serialized across connections; ping, state, manager, close, tap, world, look, input, settings, send, stack, timescale, join, screenshot, oreui and ui reads are not. On disconnect:

Held
key/mouse downreleased (unless re-pressed by another connection)
buttonsee above
input overrideruns to its deadline
managergame closes

Per-connection state in the mod: code rules.

Security

Anyone who can read the discovery file (the user's processes) can drive the game. Token compared in constant time.

  • send: packets as the signed-in player, within sendAllow (default 9 text, 77 command_request, 78 command_block_update, 97 book_edit). Ids 1, 30, 144, 147 are always refused. Widen only for your own server.
  • sendAllow: "*": every id but the ones the client sends itself: 1 login, 4 handshake, 8 resource pack response, 23 tick_sync, 30, 44 animate, 45 respawn, 75 show_credits, 93 player_skin, 94 subclient_login, 113 set_local_player_as_initialized, 115 network_stack_latency, 129/135 client cache, 144, 147, 152 emote_list, 175 subchunk_request, 193 request_network_settings, 312 serverbound_loading_screen. Others may desync the client from the server (it did not make them).
  • stack: moves, crafts, drops and destroys the player's items on any server.
  • tap: the whole session.

Turn off unneeded allowSend, allowStack, tap; never share a token.

Server target

--target_type=server (Building) runs in BDS with LeviLamina: same WebSocket, kind:"server", other ops unknown op.

opfields (default)reply
pingprotocol, pid, kind, target:"server"
statetarget, clients, loopbackOnly, stats{loopbackRebinds, serverQueriesRefused}
servertick, tps (2 s), paused, port (server-port), players[{name, xuid}]
timescalescale 0.05..20, paused, timeoutMs (5000); none = probescale, paused, source default|config|local, tps{client:null, server}, ticksPerSecond (20 × scale, 0 paused)
commandcommand (/ optional), timeoutMs (10000)success, successCount, output[], as the console

It broadcasts level_event 3007 (above) on change and to each spawned player. tps.server may trail ticksPerSecond. Errors: not_in_level, bad_args, timeout.