Skip to content

API Reference ​

Local examples use productId as the device agent ID, deviceId as the real device ID, and HTTP products paths as device agents.

Choose a Protocol ​

ScenarioRecommended ProtocolNotes
Real devices stay online, report state, and receive commandsMQTTFits device-side connections, state synchronization, and command responses.
Other agents discover and call an A2A-enabled Device AgentA2A over MQTTUses MQTT v5 discovery and request/reply topics.
Business systems create or run multi-agent Scenes and one-off TasksHTTP + SSESubmit a durable Run over HTTP, then replay and follow its events over SSE.
Business systems, console extensions, or automation scripts call Device AgentHTTPFits one-shot requests, queries, and command dispatch.
Realtime voice interactionWebSocketFits continuous audio input, realtime ASR results, and TTS output.
Browser or device clients connect to an MQTT broker over WebSocketMQTT over WebSocketThis is an MQTT transport mode, not the Device Agent voice WebSocket.

Addresses and Authentication ​

txt
HTTP: http://127.0.0.1:3000
Voice WebSocket: ws://127.0.0.1:3001/ws/voice

With direct HTTPS enabled, use https:// for the main HTTP endpoint. Configure TLS separately for the MQTT broker and voice service, using wss:// for browser connections. For a private CA, provide its CA file to clients and keep certificate verification enabled.

The local Gateway supports an optional service Bearer token and provider console SSO for HTTP and SSE. These checks do not cover the separate voice WebSocket; protect it in addition to device or platform binding validation. The local runtime does not provide general multi-tenant isolation, session ownership, or resource-level authorization. Shared or public entry points must enforce user and resource permissions. HTTPS/WSS protects transport only. See Environment Variables for authentication settings.

When AGENT_GATEWAY_API_TOKEN is configured, service clients send this request header:

http
Authorization: Bearer <AGENT_GATEWAY_API_TOKEN>

When console SSO is configured, a valid login session cookie is also accepted. Failed authentication returns 401 with { "error": "Unauthorized" }. /health, /livez, and /readyz require no authentication; /metrics uses the configured service or console authentication.

MQTT ​

Devices use MQTT to come online, report state and events, receive commands, and return results. Broker addresses, credentials, topics, and payloads follow the current IoT platform adapter; obtain the selected device's protocol from the console.

For devices managed directly by Device Agent, use the complete contract in MQTT Access:

A2A over MQTT ​

A2A clients use MQTT v5 to discover and call A2A-enabled Device Agents on the live network:

PurposeTopic
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 repliesSet 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:

json
{
  "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 resultContent
result.statusUpdatetaskId, optional contextId, and status.state, status.timestamp, and optional status.message.
result.artifactUpdatetaskId, 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 ​

Except for health and metrics endpoints, HTTP API paths start with /api and have no version segment. Platform /api/chat returns a durable Agent Run event stream; A2A submissions return JSON with a separate event stream. Other endpoints use JSON. Check the release history before upgrading.

GET /health only shows that the HTTP process can respond. It does not check the LLM, MQTT, or device availability.

JSON error responses include error, with optional code, message, details, and external platform error code providerCode. Handle errors using the HTTP status and returned code, checking whether optional fields exist.

Chat and Vision ​

MethodPathPurpose
GET/healthCheck whether the HTTP process is available.
GET/livezCheck process liveness.
GET/readyzRead readiness checks; returns 200 when ready or 503 otherwise.
GET/metricsRead metrics in Prometheus text format.
GET/api/capabilitiesRead the active split-role deployment manifest and unavailable reasons.
POST/api/chatStart text chat. Requires stream: true.
POST/api/cloud/runsEnqueue a durable Platform Agent Run.
GET/api/cloud/runs/:runIdRead a durable Platform Agent Run.
POST/api/cloud/runs/:runId/cancelCancel a queued or running Platform Agent Run.
GET/api/cloud/runs/:runId/eventsStream durable Run events. Supports Last-Event-ID.
GET/api/sessions/:sessionId/historyRead session history.
GET/api/sessions/:sessionId/artifactsList durable files created in the Session workspace.
GET/api/sessions/:sessionId/artifacts/:encodedPathDownload one listed artifact. Supports ETag and byte ranges.
POST/api/sessions/:sessionId/interruptInterrupt a session.
DELETE/api/sessions/:sessionIdArchive the current conversation and clear its context. The same sessionId remains usable.
POST/api/sessions/:sessionId/tool-approvalsGrant or revoke a high-risk tool approval. Trusted administration clients only.
POST/api/vision/framesUpload a vision frame for later chat use.

Deployment Capabilities ​

Before exposing Platform features, call GET /api/capabilities rather than infer other roles' capabilities from the API environment. The response contains its version, manifest digest, configuration revision and digests, API/Worker/Ingress health, available features, Tools, Skills, active IM channels, and machine-readable disabled reasons. Missing, expired, or inconsistent role reports return 503 with CLOUD_DEPLOYMENT_CAPABILITIES_UNAVAILABLE.

Runs pin the manifest at admission. During a rollout, only a Worker reporting the exact compatible manifest digest can execute them.

Text Chat ​

Request fieldRequirement
messageRequired non-empty text.
streamMust be true.
sessionIdTerminal clients should generate and retain it; history, interrupt, clear, and tool approval calls use the same ID.
metadata.productId, metadata.deviceIdInclude when device context is needed.
visionRefsOptional references to frames uploaded for the same session.

Chat example:

bash
$ 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:

EventDataTerminal
statustype is start, thinking, thinking_delta, text_block, tool_start, tool_end, complete, error, compaction_start, or compaction_end.No
message{ "text", "sessionId", "timestamp" }Yes
error{ "error", "sessionId", "timestamp?" }Yes

In a local runtime, only one chat request can be active per sessionId; another returns 429. Platform admits requests to a durable queue and executes them serially within each session. A client disconnect only closes event delivery; the Run continues.

Platform chat retry and reconnection rules:

  • Retain sessionId and use one Idempotency-Key header per turn. Retry with the same POST body and key. Different content returns 409; omitting the key creates a new request on every retry.
  • Reconnect with Last-Event-ID or ?after=<last-id> to replay events after the last received SSE id.
  • /api/chat does not return the Run ID. Interrupt a session with POST /api/sessions/:sessionId/interrupt.

Submit a Platform Run ​

Use POST /api/cloud/runs when a trusted business system needs a Run ID for status, events, or cancellation. It requires console session or service authentication and resource checks at the integration entry point; it does not apply the external-user device binding check below. Requests use prompt and top-level device identifiers:

bash
$ export BASE=https://agent.example.com
$ curl "$BASE/api/cloud/runs" \
  -H 'Content-Type: application/json' \
  -d '{
    "sessionId": "demo-session",
    "idempotencyKey": "temperature-query-001",
    "prompt": "Check the current temperature",
    "productId": "thermostat",
    "deviceId": "thermostat-001"
  }'

