Stream an answer as it is being written
Ask a question and receive the answer as Server-Sent Events as it is generated. The same turn as Send a message, with the same body and rules; delivery differs.
POST
This runs the same turn as Send a message, with the same request body and the same rules: one question at a time, only the owner can ask, archived conversations refuse questions, and
The question is saved and an answer message exists with
Generation has begun. The gap between
A piece of answer text. Each delta is an increment, not the text so far: append deltas in order to build the answer.
The citations the answer uses so far. A new
The answer’s final checks withdrew or rewrote the text already streamed, for example turning an answer that did not cite its sources into an abstention, or removing a link that could not be verified. Discard every
The answer finished and is saved with
Generation produced some text and then stopped early. The partial answer is saved with
Generation failed before producing any text. The answer is saved with
idempotency_key makes retries safe. The difference is delivery. The answer arrives as a text/event-stream response. See Streaming for a walkthrough.
Refusals arrive before the stream opens. Authentication, validation, permission checks and the one-turn-at-a-time claim all run first. If any of them fails, you get an ordinary HTTP error with a JSON body (400, 401, 403, 404, 409 or 429) and no stream.
This request also resolves the conversation’s assistant. If that assistant can no longer answer (it is disabled or archived, or has no knowledge attached), this request returns 400 validation_error.
If you retry with an idempotency_key whose turn has already finished, the stored turn is replayed as a stream: message.created, answer.started, the full answer text in one answer.delta, its answer.sources if it has citations, and a terminal event matching how the turn ended. The model is not asked again. A replayed turn that had failed ends with answer.error: send a new key to ask again.
If you disconnect, the answer is saved with whatever text was produced and marked interrupted, and the conversation is freed for the next question. Streams cannot be resumed and Last-Event-ID is not supported; read the answer back with List messages.
string
required
The conversation ID.
string
required
The question. 1 to 16,000 characters after trimming. Newlines and tabs are allowed; other control
characters and invisible formatting characters are rejected.
string
Your own key for this question, unique within the conversation. Up to 128 characters from
A-Z,
a-z, 0-9, _, ., : and -.object
Your own labels for the question. At most 20 entries; keys up to 64 characters matching
^[a-z][a-z0-9_.-]*$; values up to 512 characters on a single line.Response
Returns200 OK with Content-Type: text/event-stream; charset=utf-8. Each event is framed as:
idis the event’s sequence number. It starts at 1 and increases by exactly 1, so a gap means you lost an event.eventis the event type. It is the only place the type appears:datadoes not repeat it.datais a single-line JSON object. Non-ASCII characters are escaped as\uXXXX.- Ignore event types you do not recognize. New ones may be added.
message.created. Exactly one terminal event ends the stream: answer.completed, answer.interrupted or answer.error.
message.created
The question is saved and an answer message exists with status pending.
string
required
The conversation ID.
string
required
The ID of your question message.
string
required
The ID of the answer message being generated.
answer.started
Generation has begun. The gap between message.created and this event is retrieval.
string
required
The ID of the answer message.
answer.delta
A piece of answer text. Each delta is an increment, not the text so far: append deltas in order to build the answer.
string
required
The next piece of answer text.
answer.sources
The citations the answer uses so far. A new answer.sources is sent whenever the answer first cites another source, and it always arrives before the answer.delta containing that source’s [n] marker, so every marker you render can be linked immediately. Each event carries the full list, so replace your citation list rather than appending to it.
When the answer has citations, a final answer.sources with the complete list is sent just before answer.completed. If the assistant abstains after sources were sent, a final answer.sources with an empty citations list clears them.
object[]
required
answer.replaced
The answer’s final checks withdrew or rewrote the text already streamed, for example turning an answer that did not cite its sources into an abstention, or removing a link that could not be verified. Discard every answer.delta you have received and show this text instead. Later deltas, if any, append to it. The stored message holds only the final text.
string
required
The whole answer so far, replacing everything streamed before it.
answer.completed (terminal)
The answer finished and is saved with status complete.
string
required
The ID of the answer message.
string
required
answered, or abstained when the assistant declined because your knowledge did not support an
answer.boolean
required
true for an abstention. An abstention is a successful outcome, not an error; do not present its
sources as supporting an answer. See How answers work.string | null
required
Why generation stopped, as reported.
length means the answer was cut off at the output limit and
is incomplete.string | null
required
Why the assistant declined, or
null when it declined without naming a reason. Always null when
abstained is false.An open string: match the values you know and fall back to a generic notice for one you do not.
New values are not a breaking change.The widget surface receives a deliberately coarser set — see
Stream a thread message.This is a streaming field only: a message read back from
List messages carries
abstained but no reason, and a
replayed idempotent turn sends null.answer.interrupted (terminal)
Generation produced some text and then stopped early. The partial answer is saved with status interrupted. You have text to show, but it is not a complete answer.
string
required
The ID of the answer message.
string
required
Why it stopped: an error code such as
provider_failure when generation failed part-way,
incomplete_stream when generation ended without finishing, or interrupted on a replayed turn.answer.error (terminal)
Generation failed before producing any text. The answer is saved with status failed, and the conversation is free for the next question.
string
required
A stable error code, such as
provider_failure, retrieval_failure, dependency_failure or
internal_error. See Errors.string
required
A client-safe message. It never contains internal detail.
Last modified on September 26, 2026