Files
openclaw-mission-control/docs/openclaw_gateway_ws.md
2026-02-04 18:13:17 +05:30

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

  1. Open a WebSocket connection to the gateway.
  2. Server immediately sends an event frame named connect.challenge with a nonce.
  3. Client must send a req frame with method: "connect" and params: ConnectParams as the first request.
  4. Server responds with a res frame whose payload is a HelloOk object.

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/maxProtocol must include 3 (the server's expected protocol).
  • If you send device.nonce, it must match the connect.challenge nonce.
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:

  • node role can only call node.invoke.result, node.event, skills.bins.
  • operator.admin is 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: ConnectChallenge
  • agent: AgentEvent
  • chat: ChatEvent
  • presence: { presence: PresenceEntry[] }
  • tick: { ts: number }
  • talk.mode: { enabled: boolean; phase: string | null; ts: number }
  • shutdown: { reason: string; restartExpectedMs?: number }
  • health: HealthSummary
  • heartbeat: HeartbeatEventPayload
  • cron: CronEvent
  • node.pair.requested: NodePairingPendingRequest
  • node.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: DevicePairingPendingRequest
  • device.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:

  • poll
  • chat.inject

Clients should primarily rely on features.methods to discover capabilities, but these methods exist in the current implementation.