sessionId, idempotencyKey, and non-empty prompt are required. A new request returns 201 { "run": {...}, "created": true }; an identical retry returns 200, the original Run, and created: false.

Save run.id to query, stream, or cancel through the endpoints above. Events support Last-Event-ID or after. Cancellation cannot undo completed device or external actions; check their results before resubmitting.

Session history and context ​

GET /api/sessions/:sessionId/history pages backward from the latest messages. limit defaults to 20 and accepts 1–100. The response contains sessionId, messages, messageOffset, and nextCursor, plus contextUsage when available.

Pass the previous page's nextCursor unchanged as before; nextCursor: null ends pagination. Pages preserve user-turn boundaries, so the message count can exceed limit. Clearing or branching history can invalidate cursors; reload the latest page after 400.

contextUsage includes tokens, contextWindow, and percent. Usage may be unknown immediately after restoration or compaction. Wait for a later model response rather than displaying zero. Chat status events may include usage; compaction_start and compaction_end show progress and are not final answers.

Clearing archives the conversation and resets context without deleting memory or devices. See Conversations and Memory.

Durable Session Control ​

Platform POST /api/sessions/:sessionId/interrupt and DELETE /api/sessions/:sessionId use the Idempotency-Key header for replay-safe retries:

  • Use 1 to 256 characters without ASCII control characters. Each Stop or Clear needs a new key; retries reuse the original key.
  • Retries replay the first accepted target and result without affecting later work. Different sessions, action types, or targets must not share a key.
  • Compatibility requests may omit the header, but each retry becomes a new control operation rather than replaying the original result.

When Stop or Clear can arrive before a chat turn, also send that turn's idempotencyKey as X-Device-Agent-Target-Idempotency-Key. A matching request arriving later is recorded as cancelled rather than executed. Different content with the same target key returns 409; omit this header when no turn awaits admission.

Interrupt cancels queued Runs immediately and requests cancellation of running Runs. Its response distinguishes pendingAdmissionCancelled, queuedCancelled, runningCancellationRequested, and runningAwaitingAcknowledgement. Clear returns 409 AGENT_SESSION_ACTIVE while a queued or running Run exists; retry it with the same operation and target keys after Stop has settled.

Operation replay and delayed-request fencing are bounded by the Agent Run detail retention period, DEVICE_AGENT_RUN_RETENTION_HOURS. Keep client retries within a shorter window. After records expire, use a new key for a new user action.

Session Artifacts ​

GET /api/sessions/:sessionId/artifacts lists only files in the Agent Session workspace and returns each file's path, opaque encodedPath, byte length, and SHA-256 digest. Use the returned encodedPath unchanged in the download URL; do not encode a path yourself. Session history and internal JSONL files are never exposed as artifacts.

