# Stream chat

Source: https://docs.recoupable.dev/api-reference/chat/workflow

Streams an agent loop running as a durable Vercel Workflow against the session's sandbox.

## POST /api/chat

Full OpenAPI specification: https://docs.recoupable.dev/spec/research.json

## Authentication

This operation requires one of the security alternatives in the specification below. Security scheme definitions are included where present in the published specification.

[Authentication guide](https://docs.recoupable.dev/authentication)

## Operation and referenced schemas

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Recoup API - Research",
    "description": "API documentation for the Recoup platform - an AI agent platform for the music industry",
    "license": {
      "name": "MIT"
    },
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.recoupable.dev"
    }
  ],
  "paths": {
    "/api/chat": {
      "post": {
        "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"
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "ChatWorkflowRequest": {
        "type": "object",
        "description": "Request body for the sandbox-driven, Workflow-backed chat endpoint. Requires an existing session, chat, and provisioned sandbox. The Bearer key from the `Authorization` header is forwarded into the sandbox for skill-based callbacks to the Recoup API — no separate access-token field needed.",
        "required": [
          "messages",
          "chatId",
          "sessionId"
        ],
        "properties": {
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UIMessage"
            },
            "description": "Conversation history as an array of `UIMessage` objects, matching the [Vercel AI SDK](https://sdk.vercel.ai/) `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."
          },
          "chatId": {
            "type": "string",
            "format": "uuid",
            "description": "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."
          },
          "sessionId": {
            "type": "string",
            "format": "uuid",
            "description": "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](/api-reference/sandbox/create) first if not."
          },
          "context": {
            "type": "object",
            "description": "Optional per-prompt context overrides forwarded into the agent loop.",
            "properties": {
              "contextLimit": {
                "type": "integer",
                "description": "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."
              }
            }
          }
        }
      },
      "ChatStreamErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "error"
            ]
          },
          "error": {
            "type": "string"
          },
          "missing_fields": {
            "type": "array",
            "items": {
              "type": [
                "string",
                "integer"
              ]
            },
            "description": "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."
          }
        }
      },
      "UIMessage": {
        "type": "object",
        "required": [
          "id",
          "role",
          "parts"
        ],
        "description": "AI SDK UIMessage. Parts are structurally validated before provisioning for headless runs. Legacy content-only messages are rejected.",
        "properties": {
          "id": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "user",
              "assistant",
              "system"
            ]
          },
          "metadata": {},
          "parts": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "required": [
                "type"
              ],
              "properties": {
                "type": {
                  "type": "string"
                }
              },
              "additionalProperties": true
            },
            "description": "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."
              }
            ]
          }
        }
      }
    },
    "securitySchemes": {
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Your Recoup API key. [Learn more](/quickstart#api-keys)."
      }
    }
  }
}
```
