API Reference
Local examples use productId as the device agent ID, deviceId as the real device ID, and HTTP products paths as device agents.
HTTP: http://127.0.0.1:3000
Voice WebSocket: ws://127.0.0.1:3001/ws/voice
Voice HTTP: http://127.0.0.1:3001/api/chat, /api/vision/framesWhen direct HTTPS is enabled, use https:// for the main HTTP endpoint. Main HTTP TLS does not enable TLS for the MQTT broker or voice service; configure those endpoints separately and use wss:// when browsers connect to them. For a private CA, pass its CA file to clients instead of disabling certificate verification.
The Gateway does not provide user authentication, tenant isolation, session ownership, or resource-level authorization. HTTP, SSE, and voice WebSocket endpoints are for trusted systems or controlled clients only. Put them behind an authenticating and authorizing gateway before multi-user or public access. HTTPS/WSS protects traffic; it is not access control.
Choose a Protocol
| Scenario | Recommended Protocol | Notes |
|---|---|---|
| Real devices stay online, report state, and receive commands | MQTT | Fits device-side connections, state synchronization, and command responses. |
| Other agents discover and call an A2A-enabled Device Agent | A2A over MQTT | Uses MQTT v5 discovery and request/reply topics. |
| Business systems create or run multi-agent Scenes and one-off Tasks | HTTP + SSE | Submit requests over HTTP and receive planning, step, and final-result events over SSE. |
| Business systems, console extensions, or automation scripts call Device Agent | HTTP | Fits one-shot requests, queries, and command dispatch. |
| Realtime voice interaction | WebSocket | Fits continuous audio input, realtime ASR results, and TTS output. |
| Browser or device clients connect to an MQTT broker over WebSocket | MQTT over WebSocket | This is an MQTT transport mode, not the Device Agent voice WebSocket. |
MQTT
MQTT is used for device-side access. Devices use MQTT to come online, report state, receive commands, return command results, and publish events. Broker URL, credentials, and topic templates follow the console configuration.
| Direction | Topic | Purpose |
|---|---|---|
| MQTT client -> Device Agent | device-agent/{productId}/in | Send a text request to a device agent. |
| Device Agent -> MQTT client | device-agent/{productId}/out | Return a device agent reply. |
| MQTT client -> Device Agent | device-agent/{productId}/device/{deviceId}/in | Send a text request with device context. |
| Device Agent -> MQTT client | device-agent/{productId}/device/{deviceId}/out | Return a reply with device context. |
| Device Agent -> device | device-agent/{productId}/device/{deviceId}/commands | Send device commands. |
| Device -> Device Agent | device-agent/{productId}/device/{deviceId}/responses | Return command results. |
| Device -> Device Agent | v1/{productId}/{deviceId}/telemetry | Report online status and current state. |
| Device -> Device Agent | v1/{productId}/{deviceId}/event | Report device events. |
| Device -> Device Agent | device-agent/{productId}/device/{deviceId}/ntp/request | Request time synchronization. |
| Device Agent -> device | device-agent/{productId}/device/{deviceId}/ntp/response | Return time synchronization data. |
Text request payload:
{
"id": "req-001",
"prompt": "Check the current temperature",
"sessionId": "session-default:thermostat:thermostat-001",
"metadata": {
"source": "mqtt-client"
}
}Device agent reply payload:
{
"sessionId": "session-default:thermostat:thermostat-001",
"text": "The current temperature is 28 degrees.",
"metadata": {
"timestamp": "2026-05-11T10:00:00.000Z"
},
"timestamp": "2026-05-11T10:00:00.000Z"
}One chat turn produces multiple messages. Progress messages have an empty top-level text and carry progress in metadata.status:
metadata.status.type | Key fields |
|---|---|
start, thinking | Current execution stage. |
text_block | Incremental text in metadata.status.text. |
tool_start, tool_end | Tool name, input, output, or error. |
complete, error | Current execution result. The final complete reply still arrives in the top-level text of a regular message. |
To interrupt or clear a session, publish an empty prompt to the original input topic with metadata.interrupt: true or metadata.clearSession: true. The acknowledgement uses metadata.status.type: complete; metadata.replyTo matches the request id.
For more payload rules, validation details, and MQTTX examples, see MQTT Access.
A2A over MQTT
A2A clients use MQTT v5 to discover and call A2A-enabled Device Agents on the live network:
| Purpose | Topic |
|---|---|
| Discover a card | $a2a/v1/discovery/{org_id}/{unit_id}/{agent_id} |
| Discover within one organization | $a2a/v1/discovery/{org_id}/+/+ |
| Discover across organizations | $a2a/v1/discovery/+/+/+ |
| Send a request | $a2a/v1/request/{org_id}/{unit_id}/{agent_id} |
| Receive replies | Set an MQTT v5 responseTopic, normally under $a2a/v1/reply/... |
Agent cards are QoS 1 retained messages; an empty retained message removes a card. Device Agent handles JSON-RPC SendMessage requests. Publish requests at QoS 1 and include responseTopic; optional correlationData is returned unchanged. Use params.message.contextId to continue the same context and params.metadata.deviceId to target a device.
Request example:
{
"jsonrpc": "2.0",
"id": "task-001",
"method": "SendMessage",
"params": {
"message": {
"role": "user",
"parts": [
{
"type": "text",
"text": "Check the current temperature. If it is too high, switch the air conditioner to cooling mode."
}
],
"taskId": "task-001",
"contextId": "server-room-session"
},
"metadata": {
"sender": "my-a2a-client",
"deviceId": "air-conditioner-01"
}
}
}Replies use QoS 1 and are published to responseTopic:
| JSON-RPC result | Content |
|---|---|
result.statusUpdate | taskId, optional contextId, and status.state, status.timestamp, and optional status.message. |
result.artifactUpdate | taskId, optional contextId, and artifact.artifactId, artifact.parts, append, and lastChunk. |
Complete text arrives first in artifactUpdate.artifact.parts[].text, followed by TASK_STATE_COMPLETED; failures end with TASK_STATE_FAILED. Match the JSON-RPC id and correlationData, and set a client-side reply timeout.
See A2A Multi-Agent Collaboration for enabling agents, using Tasks and Scenes, and troubleshooting.
HTTP
HTTP API paths start with /api and currently have no version segment. /api/chat and the multi-agent orchestration execution endpoints return Server-Sent Events; other public integration endpoints use JSON. Check the release history before upgrading.
GET /api/health only shows that the HTTP process can respond. It does not check the LLM, MQTT, or device availability.
Chat and Vision
| Method | Path | Purpose |
|---|---|---|
GET | /api/health | Check whether the HTTP API is available. |
POST | /api/chat | Start text chat. Requires stream: true. |
GET | /api/sessions/:sessionId/history | Read session history. |
POST | /api/sessions/:sessionId/interrupt | Interrupt a session. |
DELETE | /api/sessions/:sessionId | Clear a session. |
POST | /api/sessions/:sessionId/tool-approvals | Grant or revoke a high-risk tool approval. Trusted administration clients only. |
POST | /api/vision/frames | Upload a vision frame for later chat use. |
| Request field | Requirement |
|---|---|
message | Required non-empty text. |
stream | Must be true. |
sessionId | Terminal clients should generate and retain it; history, interrupt, clear, and tool approval calls use the same ID. |
metadata.productId, metadata.deviceId | Include when device context is needed. |
visionRefs | Optional references to frames uploaded for the same session. |
Chat example:
$ curl -N http://127.0.0.1:3000/api/chat \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
-d '{
"message": "Check the current temperature and set the target temperature to 24",
"stream": true,
"sessionId": "demo-session",
"metadata": {
"productId": "thermostat",
"deviceId": "thermostat-001"
}
}'SSE events:
| Event | Data | Terminal |
|---|---|---|
status | type is start, thinking, text_block, tool_start, tool_end, complete, or error. | No |
message | { "text", "sessionId", "timestamp" } | Yes |
error | { "error", "sessionId", "timestamp?" } | Yes |
Only one chat request can be active per sessionId; another returns 429. A request waits for at most five minutes, then emits error and aborts the session. Disconnecting SSE also aborts the turn. To interrupt explicitly, call POST /api/sessions/:sessionId/interrupt.
Tool Approval
When a status event contains data.approvalRequired: true, the original SSE stream waits for approval. After verifying toolName and toolInput, a trusted administration client must submit this within five minutes:
{
"action": "approve",
"toolName": "write_file",
"scope": "once",
"target": {
"type": "write_file",
"path": "<toolInput.path>"
}
}toolName accepts write_file or execute_command; scope accepts once or session and defaults to once. An execute_command target is { "type": "execute_command", "command": "...", "cwd": "..." }. The API returns 403 when the server has not enabled the tool. Prefer a one-use approval with an exact target.
Upload a vision frame with /api/vision/frames, then pass the returned frameId and capturedAt to /api/chat as visionRefs. mimeType supports image/jpeg, image/png, and image/webp.
$ curl http://127.0.0.1:3000/api/vision/frames \
-H 'Content-Type: application/json' \
-d '{
"sessionId": "demo-session",
"deviceId": "thermostat-001",
"mimeType": "image/png",
"imageBase64": "<base64>",
"source": "camera"
}'A successful upload returns 201 with frameId, capturedAt, source, and mimeType; invalid input returns 400; an unavailable frame store returns 503. The imageBase64 string is limited to 6 MiB. Each session retains at most 20 frames for 5 minutes. Clearing the session also deletes its frames.
Pass the vision frame to chat:
{
"message": "Use this image to check whether the device screen looks abnormal",
"stream": true,
"sessionId": "demo-session",
"visionRefs": [
{
"frameId": "frame-001",
"capturedAt": "2026-05-11T10:00:00.000Z",
"source": "camera"
}
]
}Devices, Commands, and Events
| Method | Path | Purpose |
|---|---|---|
GET | /api/products | List device agents. |
GET | /api/products/:productId | Read a device agent definition, including properties, commands, parameters, and events. |
GET | /api/products/:productId/devices | List devices. Supports status and tags filters. |
GET | /api/products/:productId/devices/:deviceId | Get device details. |
POST | /api/products/:productId/devices/:deviceId/commands | Send a command to an online device. |
GET | /api/products/:productId/devices/:deviceId/events | Read device events. |
Read real IDs and the product definition first. Do not construct command names or parameters:
$ export BASE=http://127.0.0.1:3000
$ curl "$BASE/api/products"
$ export PRODUCT_ID=your_product_id
$ curl "$BASE/api/products/$PRODUCT_ID"
$ curl "$BASE/api/products/$PRODUCT_ID/devices?status=online&tags=site:lab"
$ export DEVICE_ID=your_device_idDevice list entries include status, state, tags, modelStatus, and lastSeenAt. status accepts online, offline, error, or all. Multiple tags=key:value,key:value filters must all match.
Use a command and parameters from the product definition:
$ curl "$BASE/api/products/$PRODUCT_ID/devices/$DEVICE_ID/commands" \
-H 'Content-Type: application/json' \
-d '{
"command": "set_drive_mode",
"params": {
"mode": "eco"
},
"timeoutMs": 30000
}'timeoutMs defaults to 30,000 and is limited to 120,000. Command responses:
| HTTP | Result |
|---|---|
200 | { "result": <device response> } |
400 | COMMAND_VALIDATION_FAILED |
404 | DEVICE_NOT_FOUND |
409 | DEVICE_NOT_ONLINE or DEVICE_COMMAND_FAILED |
504 | COMMAND_TIMEOUT |
After 504, the device may still execute the command later. Check device state before retrying.
Device events are newest first. limit defaults to 50 and is capped at 200. For the next page, pass the last event's id as beforeId.
Multi-Agent Orchestration
The HTTP orchestration API is available only on the main HTTP port and creates or runs A2A Scenes and one-off Tasks. Configure a usable LLM, then select a real agentId from entries in /api/a2a/agents whose status is online; do not construct it. visibility accepts public, private, or all and defaults to all. When a local device is needed, use the card's top-level productId to query a real deviceId.
| Method | Path | Purpose |
|---|---|---|
GET | /api/a2a/agents?visibility=all | List agent cards and online state in the current A2A organization and unit. |
GET | /api/a2a/scenes | List saved Scenes. |
POST | /api/a2a/scenes | Create a Scene and generate its collaboration flow asynchronously. |
GET | /api/a2a/scenes/:sceneId | Read Scene details and generation status. |
POST | /api/a2a/scenes/:sceneId/input | Add information to a Scene in needs_input. |
POST | /api/a2a/scenes/:sceneId/recreate | Replace the Scene name, goal, and agents, then regenerate it. |
DELETE | /api/a2a/scenes/:sceneId | Delete a Scene. |
POST | /api/a2a/chat | Select one ready Scene for conversation or execution. Returns SSE. |
POST | /api/a2a/scenes/:sceneId/chat | Limit the conversation to one Scene. Returns SSE. |
POST | /api/a2a/tasks | Start a one-off multi-agent Task. Returns SSE. |
Create a Scene and Wait Until It Is Ready
$ curl "$BASE/api/a2a/scenes" \
-H 'Content-Type: application/json' \
-d '{
"name": "Server room response",
"instructions": "Check server room temperature and humidity, adjust the air conditioner when the threshold is exceeded, and return an action report",
"agentIds": ["<sensor-agent-id>", "<air-conditioner-agent-id>"]
}'The endpoint returns 202 { "sceneId": "<scene-id>", "status": "creating" }. Save sceneId and poll GET /api/a2a/scenes/:sceneId:
status | What to do |
|---|---|
creating | Keep waiting and querying. Creation and regeneration both use this state. |
needs_input | Read pendingInput, submit the missing information, and continue querying. |
ready | The Scene can run. |
failed | Read failureReason, address the reported cause, and call recreate. |
When more information is needed, submit { "answers": "..." } to /api/a2a/scenes/:sceneId/input. recreate takes the same name, instructions, and agentIds, keeps sceneId, and regenerates the Scene.
Run a Scene
The shared Scene conversation uses /api/a2a/chat. Name the Scene and state the execution intent. The planner selects at most one ready Scene:
$ curl -N "$BASE/api/a2a/chat" \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
-d '{
"prompt": "Run the Server room response Scene now and return the final action report",
"contextId": "terminal-demo-001"
}'When the caller has the exact sceneId, it can send the same prompt to /api/a2a/scenes/:sceneId/chat to limit the candidate set.
Both endpoints are conversational. If the request is not explicit enough, the system can answer or ask for clarification instead of dispatching device actions. The caller stores multi-turn Scene history and sends it with each request; contextId identifies the downstream A2A context and does not persist HTTP conversation history.
Start a One-Off Task
A one-off Task does not save a Scene. devices is optional. When preselecting devices, each productId can appear only once.
$ curl -N "$BASE/api/a2a/tasks" \
-H 'Content-Type: application/json' \
-H 'Accept: text/event-stream' \
-d '{
"text": "Check every room temperature, set air conditioners to 24°C in rooms above 28°C, and return a summary",
"devices": [
{
"productId": "<sensor-product-id>",
"deviceId": "<sensor-device-id>"
},
{
"productId": "<air-conditioner-product-id>",
"deviceId": "<air-conditioner-device-id>"
}
]
}'The first request must include text. A Task can also answer or ask for clarification and does not always dispatch device actions. Optional history contains at most 12 { "role": "user|assistant", "text": "..." } messages, with at most 4,000 characters in each text. The caller stores conversation history and sends it with later new Task requests.
Handle Device Selection
When execution needs a concrete device and no usable selection was supplied, the current SSE stream emits status: input-required, then ends with binding_required. appId is the Scene ID:
event: binding_required
data: {"kind":"scene","appId":"<scene-id>","continuationId":"<id>","prompt":"<prompt>","tasks":[{"taskId":"<task-id>","productId":"<product-id>"}]}Send the continuation back to the original endpoint. Do not mix these body formats:
| Original endpoint | Continuation body |
|---|---|
/api/a2a/chat | prompt, continuationId, and devices: [{ productId, deviceId }]. |
/api/a2a/scenes/:sceneId/chat | prompt, continuationId, and deviceBindings: { taskId: deviceId }. |
/api/a2a/tasks | Only continuationId and a non-empty devices; do not send text or history again. |
continuationId remains valid for five minutes, can be used once, and is lost when the Gateway restarts. A Scene continuation is also invalidated when that Scene is deleted or regenerated. Select devices immediately after receiving the event.
Read SSE Events
Use curl -N to disable output buffering. Execution endpoints can emit these events:
| SSE event | Content |
|---|---|
status | working, input-required, completed, or failed. For metadata.type: plan, planning text is in data.text; for task_step, step details are in data.metadata. |
artifact | Final text appears in data.parts[].text. |
binding_required | Returns steps that need device bindings and a one-time continuationId, then ends the current stream. |
HTTP 200 only means that the SSE stream was established. A run failure is reported as a status event with data.state: failed; a normal run ends with data.state: completed. Keep reading until a terminal event arrives.
Multi-agent execution is not a persisted job: the API does not return a run ID or provide run history or post-disconnect status queries. Disconnecting SSE stops steps that have not been dispatched, but does not undo actions already sent to agents or devices. Execution requests have no idempotency key; do not blindly repeat a request when its result is unknown.
See A2A Multi-Agent Collaboration for the console workflow.
Workflows
HTTP can list, enable, disable, and delete workflows. Workflow creation and full replacement are handled by Device Agent workflow tools, usually from a conversation that describes the trigger and handling steps. See Workflows for the usage flow.
| Method | Path | Purpose |
|---|---|---|
GET | /api/workflows | List saved workflows. |
PATCH | /api/workflows/:workflowId | Enable or disable a workflow. Body: { "enabled": true } or { "enabled": false }. |
DELETE | /api/workflows/:workflowId | Delete a workflow. |
GET | /api/workflow-runs | List workflow runs. Supports limit, offset, and q. |
GET | /api/workflows/:workflowId/runs | List runs for one workflow. |
GET | /api/workflow-runs/:runId | Read one run and its step results. |
The list returns { "workflows": [...] }; PATCH returns { "workflow": {...} }; DELETE returns 204. Run lists return runs, total, limit, and offset; when q is provided, they also return query. limit defaults to 50 and is capped at 100.
Scheduled Tasks
Scheduled tasks are created in a Device Agent conversation. HTTP can list, pause, resume, update, cancel, and inspect their runs.
| Method | Path | Purpose |
|---|---|---|
GET | /api/timers?status=active&limit=50 | List tasks by active, paused, running, completed, or deleted status. |
PATCH | /api/timers/:timerId | Set enabled and/or replace instruction. |
DELETE | /api/timers/:timerId | Cancel a task. |
GET | /api/timers/:timerId/runs?limit=25&offset=0 | List persisted runs for one task. |
There is no HTTP create endpoint in 0.4.0. Changing the schedule rule also requires canceling the task and creating a new one in conversation.
The task list returns { "timers": [...] }, defaults to active, and caps limit at 100. PATCH returns { "timer": {...} }; DELETE returns 204. Run history returns { "runs", "total", "limit", "offset" }.
See Scheduled Tasks for creation and management in the console.
Webhook
| Method | Path | Purpose |
|---|---|---|
GET | /api/webhooks | List saved connections without returning URLs, headers, or signing secrets. |
POST | /api/webhooks | Create a connection. |
PATCH | /api/webhooks/:webhookId | Update its name, enabled state, credential, body template, or response condition. |
DELETE | /api/webhooks/:webhookId | Delete an unused connection. Returns 409 when a workflow still references it. |
POST | /api/webhooks/:webhookId/test | Send a test message and wait for the delivery result. |
Create a basic custom connection:
$ curl "$BASE/api/webhooks" \
-H 'Content-Type: application/json' \
-d '{
"name": "operations-alerts",
"enabled": true,
"credential": {
"url": "https://hooks.example.com/device-agent"
},
"preset": "custom",
"bodyTemplate": {
"message": "{{message}}"
}
}'Test the connection with POST /api/webhooks/:webhookId/test and { "message": "Device Agent test" }.
JSON request bodies are limited to 64 KiB. Create returns 201 { "webhook": {...} }; PATCH returns 200 { "webhook": {...} }; DELETE returns 204; a successful test returns 200 { "ok": true, "httpStatus": <status> }.
See Webhook for presets, signing, templates, delivery limits, and workflow use.
WebSocket
The voice channel uses /ws/voice. The connection can include these headers:
| Header | Notes |
|---|---|
Protocol-Version | Protocol version. Current value: 3. |
Device-Id | Current device ID. |
Client-Id | Client ID. Defaults to device ID when omitted. |
After connecting, send hello:
{
"type": "hello",
"version": 3,
"audio_params": {
"format": "pcm",
"sample_rate": 16000,
"channels": 1
},
"sessionId": "demo-session",
"productId": "thermostat",
"deviceId": "thermostat-001",
"provider": "aliyun"
}JSON control messages can use WebSocket text frames or type 1 binary frames. Audio uses type 0 binary frames in the format declared by audio_params. Every binary frame has a four-byte header:
| Byte | Content |
|---|---|
0 | Type: 0 audio, 1 UTF-8 JSON. |
1 | Reserved; set to 0. |
2..3 | Payload length as a big-endian uint16. |
Keep 16-bit PCM payloads even-sized and at or below 65,534 bytes. Generate a taskId for each turn and reuse it in listen, stop, and abort.
A voice turn usually follows this message flow:
| Direction | Message | Notes |
|---|---|---|
| Client -> Device Agent | hello | Start a voice session with audio parameters, device context, and speech provider. |
| Device Agent -> client | hello | Return session ID and server audio output parameters. |
| Client -> Device Agent | listen | Send `mode: auto |
| Client -> Device Agent | Binary audio frames | Send voice data. |
| Device Agent -> client | asr | Return text, definite, utterances, and taskId. |
| Client -> Device Agent | stop | Send the same taskId; may include visionRefs. |
| Device Agent -> client | agent_reply | Return text and taskId; streaming: true marks incremental text. |
| Device Agent -> client | TTS binary audio frames | Play synthesized voice reply. |
| Device Agent -> client | tts_complete | End the completed or aborted turn; may include taskId. |
| Client -> Device Agent | abort | Interrupt the turn with its taskId. |
| Client -> Device Agent | goodbye | Include the session_id returned by server hello. |
Server hello.audio_params defines the downstream TTS audio format; play PCM using its sample_rate. Even with listen.mode: auto, send stop at the end of each recording turn. Close code 1000 is normal; 1011 means the voice service is unavailable, and 1012 means voice configuration was reloaded and the client may reconnect.
See Voice Configuration for settings and Voice Interaction for usage.