Artifact downloads use application/octet-stream, ETag: "sha256:<digest>", Accept-Ranges: bytes, and Cache-Control: private, no-cache. Send If-None-Match for a 304 response or one Range: bytes=<start>-<end> interval for a 206 response. An invalid range returns 416; an unknown Session or artifact returns 404.

Durable Product Event Streams ​

PathSSE eventContents
/api/device-events/stream?namespace=&productId=&deviceId=deviceDurable Provider and browser-simulator device events; all three filters are optional.
/api/timers/eventstimerDurable Timer changed, fired, complete, and error transitions.
/api/agent-replies/eventsagent_replyDurable replies for trusted Workflow, Timer, A2A, IM, and MQTT dispatches. Ordinary chat and PromptTasks are not broadcast here.

Event stream cursor and error rules:

  • Without a cursor, receive new events only. Replay by sending the last SSE id as Last-Event-ID or ?after=<cursor>; the header takes precedence.
  • The first event is ready with { "ok": true }. Keepalive comments do not advance the cursor.
  • Invalid cursors return 400; cursors outside retained history bounds return 409 with recovery boundaries; a preflight storage outage returns 503.
  • If storage fails after streaming starts, the server sends an error event without sensitive information and closes the stream.

Platform PromptTasks ​

Platform accepts public PromptTasks only through the top-level promptTask object on POST /api/chat or POST /api/cloud/runs:

json
{
  "promptTask": { "kind": "device.modeling" }
}

Supported kinds are device.modeling, device.simulator.behavior, device.icon.select, and device.event.payload.generate. Do not put taskKind or taskScope in metadata; Platform rejects that legacy shape with 400 and PROMPT_TASK_CONTRACT_REQUIRED.

PromptTasks use a dedicated system prompt and isolated transient Session, with no tools or access to the ordinary Agent Session. sdk.bundle.enhance and unknown kinds return 400 with PROMPT_TASK_UNSUPPORTED rather than running as ordinary chat. Durable requests use a stable sessionId and idempotencyKey in /api/cloud/runs.

Tool Approval ​

In Platform mode, an approval_required SSE event contains the durable approvalId, toolName, toolCallId, exact target, message, and expiry. After verifying those fields, a trusted administration client approves the operation within five minutes:

json
{
  "approvalId": "approval-id-from-sse",
  "decision": "approve",
  "scope": "once"
}

decision accepts approve, reject, or revoke. Only approve accepts scope; scope accepts once or session and defaults to once. The server binds the decision to the stored session, Tool call, input digest, and target. Clients do not resubmit or modify the target.

Platform requires approval for write_file, execute_command, mqtt_publish, webhook, and send_im_message. The SSE stream emits approval_decision when the approval is granted, rejected, expired, consumed, or revoked. Prefer one-use approval. Modeled device operations through use_command, update_device_state, and use_device_job remain durably fenced against duplicate execution but do not require separate approval.

Standalone keeps its local action, toolName, and exact target request contract for write_file and execute_command; it does not use Platform durable approval IDs.

Vision Frames ​

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.

bash
$ 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 scope retains at most 20 frames, expiring after 5 minutes in Standalone or 15 minutes by default in Platform deployments.

In Platform deployments, the server binds each frame to its tenant, principal, namespace, session, Product, and Device scope. Send the same sessionId, productId, and deviceId when referencing the frame. A request can reference one to three frames. Missing, expired, duplicate, or scope-mismatched references return 400 with VISION_FRAME_REFERENCE_INVALID.

Pass the vision frame to chat:

json
{
  "message": "Use this image to check whether the device screen looks abnormal",
  "stream": true,
  "sessionId": "demo-session",
  "metadata": {
    "productId": "thermostat",
    "deviceId": "thermostat-001"
  },
  "visionRefs": [
    {
      "frameId": "frame-001",
      "capturedAt": "2026-05-11T10:00:00.000Z",
      "source": "camera"
    }
  ]
}

Durable Platform Agent Runs accept the same visionRefs at the top level:

bash
$ curl http://127.0.0.1:3000/api/cloud/runs \
  -H 'Content-Type: application/json' \
  -d '{
    "sessionId": "demo-session",
    "idempotencyKey": "inspect-frame-001",
    "prompt": "Use this image to check whether the device screen looks abnormal",
    "productId": "thermostat",
    "deviceId": "thermostat-001",
    "visionRefs": [
      {
        "frameId": "frame-001",
        "capturedAt": "2026-05-11T10:00:00.000Z",
        "source": "camera"
      }
    ]
  }'

Platform resolves every reference and stores immutable image bytes before it enqueues the Run. The Run can therefore survive frame expiry, API restart, lease loss, and Worker replacement. Reusing an idempotencyKey with different frame content returns 409 instead of changing the existing Run.

Platform Skills and Dynamic Tools ​

Platform stores tenant user Skills and dynamic Tool extensions as immutable revisions. Dynamic Tool source runs inside Workers, so these administration endpoints accept trusted server-side content only.

