Chat API
Create responses, stream SSE, upload files, read conversations, and submit feedback.
The examples use $ASSISTANT_API_URL as the base URL. Every endpoint requires a Bearer key with the corresponding scope.
Create a chat response
POST /v1/chat/responses · scope chat:write
{
"external_user_id": "customer_42",
"input": "Summarize your return policy.",
"conversation_id": "a12d5e41-7ab7-4c8f-b024-c2c38b5c43f4",
"file_ids": ["8d50c5c2-bf85-48a4-b252-8dc97410452c"],
"model": "optional-model-code",
"stream": false
}| Field | Required | Limits and meaning |
|---|---|---|
external_user_id | Yes | 1–256 characters; a stable identity in this assistant's scope. |
input | Yes | 1–100,000 characters. |
conversation_id | No | UUID owned by the same external user. Omit to create a conversation. |
file_ids | No | Up to 10 UUIDs uploaded by the same external user. |
model | No | Model override; accepted only when the owner's plan allows it. |
stream | No | Defaults to false; set true for SSE. |
A non-streaming request returns JSON with 200. Retrying a completed request with the same idempotency key returns the stored response metadata with 200 and the header Idempotent-Replayed: true; tokens are never replayed. Idempotency-Key accepts up to 255 characters.
Stream with SSE
curl -N "$ASSISTANT_API_URL/v1/chat/responses" \
--request POST \
--header "Authorization: Bearer $ASSISTANT_API_KEY" \
--header "Accept: text/event-stream" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: stream-customer-42-001" \
--data "{
\"external_user_id\": \"$EXTERNAL_USER_ID\",
\"input\": \"Explain the setup steps.\",
\"stream\": true
}"Each event has this form:
event: response.output_text.delta
data: {"type":"response.output_text.delta","sequence":3,"response_id":"resp_...","delta":"Hello"}
response.created is first. sequence increases monotonically; the stream ends with exactly one response.completed or error. A : heartbeat comment can appear about every 15 seconds and has no sequence.
| Event | Main content |
|---|---|
response.created | The response started. |
conversation.created | A new conversation_id. |
response.output_text.delta | Text in delta. |
response.reasoning.delta | Reasoning delta when provided by the model. |
response.tool_call.started | A tool call started. |
response.tool_call.completed | A tool call completed. |
response.file.created | A file created by the response. |
response.completed | Successful terminal event with message_id. |
error | Error terminal event. |
Your parser must join deltas by sequence and handle Unicode split across network chunks. Do not assume one TCP chunk contains one complete SSE event.
Node.js/TypeScript parser example
This example retains a buffer across network chunks and uses a streaming TextDecoder so split Unicode remains valid:
const response = await fetch(`${process.env.ASSISTANT_API_URL}/v1/chat/responses`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ASSISTANT_API_KEY}`,
Accept: "text/event-stream",
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
external_user_id: "customer_42",
input: "Explain the setup steps.",
stream: true,
}),
})
if (!response.ok || !response.body) {
throw new Error(`Chat request failed: ${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ""
for (;;) {
const { value, done } = await reader.read()
buffer += decoder.decode(value, { stream: !done })
let boundary
while ((boundary = buffer.indexOf("\n\n")) >= 0) {
const block = buffer.slice(0, boundary)
buffer = buffer.slice(boundary + 2)
if (!block || block.startsWith(":")) continue
const data = block
.split("\n")
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).trimStart())
.join("\n")
if (!data) continue
const event = JSON.parse(data)
if (event.type === "response.output_text.delta") {
process.stdout.write(event.delta)
} else if (event.type === "error") {
throw new Error(event.message ?? "Stream failed")
}
}
if (done) break
}Idempotency
Idempotency-Key is optional but recommended for every chat-creation request:
- Use one unique key per logical operation.
- Same key and body while running returns
409 REQUEST_IN_PROGRESS. - Same key with a different body returns
409 IDEMPOTENCY_CONFLICT. - Completed metadata is retained for 24 hours; an in-progress lease expires after 10 minutes.
- Retrying a stream does not replay tokens. The replay returns
idandmessage_id; fetch the text withGET /v1/responses/{id}.
Retrieve a response
GET /v1/responses/{response_id} · scope chat:write
curl "$ASSISTANT_API_URL/v1/responses/$RESPONSE_ID?external_user_id=$EXTERNAL_USER_ID" \
--header "Authorization: Bearer $ASSISTANT_API_KEY"Use it to recover the result after an SSE connection drops. The response becomes available once it finishes; until then the API returns 404 RESPONSE_NOT_FOUND. status is completed, failed, or incomplete (the client disconnected before completion).
Upload a file
POST /v1/files · scope files:write · multipart/form-data
curl "$ASSISTANT_API_URL/v1/files" \
--request POST \
--header "Authorization: Bearer $ASSISTANT_API_KEY" \
--form "external_user_id=$EXTERNAL_USER_ID" \
--form "file=@./policy.pdf"{
"id": "8d50c5c2-bf85-48a4-b252-8dc97410452c",
"object": "file",
"filename": "policy.pdf",
"bytes": 182400,
"mime_type": "application/pdf"
}The maximum size is 25 MiB. Supported types are PDF, DOCX, XLSX, CSV, TXT, and image MIME types. Pass the returned id in file_ids for the same external user.
Read a conversation
GET /v1/conversations/{conversation_id} · scope conversations:read
curl "$ASSISTANT_API_URL/v1/conversations/$CONVERSATION_ID?external_user_id=$EXTERNAL_USER_ID" \
--header "Authorization: Bearer $ASSISTANT_API_KEY"The API returns 404 CONVERSATION_NOT_FOUND when the assistant or external user does not own the conversation.
Paginate messages
GET /v1/conversations/{conversation_id}/messages · scope conversations:read
curl "$ASSISTANT_API_URL/v1/conversations/$CONVERSATION_ID/messages?external_user_id=$EXTERNAL_USER_ID&limit=50" \
--header "Authorization: Bearer $ASSISTANT_API_KEY"limit accepts 1–100 and defaults to 50. Messages are ordered oldest first. When has_more is true, send last_id as the next request's after query parameter. A cursor from another conversation returns 400 INVALID_CURSOR.
{
"object": "list",
"data": [
{
"id": "b0f16c77-a275-46c8-9dc8-8fe9fd5991cc",
"object": "message",
"role": "assistant",
"content": "Response content...",
"created_at": "2026-08-13T08:30:00Z"
}
],
"has_more": true,
"first_id": "b0f16c77-a275-46c8-9dc8-8fe9fd5991cc",
"last_id": "b0f16c77-a275-46c8-9dc8-8fe9fd5991cc"
}Submit feedback
POST /v1/messages/{message_id}/feedback · scope feedback:write
curl "$ASSISTANT_API_URL/v1/messages/$MESSAGE_ID/feedback" \
--request POST \
--header "Authorization: Bearer $ASSISTANT_API_KEY" \
--header "Content-Type: application/json" \
--data "{\"external_user_id\":\"$EXTERNAL_USER_ID\",\"rating\":\"up\"}"rating accepts only up or down. Success returns 204 No Content.
Delete an end user
DELETE /v1/users/{external_user_id} · scope users:delete
curl "$ASSISTANT_API_URL/v1/users/$EXTERNAL_USER_ID" \
--request DELETE \
--header "Authorization: Bearer $ASSISTANT_API_KEY"Permanently erases the external user's conversations, messages, memories, uploaded files, and usage rows for this assistant. Use it for account deletion and data-erasure requests. The call is idempotent and returns 204 No Content, including for users that never existed. Billing records of the assistant owner are not affected.