35 KiB
OpenClaw Gateway WebSocket Protocol
This document describes how to interact with the OpenClaw Gateway over WebSocket. It is intended for humans and LLMs and includes frame formats, auth/scopes, events, and all known methods with params and response payloads.
Protocol version: 3
Default URL: ws://127.0.0.1:18789
All timestamps are milliseconds since Unix epoch unless noted.
Connection Lifecycle
- Open a WebSocket connection to the gateway.
- Server immediately sends an
eventframe namedconnect.challengewith a nonce. - Client must send a
reqframe withmethod: "connect"andparams: ConnectParamsas the first request. - Server responds with a
resframe whosepayloadis aHelloOkobject.
If connect is not the first request, the server returns an error.
Connect Challenge
Event payload:
type ConnectChallenge = { nonce: string; ts: number };
If you provide device.nonce in connect, it must match this nonce.
Frame Formats
type RequestFrame = {
type: "req";
id: string; // client-generated
method: string;
params?: unknown;
};
type ResponseFrame = {
type: "res";
id: string; // matches RequestFrame.id
ok: boolean;
payload?: unknown;
error?: ErrorShape;
};
type EventFrame = {
type: "event";
event: string;
payload?: unknown;
seq?: number; // optional event sequence counter
stateVersion?: { presence: number; health: number };
};
Error Shape
type ErrorShape = {
code: string;
message: string;
details?: unknown;
retryable?: boolean;
retryAfterMs?: number;
};
Known error codes include:
NOT_LINKED, NOT_PAIRED, AGENT_TIMEOUT, INVALID_REQUEST, UNAVAILABLE.
Connect Params and HelloOk
type ConnectParams = {
minProtocol: number;
maxProtocol: number;
client: {
id: "webchat-ui" | "openclaw-control-ui" | "webchat" | "cli" | "gateway-client" | "openclaw-macos" | "openclaw-ios" | "openclaw-android" | "node-host" | "test" | "fingerprint" | "openclaw-probe";
displayName?: string;
version: string;
platform: string;
deviceFamily?: string;
modelIdentifier?: string;
mode: "webchat" | "cli" | "ui" | "backend" | "node" | "probe" | "test";
instanceId?: string;
};
caps?: string[];
commands?: string[];
permissions?: Record<string, boolean>;
pathEnv?: string;
role?: string; // default "operator"
scopes?: string[];
device?: {
id: string;
publicKey: string;
signature: string;
signedAt: number;
nonce?: string;
};
auth?: {
token?: string;
password?: string;
};
locale?: string;
userAgent?: string;
};
Notes:
minProtocol/maxProtocolmust include3(the server's expected protocol).- If you send
device.nonce, it must match theconnect.challengenonce.
type HelloOk = {
type: "hello-ok";
protocol: number;
server: {
version: string;
commit?: string;
host?: string;
connId: string;
};
features: {
methods: string[]; // advertised methods
events: string[]; // advertised events
};
snapshot: Snapshot;
canvasHostUrl?: string;
auth?: {
deviceToken: string;
role: string;
scopes: string[];
issuedAtMs?: number;
};
policy: {
maxPayload: number;
maxBufferedBytes: number;
tickIntervalMs: number;
};
};
Auth, Roles, and Scopes
Gateway methods are authorized by role + scopes set during connect.
Roles:
operator (default) and node.
Scopes:
operator.read, operator.write, operator.admin, operator.pairing, operator.approvals.
Notes:
noderole can only callnode.invoke.result,node.event,skills.bins.operator.adminis required for config, wizard, update, and several maintenance methods.- If a method is not explicitly read/write/pairing/approvals, it generally requires
operator.admin.
Idempotency
The following methods require idempotencyKey in params and dedupe repeated requests:
send, poll, agent, chat.send, node.invoke.
For send, poll, agent, and chat.send the idempotencyKey is used as the runId in responses/events.
Common Types
type Snapshot = {
presence: PresenceEntry[];
health: HealthSummary;
stateVersion: { presence: number; health: number };
uptimeMs: number;
configPath?: string;
stateDir?: string;
sessionDefaults?: {
defaultAgentId: string;
mainKey: string;
mainSessionKey: string;
scope?: string;
};
};
type PresenceEntry = {
host?: string;
ip?: string;
version?: string;
platform?: string;
deviceFamily?: string;
modelIdentifier?: string;
mode?: string;
lastInputSeconds?: number;
reason?: string;
tags?: string[];
text?: string;
ts: number;
deviceId?: string;
roles?: string[];
scopes?: string[];
instanceId?: string;
};
Health summary (used by health method and health event):
type HealthSummary = {
ok: true;
ts: number;
durationMs: number;
channels: Record<string, ChannelHealthSummary>;
channelOrder: string[];
channelLabels: Record<string, string>;
heartbeatSeconds: number;
defaultAgentId: string;
agents: AgentHealthSummary[];
sessions: {
path: string;
count: number;
recent: Array<{ key: string; updatedAt: number | null; age: number | null }>;
};
};
Status summary (used by status method):
type StatusSummary = {
linkChannel?: { id: string; label: string; linked: boolean; authAgeMs: number | null };
heartbeat: { defaultAgentId: string; agents: HeartbeatStatus[] };
channelSummary: string[];
queuedSystemEvents: string[];
sessions: {
paths: string[];
count: number;
defaults: { model: string | null; contextTokens: number | null };
recent: SessionStatus[];
byAgent: Array<{ agentId: string; path: string; count: number; recent: SessionStatus[] }>;
};
};
Usage summaries:
type UsageSummary = {
updatedAt: number;
providers: Array<{
provider: string;
displayName: string;
windows: Array<{ label: string; usedPercent: number; resetAt?: number }>;
plan?: string;
error?: string;
}>;
};
type CostUsageSummary = {
updatedAt: number;
days: number;
daily: Array<{ date: string; input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens: number; totalCost: number; missingCostEntries: number }>;
totals: { input: number; output: number; cacheRead: number; cacheWrite: number; totalTokens: number; totalCost: number; missingCostEntries: number };
};
Heartbeat event payload (used by heartbeat event and last-heartbeat method):
type HeartbeatEventPayload = {
ts: number;
status: "sent" | "ok-empty" | "ok-token" | "skipped" | "failed";
to?: string;
preview?: string;
durationMs?: number;
hasMedia?: boolean;
reason?: string;
channel?: string;
silent?: boolean;
indicatorType?: "ok" | "alert" | "error";
};
Chat and agent events:
type AgentEvent = {
runId: string;
seq: number;
stream: string;
ts: number;
data: Record<string, unknown>;
};
type ChatEvent = {
runId: string;
sessionKey: string;
seq: number;
state: "delta" | "final" | "aborted" | "error";
message?: unknown;
errorMessage?: string;
usage?: unknown;
stopReason?: string;
};
Cron types:
type CronSchedule =
| { kind: "at"; at: string }
| { kind: "every"; everyMs: number; anchorMs?: number }
| { kind: "cron"; expr: string; tz?: string };
type CronPayload =
| { kind: "systemEvent"; text: string }
| { kind: "agentTurn"; message: string; model?: string; thinking?: string; timeoutSeconds?: number };
type CronDelivery = { mode: "none" | "announce"; channel?: "last" | string; to?: string; bestEffort?: boolean };
type CronJob = {
id: string;
agentId?: string;
name: string;
description?: string;
enabled: boolean;
deleteAfterRun?: boolean;
createdAtMs: number;
updatedAtMs: number;
schedule: CronSchedule;
sessionTarget: "main" | "isolated";
wakeMode: "next-heartbeat" | "now";
payload: CronPayload;
delivery?: CronDelivery;
state: {
nextRunAtMs?: number;
runningAtMs?: number;
lastRunAtMs?: number;
lastStatus?: "ok" | "error" | "skipped";
lastError?: string;
lastDurationMs?: number;
};
};
type CronEvent = {
jobId: string;
action: "added" | "updated" | "removed" | "started" | "finished";
runAtMs?: number;
durationMs?: number;
status?: "ok" | "error" | "skipped";
error?: string;
summary?: string;
nextRunAtMs?: number;
};
Node and device pairing types:
type NodePairingPendingRequest = {
requestId: string;
nodeId: string;
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
deviceFamily?: string;
modelIdentifier?: string;
caps?: string[];
commands?: string[];
permissions?: Record<string, boolean>;
remoteIp?: string;
silent?: boolean;
isRepair?: boolean;
ts: number;
};
type NodePairingPairedNode = {
nodeId: string;
token: string;
displayName?: string;
platform?: string;
version?: string;
coreVersion?: string;
uiVersion?: string;
deviceFamily?: string;
modelIdentifier?: string;
caps?: string[];
commands?: string[];
bins?: string[];
permissions?: Record<string, boolean>;
remoteIp?: string;
createdAtMs: number;
approvedAtMs: number;
lastConnectedAtMs?: number;
};
type DevicePairingPendingRequest = {
requestId: string;
deviceId: string;
publicKey: string;
displayName?: string;
platform?: string;
clientId?: string;
clientMode?: string;
role?: string;
roles?: string[];
scopes?: string[];
remoteIp?: string;
silent?: boolean;
isRepair?: boolean;
ts: number;
};
type PairedDevice = {
deviceId: string;
publicKey: string;
displayName?: string;
platform?: string;
clientId?: string;
clientMode?: string;
role?: string;
roles?: string[];
scopes?: string[];
remoteIp?: string;
tokens?: Record<string, { role: string; scopes: string[]; createdAtMs: number; rotatedAtMs?: number; revokedAtMs?: number; lastUsedAtMs?: number }>;
createdAtMs: number;
approvedAtMs: number;
};
Sessions list/preview results:
type SessionsListResult = {
ts: number;
path: string;
count: number;
defaults: { modelProvider: string | null; model: string | null; contextTokens: number | null };
sessions: Array<{
key: string;
kind: "direct" | "group" | "global" | "unknown";
label?: string;
displayName?: string;
derivedTitle?: string;
lastMessagePreview?: string;
channel?: string;
subject?: string;
groupChannel?: string;
space?: string;
chatType?: string;
origin?: unknown;
updatedAt: number | null;
sessionId?: string;
systemSent?: boolean;
abortedLastRun?: boolean;
thinkingLevel?: string;
verboseLevel?: string;
reasoningLevel?: string;
elevatedLevel?: string;
sendPolicy?: "allow" | "deny";
inputTokens?: number;
outputTokens?: number;
totalTokens?: number;
responseUsage?: "on" | "off" | "tokens" | "full";
modelProvider?: string;
model?: string;
contextTokens?: number;
deliveryContext?: unknown;
lastChannel?: string;
lastTo?: string;
lastAccountId?: string;
}>;
};
type SessionsPreviewResult = {
ts: number;
previews: Array<{
key: string;
status: "ok" | "empty" | "missing" | "error";
items: Array<{ role: "user" | "assistant" | "tool" | "system" | "other"; text: string }>;
}>;
};
Events
The gateway may emit these events. Payloads use the types above.
connect.challenge:ConnectChallengeagent:AgentEventchat:ChatEventpresence:{ presence: PresenceEntry[] }tick:{ ts: number }talk.mode:{ enabled: boolean; phase: string | null; ts: number }shutdown:{ reason: string; restartExpectedMs?: number }health:HealthSummaryheartbeat:HeartbeatEventPayloadcron:CronEventnode.pair.requested:NodePairingPendingRequestnode.pair.resolved:{ requestId: string; nodeId: string; decision: "approved" | "rejected"; ts: number }node.invoke.request:{ id: string; nodeId: string; command: string; paramsJSON?: string; timeoutMs?: number; idempotencyKey?: string }device.pair.requested:DevicePairingPendingRequestdevice.pair.resolved:{ requestId: string; deviceId: string; decision: string; ts: number }voicewake.changed:{ triggers: string[] }exec.approval.requested:{ id: string; request: { command: string; cwd?: string | null; host?: string | null; security?: string | null; ask?: string | null; agentId?: string | null; resolvedPath?: string | null; sessionKey?: string | null }; createdAtMs: number; expiresAtMs: number }exec.approval.resolved:{ id: string; decision: "allow-once" | "allow-always" | "deny"; resolvedBy?: string; ts: number }
Method Reference
Health and Status
health
Params:
{ probe?: boolean }
Response:
HealthSummary
Notes: Uses cached health unless probe: true.
status
Params:
{}
Response:
StatusSummary
usage.status
Params:
{}
Response:
UsageSummary
usage.cost
Params:
{ days?: number }
Response:
CostUsageSummary
last-heartbeat
Params:
{}
Response:
HeartbeatEventPayload | null
set-heartbeats
Params:
{ enabled: boolean }
Response:
{ ok: true; enabled: boolean }
Scope: operator.admin
system-presence
Params:
{}
Response:
PresenceEntry[]
system-event
Params:
{
text: string;
deviceId?: string;
instanceId?: string;
host?: string;
ip?: string;
mode?: string;
version?: string;
platform?: string;
deviceFamily?: string;
modelIdentifier?: string;
lastInputSeconds?: number;
reason?: string;
roles?: string[];
scopes?: string[];
tags?: string[];
}
Response:
{ ok: true }
Scope: operator.admin
Logs
logs.tail
Params:
{ cursor?: number; limit?: number; maxBytes?: number }
Response:
{ file: string; cursor: number; size: number; lines: string[]; truncated?: boolean; reset?: boolean }
Channels
channels.status
Params:
{ probe?: boolean; timeoutMs?: number }
Response:
{
ts: number;
channelOrder: string[];
channelLabels: Record<string, string>;
channelDetailLabels?: Record<string, string>;
channelSystemImages?: Record<string, string>;
channelMeta?: Array<{ id: string; label: string; detailLabel: string; systemImage?: string }>;
channels: Record<string, unknown>; // plugin summaries
channelAccounts: Record<string, Array<{
accountId: string;
name?: string;
enabled?: boolean;
configured?: boolean;
linked?: boolean;
running?: boolean;
connected?: boolean;
reconnectAttempts?: number;
lastConnectedAt?: number;
lastError?: string;
lastStartAt?: number;
lastStopAt?: number;
lastInboundAt?: number;
lastOutboundAt?: number;
lastProbeAt?: number;
mode?: string;
dmPolicy?: string;
allowFrom?: string[];
tokenSource?: string;
botTokenSource?: string;
appTokenSource?: string;
baseUrl?: string;
allowUnmentionedGroups?: boolean;
cliPath?: string | null;
dbPath?: string | null;
port?: number | null;
probe?: unknown;
audit?: unknown;
application?: unknown;
[key: string]: unknown;
}>>;
channelDefaultAccountId: Record<string, string>;
}
channels.logout
Params:
{ channel: string; accountId?: string }
Response:
{ channel: string; accountId: string; cleared: boolean; [key: string]: unknown }
Scope: operator.admin
web.login.start (plugin-provided)
Params:
{ force?: boolean; timeoutMs?: number; verbose?: boolean; accountId?: string }
Response: provider-specific, typically includes QR or login URL.
web.login.wait (plugin-provided)
Params:
{ timeoutMs?: number; accountId?: string }
Response: provider-specific, typically { connected: boolean; ... }.
TTS
tts.status
Params:
{}
Response:
{
enabled: boolean;
auto: boolean | string;
provider: "openai" | "elevenlabs" | "edge";
fallbackProvider: string | null;
fallbackProviders: string[];
prefsPath: string;
hasOpenAIKey: boolean;
hasElevenLabsKey: boolean;
edgeEnabled: boolean;
}
tts.providers
Params:
{}
Response:
{ providers: Array<{ id: string; name: string; configured: boolean; models: string[]; voices?: string[] }>; active: string }
tts.enable
Params:
{}
Response:
{ enabled: true }
tts.disable
Params:
{}
Response:
{ enabled: false }
tts.convert
Params:
{ text: string; channel?: string }
Response:
{ audioPath: string; provider: string; outputFormat: string; voiceCompatible: boolean }
tts.setProvider
Params:
{ provider: "openai" | "elevenlabs" | "edge" }
Response:
{ provider: string }
Config and Update
config.get
Params:
{}
Response:
{ path: string; exists: boolean; raw: string | null; parsed: unknown; valid: boolean; config: unknown; hash?: string; issues: Array<{ path: string; message: string }>; warnings: Array<{ path: string; message: string }>; legacyIssues: Array<{ path: string; message: string }> }
config.schema
Params:
{}
Response:
{ schema: unknown; uiHints: Record<string, { label?: string; help?: string; group?: string; order?: number; advanced?: boolean; sensitive?: boolean; placeholder?: string; itemTemplate?: unknown }>; version: string; generatedAt: string }
config.set
Params:
{ raw: string; baseHash?: string }
Response:
{ ok: true; path: string; config: unknown }
Notes: baseHash is required if a config already exists.
config.patch
Params:
{ raw: string; baseHash?: string; sessionKey?: string; note?: string; restartDelayMs?: number }
Response:
{ ok: true; path: string; config: unknown; restart: unknown; sentinel: { path: string | null; payload: unknown } }
Notes: raw must be a JSON object for merge patch. Requires baseHash if config exists.
config.apply
Params:
{ raw: string; baseHash?: string; sessionKey?: string; note?: string; restartDelayMs?: number }
Response:
{ ok: true; path: string; config: unknown; restart: unknown; sentinel: { path: string | null; payload: unknown } }
Notes: Requires baseHash if config exists.
update.run
Params:
{ sessionKey?: string; note?: string; restartDelayMs?: number; timeoutMs?: number }
Response:
{ ok: true; result: { status: "ok" | "error"; mode: string; reason?: string; root?: string; before?: string | null; after?: string | null; steps: Array<{ name: string; command: string; cwd: string; durationMs: number; stdoutTail?: string | null; stderrTail?: string | null; exitCode?: number | null }>; durationMs: number }; restart: unknown; sentinel: { path: string | null; payload: unknown } }
Scope: operator.admin
Exec Approvals
exec.approvals.get
Params:
{}
Response:
{ path: string; exists: boolean; hash: string; file: { version: 1; socket?: { path?: string }; defaults?: { security?: string; ask?: string; askFallback?: string; autoAllowSkills?: boolean }; agents?: Record<string, { security?: string; ask?: string; askFallback?: string; autoAllowSkills?: boolean; allowlist?: Array<{ id?: string; pattern: string; lastUsedAt?: number; lastUsedCommand?: string; lastResolvedPath?: string }> }> } }
exec.approvals.set
Params:
{ file: ExecApprovalsFile; baseHash?: string }
Response:
Same shape as exec.approvals.get.
Notes: baseHash required if file exists.
exec.approvals.node.get
Params:
{ nodeId: string }
Response: Node-provided exec approvals snapshot for that node.
exec.approvals.node.set
Params:
{ nodeId: string; file: ExecApprovalsFile; baseHash?: string }
Response: Node-provided exec approvals snapshot after update.
exec.approval.request
Params:
{ id?: string; command: string; cwd?: string | null; host?: string | null; security?: string | null; ask?: string | null; agentId?: string | null; resolvedPath?: string | null; sessionKey?: string | null; timeoutMs?: number }
Response:
{ id: string; decision: "allow-once" | "allow-always" | "deny"; createdAtMs: number; expiresAtMs: number }
Notes: This method blocks until a decision is made or timeout occurs.
exec.approval.resolve
Params:
{ id: string; decision: "allow-once" | "allow-always" | "deny" }
Response:
{ ok: true }
Wizard
wizard.start
Params:
{ mode?: "local" | "remote"; workspace?: string }
Response:
{ sessionId: string; done: boolean; step?: WizardStep; status?: "running" | "done" | "cancelled" | "error"; error?: string }
wizard.next
Params:
{ sessionId: string; answer?: { stepId: string; value?: unknown } }
Response:
{ done: boolean; step?: WizardStep; status?: "running" | "done" | "cancelled" | "error"; error?: string }
wizard.cancel
Params:
{ sessionId: string }
Response:
{ status: "running" | "done" | "cancelled" | "error"; error?: string }
wizard.status
Params:
{ sessionId: string }
Response:
{ status: "running" | "done" | "cancelled" | "error"; error?: string }
WizardStep:
{ id: string; type: "note" | "select" | "text" | "confirm" | "multiselect" | "progress" | "action"; title?: string; message?: string; options?: Array<{ value: unknown; label: string; hint?: string }>; initialValue?: unknown; placeholder?: string; sensitive?: boolean; executor?: "gateway" | "client" }
Talk
talk.mode
Params:
{ enabled: boolean; phase?: string }
Response:
{ enabled: boolean; phase: string | null; ts: number }
Notes: For webchat clients, requires a connected mobile node.
Models
models.list
Params:
{}
Response:
{ models: Array<{ id: string; name: string; provider: string; contextWindow?: number; reasoning?: boolean }> }
Agents
agents.list
Params:
{}
Response:
{ defaultId: string; mainKey: string; scope: "per-sender" | "global"; agents: Array<{ id: string; name?: string; identity?: { name?: string; theme?: string; emoji?: string; avatar?: string; avatarUrl?: string } }> }
agents.files.list
Params:
{ agentId: string }
Response:
{ agentId: string; workspace: string; files: Array<{ name: string; path: string; missing: boolean; size?: number; updatedAtMs?: number; content?: string }> }
agents.files.get
Params:
{ agentId: string; name: string }
Response:
{ agentId: string; workspace: string; file: { name: string; path: string; missing: boolean; size?: number; updatedAtMs?: number; content?: string } }
agents.files.set
Params:
{ agentId: string; name: string; content: string }
Response:
{ ok: true; agentId: string; workspace: string; file: { name: string; path: string; missing: boolean; size?: number; updatedAtMs?: number; content?: string } }
agent
Params:
{
message: string;
agentId?: string;
to?: string;
replyTo?: string;
sessionId?: string;
sessionKey?: string;
thinking?: string;
deliver?: boolean;
attachments?: Array<{ type?: string; mimeType?: string; fileName?: string; content?: unknown }>;
channel?: string;
replyChannel?: string;
accountId?: string;
replyAccountId?: string;
threadId?: string;
groupId?: string;
groupChannel?: string;
groupSpace?: string;
timeout?: number;
lane?: string;
extraSystemPrompt?: string;
idempotencyKey: string;
label?: string;
spawnedBy?: string;
}
Response:
{ runId: string; status: "accepted"; acceptedAt: number }
Final response (same request id, later):
{ runId: string; status: "ok" | "error"; summary: string; result?: unknown }
Notes: The gateway may send multiple res frames for the same id.
agent.identity.get
Params:
{ agentId?: string; sessionKey?: string }
Response:
{ agentId: string; name?: string; avatar?: string; emoji?: string }
agent.wait
Params:
{ runId: string; timeoutMs?: number }
Response:
{ runId: string; status: "ok" | "error" | "timeout"; startedAt?: number; endedAt?: number; error?: string }
Skills
skills.status
Params:
{ agentId?: string }
Response:
{ workspaceDir: string; managedSkillsDir: string; skills: Array<{ name: string; description?: string; source?: string; bundled?: boolean; filePath?: string; baseDir?: string; skillKey?: string; emoji?: string; homepage?: string; always?: boolean; disabled?: boolean; blockedByAllowlist?: boolean; eligible?: boolean; requirements?: { bins?: string[]; anyBins?: string[]; env?: string[]; config?: string[]; os?: string[] }; missing?: { bins?: string[]; anyBins?: string[]; env?: string[]; config?: string[]; os?: string[] }; configChecks?: unknown[]; install?: unknown[] }> }
skills.bins
Params:
{}
Response:
{ bins: string[] }
skills.install
Params:
{ name: string; installId: string; timeoutMs?: number }
Response:
{ ok: boolean; message?: string; [key: string]: unknown }
Notes: Install details are returned from the skill installer.
skills.update
Params:
{ skillKey: string; enabled?: boolean; apiKey?: string; env?: Record<string, string> }
Response:
{ ok: true; skillKey: string; config: { enabled?: boolean; apiKey?: string; env?: Record<string, string> } }
Voice Wake
voicewake.get
Params:
{}
Response:
{ triggers: string[] }
voicewake.set
Params:
{ triggers: string[] }
Response:
{ triggers: string[] }
Also emits voicewake.changed event.
Sessions
sessions.list
Params:
{ limit?: number; activeMinutes?: number; includeGlobal?: boolean; includeUnknown?: boolean; includeDerivedTitles?: boolean; includeLastMessage?: boolean; label?: string; spawnedBy?: string; agentId?: string; search?: string }
Response:
SessionsListResult
sessions.preview
Params:
{ keys: string[]; limit?: number; maxChars?: number }
Response:
SessionsPreviewResult
sessions.resolve
Params:
{ key?: string; sessionId?: string; label?: string; agentId?: string; spawnedBy?: string; includeGlobal?: boolean; includeUnknown?: boolean }
Response:
{ ok: true; key: string }
sessions.patch
Params:
{ key: string; label?: string | null; thinkingLevel?: string | null; verboseLevel?: string | null; reasoningLevel?: string | null; responseUsage?: "off" | "tokens" | "full" | "on" | null; elevatedLevel?: string | null; execHost?: string | null; execSecurity?: string | null; execAsk?: string | null; execNode?: string | null; model?: string | null; spawnedBy?: string | null; sendPolicy?: "allow" | "deny" | null; groupActivation?: "mention" | "always" | null }
Response:
{ ok: true; path: string; key: string; entry: unknown }
Scope: operator.admin
sessions.reset
Params:
{ key: string }
Response:
{ ok: true; key: string; entry: unknown }
Scope: operator.admin
sessions.delete
Params:
{ key: string; deleteTranscript?: boolean }
Response:
{ ok: true; key: string; deleted: boolean; archived: string[] }
Scope: operator.admin
sessions.compact
Params:
{ key: string; maxLines?: number }
Response:
{ ok: true; key: string; compacted: boolean; reason?: string; archived?: string; kept?: number }
Scope: operator.admin
Nodes
node.pair.request
Params:
{ nodeId: string; displayName?: string; platform?: string; version?: string; coreVersion?: string; uiVersion?: string; deviceFamily?: string; modelIdentifier?: string; caps?: string[]; commands?: string[]; remoteIp?: string; silent?: boolean }
Response:
{ status: "pending"; request: NodePairingPendingRequest; created: boolean }
Scope: operator.pairing
node.pair.list
Params:
{}
Response:
{ pending: NodePairingPendingRequest[]; paired: NodePairingPairedNode[] }
Scope: operator.pairing
node.pair.approve
Params:
{ requestId: string }
Response:
{ requestId: string; node: NodePairingPairedNode }
Scope: operator.pairing
node.pair.reject
Params:
{ requestId: string }
Response:
{ requestId: string; nodeId: string }
Scope: operator.pairing
node.pair.verify
Params:
{ nodeId: string; token: string }
Response:
{ ok: boolean; node?: NodePairingPairedNode }
Scope: operator.pairing
node.rename
Params:
{ nodeId: string; displayName: string }
Response:
{ nodeId: string; displayName: string }
Scope: operator.pairing
node.list
Params:
{}
Response:
{ ts: number; nodes: Array<{ nodeId: string; displayName?: string; platform?: string; version?: string; coreVersion?: string; uiVersion?: string; deviceFamily?: string; modelIdentifier?: string; remoteIp?: string; caps: string[]; commands: string[]; pathEnv?: string; permissions?: Record<string, boolean>; connectedAtMs?: number; paired: boolean; connected: boolean }> }
node.describe
Params:
{ nodeId: string }
Response:
{ ts: number; nodeId: string; displayName?: string; platform?: string; version?: string; coreVersion?: string; uiVersion?: string; deviceFamily?: string; modelIdentifier?: string; remoteIp?: string; caps: string[]; commands: string[]; pathEnv?: string; permissions?: Record<string, boolean>; connectedAtMs?: number; paired: boolean; connected: boolean }
node.invoke
Params:
{ nodeId: string; command: string; params?: unknown; timeoutMs?: number; idempotencyKey: string }
Response:
{ ok: true; nodeId: string; command: string; payload: unknown; payloadJSON?: string | null }
Notes: Requires the node to be connected and command allowed by policy/allowlist.
node.invoke.result
Params:
{ id: string; nodeId: string; ok: boolean; payload?: unknown; payloadJSON?: string; error?: { code?: string; message?: string } }
Response:
{ ok: true } | { ok: true; ignored: true }
Scope: node role only.
node.event
Params:
{ event: string; payload?: unknown; payloadJSON?: string }
Response:
{ ok: true }
Scope: node role only.
Devices
device.pair.list
Params:
{}
Response:
{ pending: DevicePairingPendingRequest[]; paired: Array<PairedDevice & { tokens?: Array<{ role: string; scopes: string[]; createdAtMs: number; rotatedAtMs?: number; revokedAtMs?: number; lastUsedAtMs?: number }> }> }
Scope: operator.pairing
device.pair.approve
Params:
{ requestId: string }
Response:
{ requestId: string; device: PairedDevice }
Scope: operator.pairing
device.pair.reject
Params:
{ requestId: string }
Response:
{ requestId: string; deviceId: string }
Scope: operator.pairing
device.token.rotate
Params:
{ deviceId: string; role: string; scopes?: string[] }
Response:
{ deviceId: string; role: string; token: string; scopes: string[]; rotatedAtMs: number }
Scope: operator.pairing
device.token.revoke
Params:
{ deviceId: string; role: string }
Response:
{ deviceId: string; role: string; revokedAtMs: number }
Scope: operator.pairing
Cron and Wake
wake
Params:
{ mode: "now" | "next-heartbeat"; text: string }
Response:
{ ok: boolean }
cron.list
Params:
{ includeDisabled?: boolean }
Response:
{ jobs: CronJob[] }
cron.status
Params:
{}
Response:
{ enabled: boolean; storePath: string; jobs: number; nextWakeAtMs: number | null }
cron.add
Params:
{ name: string; agentId?: string | null; description?: string; enabled?: boolean; deleteAfterRun?: boolean; schedule: CronSchedule; sessionTarget: "main" | "isolated"; wakeMode: "next-heartbeat" | "now"; payload: CronPayload; delivery?: CronDelivery }
Response:
CronJob
Scope: operator.admin
cron.update
Params:
{ id?: string; jobId?: string; patch: Partial<CronJob> }
Response:
CronJob
Scope: operator.admin
cron.remove
Params:
{ id?: string; jobId?: string }
Response:
{ ok: true; removed: boolean }
Scope: operator.admin
cron.run
Params:
{ id?: string; jobId?: string; mode?: "due" | "force" }
Response:
{ ok: true; ran: true } | { ok: true; ran: false; reason: "not-due" }
Scope: operator.admin
cron.runs
Params:
{ id?: string; jobId?: string; limit?: number }
Response:
{ entries: Array<{ ts: number; jobId: string; action: "finished"; status?: "ok" | "error" | "skipped"; error?: string; summary?: string; runAtMs?: number; durationMs?: number; nextRunAtMs?: number }> }
Messaging
send
Params:
{ to: string; message: string; mediaUrl?: string; mediaUrls?: string[]; gifPlayback?: boolean; channel?: string; accountId?: string; sessionKey?: string; idempotencyKey: string }
Response:
{ runId: string; messageId: string; channel: string; chatId?: string; channelId?: string; toJid?: string; conversationId?: string }
Notes: runId is idempotencyKey.
poll (undocumented but implemented)
Params:
{ to: string; question: string; options: string[]; maxSelections?: number; durationHours?: number; channel?: string; accountId?: string; idempotencyKey: string }
Response:
{ runId: string; messageId: string; channel: string; toJid?: string; channelId?: string; conversationId?: string; pollId?: string }
Notes: poll is not advertised in features.methods but is implemented.
Chat (WebSocket-native)
chat.history
Params:
{ sessionKey: string; limit?: number }
Response:
{ sessionKey: string; sessionId?: string; messages: unknown[]; thinkingLevel?: string }
chat.send
Params:
{ sessionKey: string; message: string; thinking?: string; deliver?: boolean; attachments?: unknown[]; timeoutMs?: number; idempotencyKey: string }
Response (immediate ack):
{ runId: string; status: "started" }
Possible cached response:
{ runId: string; status: "in_flight" }
Final state is delivered via chat events.
chat.abort
Params:
{ sessionKey: string; runId?: string }
Response:
{ ok: true; aborted: boolean; runIds: string[] }
chat.inject (undocumented but implemented)
Params:
{ sessionKey: string; message: string; label?: string }
Response:
{ ok: true; messageId: string }
Notes: chat.inject is not advertised in features.methods but is implemented.
Browser
browser.request
Params:
{ method: "GET" | "POST" | "DELETE"; path: string; query?: Record<string, unknown>; body?: unknown; timeoutMs?: number }
Response:
unknown (proxy result body)
Notes: If a connected browser-capable node is available, requests are proxied through it. Otherwise, the local browser control service is used if enabled.
Misc
system-presence, system-event, last-heartbeat, set-heartbeats
See Health and Status section.
Hidden or Not Advertised in features.methods
The following methods are implemented but not in the features.methods list returned by HelloOk:
pollchat.inject
Clients should primarily rely on features.methods to discover capabilities, but these methods exist in the current implementation.