MethodPathPurpose
GET/api/skillsList current user Skills and their enabled state.
POST/api/skills/importImport a Skill ZIP from multipart field file.
GET/api/skills/:name/exportExport the current Skill revision as a ZIP.
PATCH/api/skills/:nameEnable or disable a Skill with { "enabled": true|false }.
DELETE/api/skills/:nameRemove the Skill from the current catalog.
GET/api/toolsList current dynamic Tools, descriptors, revisions, and enabled state.
PATCH/api/tools/:nameEnable or disable one dynamic Tool with { "enabled": true|false }.
GET/api/tool-editor/extensionsList current Tool extension packages.
GET/api/tool-editor/extensions/:name/sourceRead one current Tool extension source package.
POST/api/tool-editor/extensions/saveValidate and save an immutable Tool extension revision.
DELETE/api/tool-editor/extensions/:nameRemove the extension from the current catalog.

A Skill ZIP must contain one <skill-folder>/SKILL.md with name and description in YAML frontmatter. It may include references/, templates/, and assets/. Limits: 5 MiB compressed, 256 entries, and 25 MiB decompressed.

bash
$ curl "$BASE/api/skills/import" \
  -F 'file=@customer-diagnostics.zip'
$ curl "$BASE/api/skills/customer-diagnostics" \
  -X PATCH \
  -H 'Content-Type: application/json' \
  -d '{"enabled":false}'

Each Tool extension exports a default registration function using the supplied registerTool and Type. Every Tool needs an execution descriptor: read operations use { "execution": "read", "approval": "none" }; effects requiring approval declare approval: "required" and expose only approved values and SHA-256 digests in approval requests. Missing descriptors, static or extension Tool name collisions, invalid schemas, or unsupported npm imports return 400 without creating a revision.

json
{
  "name": "customer-tools",
  "code": "export default function ({ registerTool, Type }) { registerTool({ name: 'customer_lookup', description: 'Look up one customer.', parameters: Type.Object({ customerId: Type.String() }), async execute(_id, params) { return { content: [{ type: 'text', text: params.customerId }], details: {} }; } }, { execution: 'read', approval: 'none' }); }"
}

Submit this body to POST /api/tool-editor/extensions/save for an immutable revision and contentDigest.

Agent Runs pin the enabled Skill and Tool revisions at admission; catalog changes affect new Runs only. Worker replacement and idempotent retries retain the original revisions. A Worker fails the Run if revision, digest, or Tool policy checks fail.

Platform MQTT Operations and Zero EMQX ​

Platform MQTT subscriptions are durable operations executed by Ingress. The API opens no separate broker connection, and accepted requests survive API or Ingress replacement.

MethodPathPurpose
POST/api/mqtt/subscriptionsStart one bounded MQTT subscription.
GET/api/mqtt/subscriptions/:operationIdRead the durable subscription state or result.
POST/api/mqtt/subscriptions/:operationId/cancelRequest cancellation and broker unsubscribe.
GET, PUT/api/agent-configRead or replace the revisioned non-secret Platform runtime configuration.
POST/api/agent-config/zero-emqxProvision and apply one managed Zero EMQX instance.
GET/api/agent-config/zero-emqx/operations/:operationIdRead the durable provisioning state or result.
GET/api/agent-config/mqtt-statusRead the current Ingress-owned broker connection status.

Subscription and Zero EMQX creation require an Idempotency-Key header of 1 to 200 characters. The same key and request return the original operation; different content returns 409 with CLOUD_MQTT_OPERATION_CONFLICT.

Start a subscription with a topic filter and explicit resource bounds:

bash
$ curl "$BASE/api/mqtt/subscriptions" \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: collect-demo-events-001' \
  -d '{
    "topic": "demo/+/events",
    "qos": 1,
    "duration": 10,
    "maxMessages": 20,
    "maxBytes": 65536
  }'

Subscription parameters:

  • qos: defaults to 0.
  • duration: defaults to 10 seconds; range 1 to 60.
  • maxMessages: defaults to 100; range 1 to 100.
  • maxBytes: defaults to 262,144; range 1 to 1,048,576.
  • Optional MQTT 5 properties: subscriptionIdentifier (1 to 268,435,455) and up to 32 userProperties. Each property name and value is limited to 512 UTF-8 bytes.

Operation states are pending, running, succeeded, failed, cancelled, or uncertain. A new operation completed within the short wait returns 201; otherwise, it returns 202 and Location. Poll that URL until terminal. Success includes messages, messageCount, totalBytes, and limitReached.

Only cancelled confirms cancellation. uncertain means broker cleanup is unconfirmed; check the broker state before deciding whether to retry with a new key.

Provision Zero EMQX only after an explicit user confirmation:

bash
$ curl "$BASE/api/agent-config/zero-emqx" \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: provision-demo-broker-001' \
  -d '{"confirmed":true,"tag":"customer-demo"}'

