Assistant CoreAssistant Core
Developer API

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
}
FieldRequiredLimits and meaning
external_user_idYes1–256 characters; a stable identity in this assistant's scope.
inputYes1–100,000 characters.
conversation_idNoUUID owned by the same external user. Omit to create a conversation.
file_idsNoUp to 10 UUIDs uploaded by the same external user.
modelNoModel override; accepted only when the owner's plan allows it.
streamNoDefaults 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.

EventMain content
response.createdThe response started.
conversation.createdA new conversation_id.
response.output_text.deltaText in delta.
response.reasoning.deltaReasoning delta when provided by the model.
response.tool_call.startedA tool call started.
response.tool_call.completedA tool call completed.
response.file.createdA file created by the response.
response.completedSuccessful terminal event with message_id.
errorError 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 id and message_id; fetch the text with GET /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.

On this page