Messages & Streaming
This page covers the core chat loop: sending a user message, getting the agent's reply (optionally streamed), executing any actions the reply asks for, and reporting the results back so the agent can continue.
Actions in a reply are wire envelopes of { id, type, payload } — see Agent Actions for the full set of action types and payload shapes.
Send a message
POST /agent/message
Auth: required.
Body
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | The user's message text. |
sessionId | string | No | Existing session to continue. If omitted, a new session is created. |
context | object | No | Opaque context object describing what's currently visible in the app — required for the agent to reason about the UI. context.currentScreen?: string is a common and recommended field. |
mcpVersion | string | No | Version of your MCP document, for correlating the reply with a specific app build. |
Response — 200
{
"success": true,
"data": {
"sessionId": "...",
"message": {
"id": "...",
"role": "assistant",
"content": "...",
"summary": "...",
"actions": [
{ "id": "...", "type": "navigate", "payload": { ... } }
]
},
"usage": {
"tokensUsed": 0,
"modelUsed": "...",
"latencyMs": 0
}
}
}
A request that fails body validation (e.g. missing content) returns 422 with error.code: "VALIDATION_ERROR".
curl
curl https://api.appilots.com/api/v1/agent/message \
-X POST \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{
"content": "Take me to my profile",
"sessionId": "SESSION_ID",
"context": { "currentScreen": "Home" }
}'
TypeScript
const res = await fetch("https://api.appilots.com/api/v1/agent/message", {
method: "POST",
headers: {
Authorization: "Bearer ak_xxxxxxxxxxxx...",
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "Take me to my profile",
sessionId,
context: { currentScreen: "Home" },
}),
});
const { data } = await res.json();
Stream a message
POST /agent/message/stream
Same request body as POST /agent/message, but the response is Content-Type: text/event-stream (Server-Sent Events) instead of a single JSON body — use this to show the assistant's reply incrementally as it's generated.
Auth: required.
Events
Each SSE frame is a data: {...} line containing JSON with a type field:
type | Fires | Payload |
|---|---|---|
session | Once, early | { "type": "session", "sessionId": "..." } |
text-delta | Repeated | { "type": "text-delta", "content": "..." } — incremental text; concatenate across events to build the full reply. |
tool-call | Zero or more | { "type": "tool-call", "action": { "id", "type", "payload" } } |
done | Once, terminal | { "type": "done", "messageId": "...", "usage?": { "tokensUsed": 0 } } — the stream ends after this event. |
curl
curl https://api.appilots.com/api/v1/agent/message/stream \
-X POST \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..." \
-H "Content-Type: application/json" \
-N \
-d '{
"content": "Take me to my profile",
"sessionId": "SESSION_ID",
"context": { "currentScreen": "Home" }
}'
TypeScript
const res = await fetch("https://api.appilots.com/api/v1/agent/message/stream", {
method: "POST",
headers: {
Authorization: "Bearer ak_xxxxxxxxxxxx...",
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "Take me to my profile",
sessionId,
context: { currentScreen: "Home" },
}),
});
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
for (const line of buffer.split("\n\n")) {
if (!line.startsWith("data: ")) continue;
const event = JSON.parse(line.slice(6));
// handle event.type: "session" | "text-delta" | "tool-call" | "done"
}
}
Error behavior: if a connection error occurs before any SSE event is emitted, the endpoint falls back to a plain JSON error body (using the standard error envelope) instead of an SSE stream. Treat "connection error before any event" as safe to retry via the non-streaming POST /agent/message endpoint.
React Native's fetch doesn't support incremental reads, so the Appilots SDK uses XMLHttpRequest internally for this endpoint on RN. If you're integrating from a browser, Node, or curl, a normal streaming fetch/EventSource-style read works fine.
Continue a turn
POST /agent/continue
After your client executes the actions from a previous /agent/message (or /agent/message/stream) response, call this endpoint with the results to get the agent's next step. A single user turn can span multiple hops: keep calling /agent/continue and executing any new actions it returns as long as the response says the turn isn't done yet.
Auth: required.
Body
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | Yes | The session this turn belongs to. |
results | array | Yes | Outcomes of the actions you just executed: { actionId, type, success, error?, summary? }. |
context | object | Yes | Same opaque UI context object as /agent/message. |
hop | number | No | Start at 1 for the first continuation after the initial message, increment by 1 each subsequent call within the same turn. |
Response — 200
{
"success": true,
"data": {
"sessionId": "...",
"message": {
"id": "...",
"role": "assistant",
"content": "...",
"actions": [ { "id": "...", "type": "...", "payload": { ... } } ]
},
"usage": { "tokensUsed": 0, "modelUsed": "...", "latencyMs": 0 },
"isDone": false,
"hop": 2
}
}
Keep calling /agent/continue while isDone is false and actions is non-empty. A turn has a bounded number of steps; once the server decides no further hops are useful, it returns isDone: true even without a final natural stopping point — treat that as the signal to stop the loop regardless of whether actions is empty.
A request that fails body validation returns 422 (VALIDATION_ERROR). A sessionId that doesn't exist or belongs to a different project returns 404 (NOT_FOUND).
curl
curl https://api.appilots.com/api/v1/agent/continue \
-X POST \
-H "Authorization: Bearer ak_xxxxxxxxxxxx..." \
-H "Content-Type: application/json" \
-d '{
"sessionId": "SESSION_ID",
"hop": 1,
"results": [
{ "actionId": "ACTION_ID", "type": "navigate", "success": true }
],
"context": { "currentScreen": "Profile" }
}'
TypeScript
let hop = 1;
let isDone = false;
while (!isDone) {
const res = await fetch("https://api.appilots.com/api/v1/agent/continue", {
method: "POST",
headers: {
Authorization: "Bearer ak_xxxxxxxxxxxx...",
"Content-Type": "application/json",
},
body: JSON.stringify({
sessionId,
hop,
results: [{ actionId: "ACTION_ID", type: "navigate", success: true }],
context: { currentScreen: "Profile" },
}),
});
const { data } = await res.json();
isDone = data.isDone;
hop += 1;
// execute data.message.actions, then loop with their results
}
Related pages
- Agent Actions — action types and payload shapes
- Actions — reporting individual action outcomes outside the
/agent/continueloop - Errors — full error code reference