API REFERENCE
Stream chat
On this page
/api/chatStreams an agent loop running as a durable Vercel Workflow against the session's sandbox. The agent uses sandbox-only tools (bash, read, write, grep, glob, todo, task, ask_user_question, skill, fetch) — no MCP or Composio. Requires a sandbox provisioned via POST /api/sandbox.
Related endpoints
- POST /api/chat/runs — the headless counterpart: it provisions its own session + sandbox and runs without a streaming client. Use that for tasks/cron/programmatic runs; use this for interactive, streaming turns on an existing chat.
- GET /api/chat/{chatId}/stream — reconnect to an in-progress response.
- GET /api/chat/runs/{runId} — check whether a headless run finished (status, not content).
A new turn is separate from reconnecting. If the early active-run check finds an existing run whose status is running or pending, POST returns 409 before saving the new message or starting a new workflow. Keep the rejected text and attachments locally, reconnect with GET, and let the caller decide when to submit them again. Do not queue or automatically resend them, or delete persisted chat history to recover an unaccepted submission. Later start conflicts can also return 409 after work has started; inspect chat state before retrying. An unresolved startup reservation, unknown run status, failed status lookup or failed stale-slot clear returns retryable 503 before saving the submitted turn or starting a new workflow. Preserve the text and attachments for a later manual retry.
To make a reader disconnect leave the workflow running, send ?streamLifecycle=explicit-stop-v1 on this POST and every reconnect GET. Use POST /api/chat/{chatId}/stop for explicit cancellation; aborting the local reader is not confirmation that server work stopped. Only exactly one streamLifecycle query parameter with that exact value opts in. Omitted, empty, unknown or repeated parameters retain legacy disconnect cancellation.
Authentication
x-api-key in header
Request
curl --request POST \
--url 'https://api.recoupable.dev/api/chat' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"messages": [
{
"id": "string",
"role": "user",
"parts": [
{
"type": "text",
"text": "Summarize my next release."
}
]
}
],
"chatId": "YOUR_ID",
"sessionId": "YOUR_ID"
}'Replace the YOUR_ placeholders with your values. Required query parameters are included; optional parameters are listed below.
Try it
Fill in the fields, send the request from your browser, and read the live response. The curl below updates as you type.
Kept in this browser tab only and cleared when it closes.
curl --request POST \
--url 'https://api.recoupable.dev/api/chat' \
--header 'Content-Type: application/json' \
--data '{
"messages": [
{
"id": "string",
"role": "user",
"parts": [
{
"type": "text",
"text": "Summarize my next release."
}
]
}
],
"chatId": "YOUR_ID",
"sessionId": "YOUR_ID"
}'Run this one from the terminal with the curl above.
Parameters
Query parameters
streamLifecyclestringSend streamLifecycle=explicit-stop-v1 exactly once on the initial POST and every reconnect GET to keep server work running when that reader disconnects. Only exactly one parameter with that exact value opts in. Omitting it, an empty or unknown value, or repeating the parameter retains legacy disconnect cancellation. Use authenticated POST /api/chat/{chatId}/stop for cancellation; a local reader abort is not confirmation that server work stopped. This is a query parameter, not a request-body field.
Request body required
Chat workflow request
application/json
messagesarray<UIMessage>requiredConversation history as an array of UIMessage objects, matching the Vercel AI SDK UIMessage shape. The last entry is typically the new user turn — the agent loop reads the entire array and persists the latest user message before the workflow starts.
Item properties for messages
idstringrequiredrolestring · enumrequiredValues: "user", "assistant", "system"
metadataanypartsarray<object>requiredSDK discriminated parts: text (text), file (mediaType and url), reasoning, supported tool and data-* parts. Each must satisfy the installed AI SDK schema; a type field alone is insufficient.
Example: [{"type":"text","text":"Summarize my next release."}]
minItems: 1
Item properties for parts
typestringrequiredAdditional properties
Additional keys are allowed.
chatIdstringrequiredUUID of an existing chat under sessionId. The chat's active_stream_id column is used for compare-and-set race control across concurrent requests.
format: uuid
sessionIdstringrequiredUUID of the session that owns both the chat and the sandbox. The session must have a non-null sandbox_state — call POST /api/sandbox first if not.
format: uuid
contextobjectOptional per-prompt context overrides forwarded into the agent loop.
Properties for context
contextLimitintegerUpper bound on the number of tokens / messages of conversation history the agent should consider for this prompt. Overrides the model's default for this call only.
Responses
200Server-Sent Events stream of UI message parts for the newly started workflow, compatible with the Vercel AI SDK createUIMessageStreamResponse. The x-workflow-run-id response header carries the durable workflow run ID for resume/stop operations. An existing active response is rejected with 409 instead of being returned as acceptance of the submitted new message.+
x-workflow-run-idresponse headerDurable workflow run ID. Persist alongside the chat so a later GET /api/chat/{chatId}/stream request can resume this run after a disconnect.
text/event-stream
string
400Bad request — missing required fields, invalid body, or sandbox not initialized for the session.+
application/json
statusstring · enumValues: "error"
errorstringrequiredmissing_fieldsarray<string | integer>Optional validation path for the first rejected field. String entries identify object/query fields; integer entries identify array positions. An empty path can represent validation of the path value itself. Lookup or workflow errors can omit this field.
Item properties for missing_fields
string | integer
401Missing or invalid API key.+
application/json
statusstring · enumValues: "error"
errorstringrequiredmissing_fieldsarray<string | integer>Optional validation path for the first rejected field. String entries identify object/query fields; integer entries identify array positions. An empty path can represent validation of the path value itself. Lookup or workflow errors can omit this field.
Item properties for missing_fields
string | integer
403The API key's account does not own the referenced sessionId or chatId.+
application/json
statusstring · enumValues: "error"
errorstringrequiredmissing_fieldsarray<string | integer>Optional validation path for the first rejected field. String entries identify object/query fields; integer entries identify array positions. An empty path can represent validation of the path value itself. Lookup or workflow errors can omit this field.
Item properties for missing_fields
string | integer
404Session or chat not found.+
application/json
statusstring · enumValues: "error"
errorstringrequiredmissing_fieldsarray<string | integer>Optional validation path for the first rejected field. String entries identify object/query fields; integer entries identify array positions. An empty path can represent validation of the path value itself. Lookup or workflow errors can omit this field.
Item properties for missing_fields
string | integer
409A response is already active, a concurrent request claimed the chat, or another conflict prevented this start. The early active-run rejection does not save the submitted message or start a new workflow. Later conflicts may occur after work starts. Preserve rejected text and attachments, inspect chat state and use GET /api/chat/{chatId}/stream to reconnect before a manual retry; no automatic replay is performed.+
application/json
statusstring · enumValues: "error"
errorstringrequiredmissing_fieldsarray<string | integer>Optional validation path for the first rejected field. String entries identify object/query fields; integer entries identify array positions. An empty path can represent validation of the path value itself. Lookup or workflow errors can omit this field.
Item properties for missing_fields
string | integer
500Server error, including a failed chat or session lookup. A failed lookup is not a missing-chat result.+
application/json
statusstring · enumValues: "error"
errorstringrequiredmissing_fieldsarray<string | integer>Optional validation path for the first rejected field. String entries identify object/query fields; integer entries identify array positions. An empty path can represent validation of the path value itself. Lookup or workflow errors can omit this field.
Item properties for missing_fields
string | integer
503Run state cannot be resolved safely, including an unresolved startup reservation, unknown status, failed status lookup or failed stale-slot clear. A pending startup reservation remains held. The early rejection does not save the submitted turn or start a new workflow. Keep the text and attachments for a later manual retry; no automatic replay.+
application/json
statusstring · enumValues: "error"
errorstringrequiredmissing_fieldsarray<string | integer>Optional validation path for the first rejected field. String entries identify object/query fields; integer entries identify array positions. An empty path can represent validation of the path value itself. Lookup or workflow errors can omit this field.
Item properties for missing_fields
string | integer
Full specification
Download the OpenAPI file for complete schemas, constraints, and examples.
Download research.jsonView operation source
{
"operationId": "postChat",
"summary": "Stream sandbox-driven chat (Vercel Workflow)",
"description": "Streams an agent loop running as a durable [Vercel Workflow](https://vercel.com/docs/workflow) against the session's sandbox. The agent uses sandbox-only tools (`bash`, `read`, `write`, `grep`, `glob`, `todo`, `task`, `ask_user_question`, `skill`, `fetch`) — no MCP or Composio. Requires a sandbox provisioned via [`POST /api/sandbox`](/api-reference/sandbox/create).\n\n**Related endpoints**\n- [`POST /api/chat/runs`](/api-reference/chat/runs) — the **headless** counterpart: it provisions its own session + sandbox and runs without a streaming client. Use that for tasks/cron/programmatic runs; use this for **interactive, streaming** turns on an existing chat.\n- [`GET /api/chat/{chatId}/stream`](/api-reference/chat/workflow-stream) — reconnect to an in-progress response.\n- [`GET /api/chat/runs/{runId}`](/api-reference/chat/runs-status) — check **whether** a headless run finished (status, not content).\n\nA new turn is separate from reconnecting. If the early active-run check finds an existing run whose status is running or pending, POST returns `409` before saving the new message or starting a new workflow. Keep the rejected text and attachments locally, reconnect with GET, and let the caller decide when to submit them again. Do not queue or automatically resend them, or delete persisted chat history to recover an unaccepted submission. Later start conflicts can also return `409` after work has started; inspect chat state before retrying. An unresolved startup reservation, unknown run status, failed status lookup or failed stale-slot clear returns retryable `503` before saving the submitted turn or starting a new workflow. Preserve the text and attachments for a later manual retry.\n\nTo make a reader disconnect leave the workflow running, send `?streamLifecycle=explicit-stop-v1` on this POST and every reconnect GET. Use `POST /api/chat/{chatId}/stop` for explicit cancellation; aborting the local reader is not confirmation that server work stopped. Only exactly one streamLifecycle query parameter with that exact value opts in. Omitted, empty, unknown or repeated parameters retain legacy disconnect cancellation.",
"security": [
{
"apiKeyAuth": []
}
],
"requestBody": {
"description": "Chat workflow request",
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatWorkflowRequest"
}
}
}
},
"responses": {
"200": {
"description": "Server-Sent Events stream of UI message parts for the newly started workflow, compatible with the Vercel AI SDK `createUIMessageStreamResponse`. The `x-workflow-run-id` response header carries the durable workflow run ID for resume/stop operations. An existing active response is rejected with 409 instead of being returned as acceptance of the submitted new message.",
"headers": {
"x-workflow-run-id": {
"description": "Durable workflow run ID. Persist alongside the chat so a later `GET /api/chat/{chatId}/stream` request can resume this run after a disconnect.",
"schema": {
"type": "string"
}
}
},
"content": {
"text/event-stream": {
"schema": {
"type": "string",
"description": "Server-Sent Events stream containing UI message parts"
}
}
}
},
"400": {
"description": "Bad request — missing required fields, invalid body, or sandbox not initialized for the session.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatStreamErrorResponse"
}
}
}
},
"401": {
"description": "Missing or invalid API key.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatStreamErrorResponse"
}
}
}
},
"403": {
"description": "The API key's account does not own the referenced `sessionId` or `chatId`.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatStreamErrorResponse"
}
}
}
},
"404": {
"description": "Session or chat not found.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatStreamErrorResponse"
}
}
}
},
"409": {
"description": "A response is already active, a concurrent request claimed the chat, or another conflict prevented this start. The early active-run rejection does not save the submitted message or start a new workflow. Later conflicts may occur after work starts. Preserve rejected text and attachments, inspect chat state and use GET /api/chat/{chatId}/stream to reconnect before a manual retry; no automatic replay is performed.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatStreamErrorResponse"
}
}
}
},
"500": {
"description": "Server error, including a failed chat or session lookup. A failed lookup is not a missing-chat result.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatStreamErrorResponse"
}
}
}
},
"503": {
"description": "Run state cannot be resolved safely, including an unresolved startup reservation, unknown status, failed status lookup or failed stale-slot clear. A pending startup reservation remains held. The early rejection does not save the submitted turn or start a new workflow. Keep the text and attachments for a later manual retry; no automatic replay.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ChatStreamErrorResponse"
}
}
}
}
},
"parameters": [
{
"name": "streamLifecycle",
"in": "query",
"required": false,
"description": "Send streamLifecycle=explicit-stop-v1 exactly once on the initial POST and every reconnect GET to keep server work running when that reader disconnects. Only exactly one parameter with that exact value opts in. Omitting it, an empty or unknown value, or repeating the parameter retains legacy disconnect cancellation. Use authenticated POST /api/chat/{chatId}/stop for cancellation; a local reader abort is not confirmation that server work stopped. This is a query parameter, not a request-body field.",
"schema": {
"type": "string"
},
"example": "explicit-stop-v1"
}
]
}