tag is optional and accepts 1 to 63 characters. Configure the Zero EMQX manager and a ready Ingress role first. New credentials are encrypted and committed with non-secret configuration; Ingress reconnects using that revision. The API never returns the broker username or password. Success includes instanceId, credential-free brokerUrl and wsUrl, expiresAt, configRevision, and applyStatus: "applied".

If provisioning may have completed but its reply was lost, the operation ends as uncertain with ZERO_EMQX_PROVISION_UNCERTAIN and is not retried automatically.

Use the status endpoint to verify the configuration currently owned by Ingress:

bash
$ curl "$BASE/api/agent-config/mqtt-status"

The response includes state, connected, credential-free brokerUrl, revision, activeConfigRevision, restartRequired, updatedAt, and optional sanitized lastError, never MQTT credentials. connected: true means the connection and required subscriptions are ready; restartRequired: true means the saved revision is newer than the active Ingress revision.

GET /api/agent-config returns x-device-agent-config-revision, x-device-agent-restart-required, and x-device-agent-restart-sections. The last header lists cached sections requiring restart, separated by commas, or is empty when no restart is needed. Affected roles exit gracefully for the deployment supervisor to replace them using the new revision.

For updates, send the current revision in the x-device-agent-config-revision header of PUT /api/agent-config. Missing or invalid revisions return 400; stale revisions return 409. The API stores non-secret configuration, masks deployment Secrets in responses, and rejects plaintext deployment Secrets in requests. Startup environment variables seed revision 1 only; use this API for later updates.

Devices, Commands, and Events ​

MethodPathPurpose
GET/api/productsList device agents.
GET/api/products/:productIdRead a device agent definition, including properties, commands, parameters, and events.
GET/api/products/:productId/devicesList devices. Supports status and tags filters.
GET/api/products/:productId/devices/:deviceIdGet device details.
POST/api/products/:productId/devices/:deviceId/commandsSend a command to an online device.
GET/api/products/:productId/devices/:deviceId/eventsRead device events.

Read real IDs and the product definition first. Do not construct command names or parameters:

bash
$ 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_id

Device 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:

bash
$ 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:

HTTPResult
200{ "result": <device response> }
400COMMAND_VALIDATION_FAILED
404DEVICE_NOT_FOUND
409DEVICE_NOT_ONLINE or DEVICE_COMMAND_FAILED
504COMMAND_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.

Product A2A Card Lifecycle ​

Use the Product A2A Card endpoints to publish or remove the retained discovery card generated from the Product runtime model.

MethodPathPurpose
POST/api/products/:productId/a2a-cardPublish the Product Agent Card.
DELETE/api/products/:productId/a2a-cardRemove the retained Product Agent Card.
GET/api/products/:productId/a2a-card/operations/:operationIdRead one Platform publication operation.
DELETE/api/products/:productIdDelete the Product and remove its retained Agent Card.

The publish body accepts publishToA2ARegistry, which defaults to false:

bash
$ curl "$BASE/api/products/$PRODUCT_ID/a2a-card" \
  -X POST \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: publish-product-card-001' \
  -d '{"publishToA2ARegistry":true}'

When Platform Ingress completes the operation within the wait, publish and unpublish return 200, and Product deletion returns 204. Otherwise, the response is 202 with { "operationId": "...", "status": "pending" } and Location pointing to the Product operation URL. Poll it until status is applied or failed:

json
{
  "operationId": "operation-001",
  "productId": "thermostat",
  "action": "unpublish",
  "status": "applied",
  "desiredRevision": 2,
  "appliedRevision": 2,
  "createdAt": "2026-08-20T01:00:00.000Z",
  "updatedAt": "2026-08-20T01:00:01.000Z"
}

Reuse Idempotency-Key for the same mutation to return the original operation without a new Product revision. Responses expose no Broker credentials, connection errors, or stored Agent Card.

External Platform User Binding ​

For ThingsNeural devices, the entries below verify user-device bindings through its Client API, with briefly cached results.

External callers must send the platform user ID in X-External-User-Id; a missing or blank header returns 401. A valid console session skips this check for chat and Vision uploads. Other IoT platforms do not require the header.

EntryWhen checked
Device, command, and event APIsEvery request; device lists only include devices bound to the user.
POST /api/chatWhen metadata carries a deviceId; referencing a guarded product without a device is rejected.
POST /api/vision/framesWhen the body carries a deviceId.
/ws/voiceWhen the product is ThingsNeural-backed; pass the user as ?userId= in the URL. See the WebSocket section below.
HTTPError codeMeaning
401EXTERNAL_IDENTITY_REQUIREDMissing external user ID or device identifier.
403EXTERNAL_DEVICE_NOT_BOUNDThe user is not bound to this device, or the request referenced a guarded product without a device.
503EXTERNAL_BINDING_UNAVAILABLEBinding lookup is temporarily unavailable; retry later.

"Device not found" and "not bound" both return 403, with no external distinction.

Multi-Agent Orchestration ​

