Recoup

Get started with Recoup.

API REFERENCE

Stream chat

On this page
POST/api/chat

Streams 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
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 for this request
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

streamLifecyclestring

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.

Request body required

Chat workflow request

application/json

messagesarray<UIMessage>required

Conversation 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
idstringrequired
rolestring · enumrequired

Values: "user", "assistant", "system"

metadataany
partsarray<object>required

SDK 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
typestringrequired
Additional properties

Additional keys are allowed.

chatIdstringrequired

UUID 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

sessionIdstringrequired

UUID 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

contextobject

Optional per-prompt context overrides forwarded into the agent loop.

Properties for context
contextLimitinteger

Upper 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 header

Durable 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 · enum

Values: "error"

errorstringrequired
missing_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 · enum

Values: "error"

errorstringrequired
missing_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 · enum

Values: "error"

errorstringrequired
missing_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 · enum

Values: "error"

errorstringrequired
missing_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 · enum

Values: "error"

errorstringrequired
missing_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 · enum

Values: "error"

errorstringrequired
missing_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 · enum

Values: "error"

errorstringrequired
missing_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.json
View operation source
json
{
  "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"
    }
  ]
}