DOCS
Dashboardnorth_eastGet an API keyvpn_key
PAI Chat·Streaming responses

Streaming responses

Sending a message streams the assistant's reply back token by token over Server-Sent Events (text/event-stream). Read the stream incrementally to show text as it is generated rather than waiting for the whole reply.

Sending a message

POST the message to a chat session. Include x-api-key and x-chatbot-id. Optional body fields include documentIds, imageIds, and an image object for image generation. Use curl -N to disable buffering while testing.

cURL
curl -N https://chat-api-dev.paicloud.ai//message/CHAT_ID \
  -H "x-api-key: $PAI_CHAT_API_KEY" \
  -H "x-chatbot-id: YOUR_CHATBOT_ID" \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello"}'

Response stream

The response opens with SSE headers and emits data: lines, each a JSON object, separated by a blank line. Text arrives as { "content": "…" } chunks; concatenate them in order. The stream ends with a literal data: [DONE].

SSE stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
X-Accel-Buffering: no

data: {"content": "Hel"}

data: {"content": "lo, how can I help?"}

data: [DONE]

Event shapes

Most frames carry text. Some carry a type for non-text events — attached files and email delivery. Ignore any type you do not handle. Always stop at [DONE].

Event types
# Text tokens — append content as it arrives
data: {"content": "partial text…"}

# Generated files attached to the reply
data: {"type": "files", "files": [ ... ]}

# Files sent to the user by email
data: {"type": "send_files_to_email", ...}

# Terminator — stop reading
data: [DONE]

Parsing the stream

Read the body as a stream, buffer bytes, split on the blank-line frame separator, strip the data: prefix, and JSON.parse each frame unless it is [DONE].

Node
const res = await fetch(`${BASE_URL}/message/${chatId}`, {
  method: 'POST',
  headers: {
    'x-api-key': process.env.PAI_CHAT_API_KEY,
    'x-chatbot-id': process.env.PAI_CHATBOT_ID,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ message: 'Hello' }),
})

const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let answer = ''

while(true) {
  const { value, done } = await reader.read()
  if (done) break
  buffer += decoder.decode(value, { stream: true })

  // SSE frames are separated by a blank line
  const frames = buffer.split('\n\n')
  buffer = frames.pop() ?? ''

  for (const frame of frames) {
    const line = frame.replace(/^data: /, '').trim()
    if (!line || line === '[DONE]') continue
    const event = JSON.parse(line)
    if (event.content) answer += event.content
    // else handle event.type === 'files' | 'send_files_to_email'
  }
}
bolt
Do not buffer the whole response. Proxies should pass text/event-stream through untouched — the API sets X-Accel-Buffering: no and Cache-Control: no-transform to prevent buffering.

See Authentication for headers and Error codes for non-stream failures (a 4xx/5xx is returned before the stream opens).