Platform creates and executes A2A Scenes and one-off Tasks as durable Worker Runs. Configure a usable LLM, then select an agentId with status: online from the cards returned by /api/a2a/agents. visibility accepts public, private, or all, defaulting to all. For a local device, use the card's top-level productId to query deviceId rather than construct identifiers.

MethodPathPurpose
GET/api/a2a/agents?visibility=allList agent cards and online state in the current A2A organization and unit.
GET/api/a2a/scenesList saved Scenes.
POST/api/a2a/scenesAdmit durable Scene creation and flow generation.
GET/api/a2a/scenes/:sceneIdRead Scene details and generation status.
POST/api/a2a/scenes/:sceneId/inputAdmit durable input for a Scene in needs_input.
POST/api/a2a/scenes/:sceneId/recreateAdmit durable Scene replacement and regeneration.
DELETE/api/a2a/scenes/:sceneIdDelete a Scene.
POST/api/a2a/chatAdmit a durable global Scene conversation or execution.
POST/api/a2a/scenes/:sceneId/chatAdmit a durable conversation or execution for one ready Scene.
POST/api/a2a/tasksAdmit a durable one-off multi-agent Task.
GET/api/a2a/runs/:runIdRead one durable orchestration Run.
POST/api/a2a/runs/:runId/cancelRequest cancellation of one Run.
GET/api/a2a/runs/:runId/eventsReplay and follow one Run's durable SSE events.

Submit and Monitor a Run ​

A2A submission POST endpoints require the Idempotency-Key header, except POST /api/a2a/runs/:runId/cancel. The same key and request return the original Run; different content returns 409.

bash
$ curl -i "$BASE/api/a2a/scenes" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: create-server-room-scene-001' \
  -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>"]
  }'

Every submission returns 202, JSON containing run, and Location: /api/a2a/runs/<runId>. Scene generation also returns sceneId and Scene status. Retain the Run and Scene identifiers.

Create or recreate Scenes with name, instructions, and agentIds; supply Scene input with { "answers": "..." }. Scene and global chat use their respective conversation inputs. One-off Tasks use text and optional devices.

Poll GET /api/a2a/runs/:runId. pending and running are non-terminal; terminal states are input_required, succeeded, failed, cancelled, and uncertain.

input_required ends the current Run. If device binding is needed, the terminal SSE event supplies a one-use continuationId. Submit it with the binding selection and a new Idempotency-Key to the corresponding Scene, global chat, or Task endpoint. Monitor the returned new Run; the original never resumes. After Scene generation, also query the Scene:

statusWhat to do
creatingKeep waiting and querying. Creation and regeneration both use this state.
needs_inputRead pendingInput, submit the missing information, and continue querying.
readyThe Scene can run.
failedRead failureReason, address the reported cause, and call recreate.

Follow events with curl -N "$BASE/api/a2a/runs/$RUN_ID/events". Reconnect using the monotonic SSE id in Last-Event-ID: <last-id> or ?after=<last-id>. Disconnecting only ends the subscription; use the cancel endpoint to stop execution. uncertain means an external action may have completed, so check its target before resubmitting.

See A2A Multi-Agent Collaboration for the console workflow.

Workflows ​

HTTP endpoints manage workflows and run records; conversation tools also support creation and complete replacement. See Workflows for the usage flow.

MethodPathPurpose
GET/api/workflow-capabilitiesList available workflow tools, skills, and IM channels.
GET/api/tool-catalogList enabled tool names and descriptions.
GET/api/workflowsList saved workflows.
PUT/api/workflows/:workflowIdCreate or completely replace one workflow definition.
PATCH/api/workflows/:workflowIdEnable or disable a workflow. Body: { "enabled": true } or { "enabled": false }.
DELETE/api/workflows/:workflowIdDelete a workflow.
GET/api/workflow-runsList workflow runs. Supports limit, offset, and q.
GET/api/workflows/:workflowId/runsList runs for one workflow.
GET/api/workflow-runs/:runIdRead one run and its step results.

PUT replaces the complete definition, so include every field to retain. Body id is optional but must match the path when supplied. Omitting enabled sets it to true, even for an existing workflow. Creation returns 201; replacement returns 200.

bash
$ export WORKFLOW_ID=battery-low-alert
$ curl "$BASE/api/workflows/$WORKFLOW_ID" \
  -X PUT \
  -H 'Content-Type: application/json' \
  -d '{
    "trigger": {
      "kind": "device_event",
      "eventName": "battery_low_warning"
    },
    "actions": [
      {
        "type": "log_event",
        "eventType": "device_event"
      }
    ]
  }'

Invalid JSON objects or workflow definitions return 400 with code WORKFLOW_DEFINITION_INVALID. A body id that differs from the path returns 400 with code WORKFLOW_ID_MISMATCH.

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.

MethodPathPurpose
GET/api/timers?status=active&limit=50List tasks by active, paused, running, completed, or deleted status.
PATCH/api/timers/:timerIdSet enabled and/or replace instruction.
DELETE/api/timers/:timerIdCancel a task.
GET/api/timers/:timerId/runs?limit=25&offset=0List persisted runs for one task.

The conversation tool update_scheduled_task changes schedule kind, expression, and time zone while preserving the task ID and targets. HTTP PATCH supports only instruction and enabled state. Cancel and recreate a task to change its device scope.

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 ​

MethodPathPurpose
GET/api/webhooksList saved connections without returning URLs, headers, or signing secrets.
POST/api/webhooksCreate a connection.
PATCH/api/webhooks/:webhookIdUpdate its name, enabled state, credential, body template, or response condition.
DELETE/api/webhooks/:webhookIdDelete an unused connection. Returns 409 when a workflow still references it.
POST/api/webhooks/:webhookId/testSend a test message and wait for the delivery result.

Create a basic custom connection:

bash
$ 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.

Platform Monitoring, Credits, and Licensing ​

These endpoints require a platform deployment's address and authentication. For console workflows, see Monitoring, Device AI Credits, and License Management.

MethodPathPurpose and main parameters
GET/api/monitoringPlatform metrics; required section, optional range.
GET/api/monitoring/ai-usage/devicesDevice search; keyword q and limit from 1–20. Alternatively, use productId and deviceId to resolve one device.
GET/api/monitoring/ai-usageDevice usage; required productId, deviceId, from, and to.
GET/api/monitoring/ai-usage/cumulativeCumulative usage; optional from and paired productId and deviceId.
GET/api/monitoring/voiceVoice metrics; optional range and paired device identifiers.
GET/api/monitoring/voice/turnsDevice voice turns; required device identifiers, from, and to; optional cursor.
GET/api/monitoring/voice/turnOne voice turn; required device identifiers and a UUID id.
GET/api/ai-credits/accountsDevice balance; required productId and deviceId.
POST/api/ai-credits/adjustmentsAdd to or deduct from a device's paid / manual balance.
GET/api/cloud/licenseRead deployment authorization, usage, queue size, and validity.
POST/api/cloud/licenseImport a license file using the separate License administrator token.

Monitoring Queries ​

For /api/monitoring, section accepts overview, usage, cluster, connections, or latency. range accepts 1h, 24h, 7d, or 30d, defaulting to 24h. The response contains the selected section's snapshot, time window, and collection status. Missing or unavailable monitoring returns 503 with CLOUD_MONITORING_NOT_CONFIGURED or CLOUD_MONITORING_UNAVAILABLE respectively.

bash
curl 'https://agent.example.com/api/monitoring?section=overview&range=24h'

Device usage requires timezone-qualified ISO from and to values forming a forward range of at most 31 days. Results are grouped by call completion time and include summary, byModel, and series. Optional filters are kind (llm, asr, tts), provider, model, source, and outcome (completed, failed, cancelled). Supply these query parameters directly; do not use a filters query parameter.

Cumulative queries have no 31-day limit and use call start times. Omitting device identifiers aggregates recorded device calls in the current deployment scope; providing them limits results to that device. Omitting from starts at the first record. Costs estimate priced calls; unpriced calls are not necessarily free.

Voice summaries accept range values 1h, 24h, or 7d, defaulting to 24h. Turn queries return turns and nextCursor. Keep the original device and time window when paging and pass the returned cursor as cursor. Per-turn metrics are retained for 7 days; a missing detail returns 404.

Device Balance Adjustments ​

A balance snapshot includes currency, limit, paidBalance, allowanceBalance, totalBalance, and used. Reading an existing device's balance does not create a persistent credit account.

Adjustment requests use this JSON shape:

json
{
  "productId": "thermostat",
  "deviceId": "device-001",
  "amount": 10,
  "reason": "Additional trial credits",
  "idempotencyKey": "credit-adjustment-example-001"
}

Adjustment rules:

  • amount uses the settlement currency, must be nonzero, and accepts up to six decimal places. Positive values add credits; negative values deduct them.
  • reason is required, with up to 512 characters.
  • Retry with the same idempotencyKey and payload; use a new key for a new adjustment. Keys are scoped to device accounts; different content within an account returns 409.
  • Success returns the updated balance snapshot.

Manage shared budgets and pricing through the aiCredits section of the existing GET / PUT /api/agent-config endpoints. Preserve the full configuration and send its current revision when updating. The settlement currency cannot change once credit accounts exist.

Import a Platform License ​

GET /api/cloud/license returns the authorization state, claims, and usage: used counts requests started in the last 60 seconds, hardLimit is the requests-per-minute limit including burst allowance, and queued counts waiting requests.

For imports, send the complete license text in the POST body with Content-Type: text/plain and the X-Device-Agent-License-Admin-Token header. This token is separate from everyday login and service credentials. The deployment's normal login or service authentication still applies. Call the endpoint from a trusted management client and keep the token out of public frontend configuration.

Success returns the new snapshot without a deployment restart. Missing or incorrect tokens, mismatched files, expired licenses, and not-yet-active licenses return 403; revision conflicts return 409. A failed import preserves the installed license. A License does not include third-party model or voice service allowances.

Platform Runtime Logs ​

Platform deployments archive sanitized JSON log records from the API, Worker, and Ingress roles in PostgreSQL. These administration endpoints never expose pod file paths and are intended for trusted server-to-server clients.

MethodPathPurpose
GET/api/logs/files?role=<role>&instanceId=<instance>&limit=200&cursor=<cursor>List daily archives with a stable snapshot cursor. Both filters are optional, but instanceId requires role; limit is 1–500.
GET/api/logs/tail?role=<role>&instanceId=<instance>&file=<file>&lines=200&before=<cursor>Read up to 1,000 records and page backward with nextBeforeOffset.
GET/api/logs/download?role=<role>&instanceId=<instance>&file=<file>Download one exact archive as newline-delimited JSON.

role is api, worker, or ingress. A single-process cloud-all development runtime reports all. First list the available role, instance, and file values; do not construct instance IDs. Archive names use the UTC form device-agent-YYYY-MM-DD.log.

bash
$ curl "$BASE/api/logs/files?role=worker&limit=200"
$ curl "$BASE/api/logs/tail?role=worker&instanceId=worker-a&file=device-agent-2026-08-20.log&lines=200"

Pagination and errors:

  • Pass the files response's nextCursor unchanged as cursor until null. It pins the initial archive snapshot to prevent skipped files after role replacement or new log arrivals.
  • Tail pages run oldest-to-newest. Pass a non-null nextBeforeOffset as before for earlier records; an exhausted cursor returns 200 with empty records.
  • Unknown archives return 404; downloads over 64 MiB return 413 with CLOUD_LOG_DOWNLOAD_TOO_LARGE; unavailable archives return 503 with CLOUD_LOG_ARCHIVE_UNAVAILABLE.

Defaults are 2,000 buffered records per process, batches of 200, and 14-day retention. A role becomes ready after its startup marker is archived. Transient failures retry; unconfirmed write timeouts, sustained failures, or 100 dropped records make the role unhealthy for replacement.

Adjust policy with DEVICE_AGENT_CLOUD_LOG_BUFFER_RECORDS, DEVICE_AGENT_CLOUD_LOG_BATCH_SIZE, DEVICE_AGENT_CLOUD_LOG_FLUSH_INTERVAL_MS, DEVICE_AGENT_CLOUD_LOG_RETENTION_DAYS, DEVICE_AGENT_CLOUD_LOG_PRUNE_INTERVAL_MS, and DEVICE_AGENT_CLOUD_LOG_PRUNE_BATCH_SIZE. Keep all roles consistent during rollouts.

WebSocket ​

The voice channel uses /ws/voice. The connection can include these headers:

HeaderNotes
Protocol-VersionProtocol version. Current value: 3.
Device-IdCurrent device ID.
Client-IdClient ID. Defaults to device ID when omitted.

For adapters enforcing user-device bindings, external users supply userId in the URL. If hello also supplies it, values must match. The service validates and periodically rechecks bindings; unbinding closes the connection with 1008.

Adapters offering device authentication also accept URL parameters productKey, deviceKey, and sign. Generate them using that platform's device credential rules. hello.deviceId must match deviceKey; hello.productId is the Device Agent ID. Device authentication and external-user binding are distinct paths selected by the deployment. Platform administration credentials are not device signatures. Rejected authentication closes with 1008; an unavailable authentication service closes with 1011.

After connecting, send hello:

json
{
  "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:

ByteContent
0Type: 0 audio, 1 UTF-8 JSON.
1Reserved; set to 0.
2..3Payload 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:

DirectionMessageNotes
Client -> Device AgenthelloStart a voice session with audio parameters, device context, and speech provider.
Device Agent -> clienthelloReturn session ID and server audio output parameters.
Client -> Device AgentlistenSend `mode: auto
Client -> Device AgentBinary audio framesSend voice data.
Device Agent -> clientasrReturn text, definite, utterances, and taskId.
Client -> Device AgentstopSend the same taskId; may include visionRefs.
Device Agent -> clientagent_replyReturn text and taskId; streaming: true marks incremental text.
Device Agent -> clientTTS binary audio framesPlay synthesized voice reply.
Device Agent -> clienttts_completeEnd the completed or aborted turn; may include taskId.
Client -> Device AgentabortInterrupt the turn with its taskId.
Client -> Device AgentgoodbyeInclude the session_id returned by server hello.

Server hello.audio_params defines the downstream TTS format; play PCM using its sample_rate. Even with listen.mode: auto, send stop after each recording turn.

Close codes: 1000 is normal; 1008 means binding validation failed (missing or mismatched userId, or an unbound device); 1011 means voice or binding services are unavailable; 1012 means configuration reloaded and the client may reconnect.

See Voice Configuration for settings and Voice Interaction for usage.