This guide explains how to stream messages incrementally in Google Chat using
the StreamMessage resource in the Chat API. Streaming lets your
Google Chat app display live progress indicators, intermediate
reasoning or tool-execution steps, and incremental text responses before
finalizing a message.
Streaming is designed for Chat apps powered by generative AI or long-running backend workflows where generating a complete response takes several seconds or minutes.
How message streaming works
A streaming message progresses through an active streaming state before
becoming a standard
Message
resource in a space:
- Start a stream (
spaces.streamMessages.create): Your Chat app creates aStreamMessagein a threaded space, referencing the user interaction message it's replying to. The API displays a live progress indicator in the thread and returns aStreamMessageresource name and an initialnextChunkToken. - Update the stream (
appendStreamMessageContentorpatch): As your Chat app generates output, it sends sequential updates using the latestnextChunkToken:- Call
spaces.streamMessages.appendStreamMessageContentto concatenate new text chunks, append intermediate steps, add inline citations, or updateprogressText. - Call
spaces.streamMessages.patchto replace the streamed content chunks or updateprogressTextin place.
- Call
- Finalize or discard the stream (
commitordelete):- Call
spaces.streamMessages.committo finalize the stream and convert it into a permanentMessageresource. You can also attach commit-only elements such as cards, accessory widgets, and footer sources when committing. - Call
spaces.streamMessages.deleteto discard an active stream without saving a message in the space.
- Call
While a stream is active, users in the space can click Stop to cancel generation. Chat sends a cancellation system command event to your Chat app so you can stop backend work and either commit partial output or delete the stream.
Prerequisites
To stream messages with the Chat API, complete the following prerequisites:
- A Business or Enterprise Google Workspace account with access to Google Chat.
- Set up your environment:
- Create a Google Cloud project.
- Enable and configure the Google Chat API with a name, icon, and description for your Chat app.
- Create service account credentials and save them as
credentials.json. Message streaming requires app authentication with thehttps://www.googleapis.com/auth/chat.botauthorization scope; user authentication isn't supported.
- Add your Chat app to a threaded
Chat space (or a 1:1 direct message that already contains at
least one message), and obtain the resource name of an accessible message
to reply to (
spaces/SPACE_ID/messages/MESSAGE_ID). For Python samples, install the Google API Client Library for Python and
google-auth, and initialize the Chat API service client:pip install google-api-python-client google-authfrom google.oauth2 import service_account from googleapiclient.discovery import build SCOPES = ["https://www.googleapis.com/auth/chat.bot"] creds = service_account.Credentials.from_service_account_file( "credentials.json", scopes=SCOPES ) chat = build("chat", "v1", credentials=creds)
Start a stream
To start streaming a reply, call spaces.streamMessages.create and pass a
StreamMessage in the request body:
referencedMessage(Required): Resource name of the message that triggered the response, formatted asspaces/{space}/messages/{message}. The referenced message must belong to the same space and be accessible to your Chat app. Chat posts the streamed reply in the same thread asreferencedMessage.progressText(Required): Non-empty plain-text status label (up to 250 characters) shown next to the progress indicator, such as"Thinking...".markupSyntax(Optional): Text formatting syntax for the message, such asMARKUP_SYNTAX_MARKDOWNorMARKUP_SYNTAX_CHAT(defaults to the Chat app's configured syntax ifMARKUP_SYNTAX_UNSPECIFIED). SetMARKUP_SYNTAX_MARKDOWNif you plan to include inline citations.streamContentChunks(Optional): Initialtext,step, orcitationchunks to display immediately.
Save both name (spaces/{space}/streamMessages/{stream_message}) and
nextChunkToken from the response for subsequent calls.
HTTP
POST https://chat.googleapis.com/v1/spaces/SPACE_ID/streamMessages HTTP/1.1
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"referencedMessage": "spaces/SPACE_ID/messages/MESSAGE_ID",
"progressText": "Thinking...",
"markupSyntax": "MARKUP_SYNTAX_MARKDOWN"
}
Python
stream = (
chat.spaces()
.streamMessages()
.create(
parent="spaces/SPACE_ID",
body={
"referencedMessage": "spaces/SPACE_ID/messages/MESSAGE_ID",
"progressText": "Thinking...",
"markupSyntax": "MARKUP_SYNTAX_MARKDOWN",
},
)
.execute()
)
stream_name = stream["name"]
chunk_token = stream["nextChunkToken"]
The method returns a StreamMessage resource:
{
"name": "spaces/SPACE_ID/streamMessages/STREAM_MESSAGE_ID",
"progressText": "Thinking...",
"markupSyntax": "MARKUP_SYNTAX_MARKDOWN",
"nextChunkToken": "INITIAL_CHUNK_TOKEN"
}
Append content chunks
As your Chat app produces intermediate steps or text
tokens, call spaces.streamMessages.appendStreamMessageContent:
- Pass the latest
nextChunkTokeninchunkToken. Each successful call returns a newnextChunkTokenthat you must pass on your next update or commit request. textchunks concatenate directly onto previously streamed text. Include spaces or newlines where needed between chunks.stepchunks append to the list of intermediate steps shown on the message.progressTextis optional. If you omitprogressTextor set it to"", Chat keeps displaying the previously setprogressText.
HTTP
POST https://chat.googleapis.com/v1/spaces/SPACE_ID/streamMessages/STREAM_MESSAGE_ID:appendStreamMessageContent HTTP/1.1
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"chunkToken": "INITIAL_CHUNK_TOKEN",
"progressText": "Searching documentation...",
"streamContentChunks": [
{
"step": {
"title": "Search knowledge base",
"description": "Found 3 matching articles."
}
},
{
"text": "See <chat-citation data-id=\"c1\">guide</chat-citation>:\n"
},
{
"citation": {
"id": "c1",
"citedSources": [
{
"title": "System Architecture Guide",
"uri": "https://example.com/docs/arch"
}
]
}
}
]
}
Python
stream = (
chat.spaces()
.streamMessages()
.appendStreamMessageContent(
name=stream_name,
body={
"chunkToken": chunk_token,
"progressText": "Searching documentation...",
"streamContentChunks": [
{
"step": {
"title": "Search knowledge base",
"description": "Found 3 matching articles.",
}
},
{
"text": (
'See <chat-citation data-id="c1">'
"guide</chat-citation>:\n"
)
},
{
"citation": {
"id": "c1",
"citedSources": [
{
"title": "System Architecture Guide",
"uri": "https://example.com/docs/arch",
}
],
}
},
],
},
)
.execute()
)
chunk_token = stream["nextChunkToken"]
Replace streamed content or progress text
If you need to rewrite previously streamed content—for example, after running a
post-processing or safety filter—or update only progressText without
appending chunks, call spaces.streamMessages.patch:
- Set
updateMaskto a comma-separated list of fields to update:progressText,streamContentChunks, orprogressText,streamContentChunks. - Include the latest
nextChunkTokeninside theStreamMessagerequest body, and record the newnextChunkTokenreturned in the response. - When
streamContentChunksis inupdateMask, the provided chunks replace previously streamed content instead of appending to it. When
progressTextis inupdateMask,progressTextmust be non-empty plain text up to 250 characters.
HTTP
PATCH https://chat.googleapis.com/v1/spaces/SPACE_ID/streamMessages/STREAM_MESSAGE_ID?updateMask=progressText,streamContentChunks HTTP/1.1
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"nextChunkToken": "LATEST_CHUNK_TOKEN",
"progressText": "Formatting response...",
"streamContentChunks": [
{
"text": "Updated response text."
}
]
}
Python
stream = (
chat.spaces()
.streamMessages()
.patch(
name=stream_name,
updateMask="progressText,streamContentChunks",
body={
"nextChunkToken": chunk_token,
"progressText": "Formatting response...",
"streamContentChunks": [
{"text": "Updated response text."}
],
},
)
.execute()
)
chunk_token = stream["nextChunkToken"]
Finalize a stream
When generation finishes, call spaces.streamMessages.commit with the latest
chunkToken to finalize the stream into a permanent
Message
resource:
- You can optionally include final
streamContentChunksin the commit request, including commit-only chunk types (card,accessoryWidget, andcitedSource). If you already streamed non-empty content in earlier calls, you can omit
streamContentChunkswhen committing. The final committed message can't be empty.
HTTP
POST https://chat.googleapis.com/v1/spaces/SPACE_ID/streamMessages/STREAM_MESSAGE_ID:commit HTTP/1.1
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"chunkToken": "LATEST_CHUNK_TOKEN",
"streamContentChunks": [
{
"text": "All checks passed."
},
{
"citedSource": {
"title": "Deployment Runbook",
"uri": "https://example.com/docs/runbook"
}
},
{
"card": {
"cardId": "details-card",
"card": {
"header": {
"title": "Build Status"
},
"sections": [
{
"widgets": [
{
"textParagraph": {
"text": "All 42 checks succeeded."
}
}
]
}
]
}
}
}
]
}
Python
status_card = {
"cardId": "details-card",
"card": {
"header": {"title": "Build Status"},
"sections": [
{
"widgets": [
{"textParagraph": {"text": "All 42 checks succeeded."}}
]
}
],
},
}
commit_response = (
chat.spaces()
.streamMessages()
.commit(
name=stream_name,
body={
"chunkToken": chunk_token,
"streamContentChunks": [
{"text": "All checks passed."},
{
"citedSource": {
"title": "Deployment Runbook",
"uri": "https://example.com/docs/runbook",
}
},
{"card": status_card},
],
},
)
.execute()
)
final_message = commit_response["message"]
print(f"Committed message: {final_message['name']}")
Discard an active stream
If an unrecoverable error occurs or a user cancels generation and you don't
want to leave a partial reply in the thread, call
spaces.streamMessages.delete:
HTTP
DELETE https://chat.googleapis.com/v1/spaces/SPACE_ID/streamMessages/STREAM_MESSAGE_ID HTTP/1.1
Authorization: Bearer ACCESS_TOKEN
Python
chat.spaces().streamMessages().delete(name=stream_name).execute()
Once a stream is committed into a Message, call
spaces.messages.delete instead if you need
to delete the finalized message.
Supported content chunk types
Each entry in streamContentChunks specifies one field in the
StreamContentChunk union. Some chunk types can be sent throughout the stream,
while others are accepted only in spaces.streamMessages.commit:
| Chunk field | Supported methods | Final Message field |
Notes |
|---|---|---|---|
text |
create, appendStreamMessageContent,
patch, commit |
text |
Incremental text. Concatenated on
appendStreamMessageContent; replaced on patch.
Final committed text can't exceed 32,000 bytes. |
step |
create, appendStreamMessageContent,
patch, commit |
elements.steps.steps[] |
Intermediate processing step with plain-text title
(required) and optional description. |
citation |
create, appendStreamMessageContent,
patch, commit |
elements.citations[] |
Inline
Citation with an alphanumeric id (under 63
characters, underscores allowed) and citedSources. Requires
markupSyntax: "MARKUP_SYNTAX_MARKDOWN" and matching
<chat-citation data-id="ID">...</chat-citation>
tags in text. Sending a citation with an
existing id overwrites the earlier citation. |
card |
commit only |
cardsV2[] |
Interactive or informational
CardWithId card. |
accessoryWidget |
commit only |
accessoryWidgets[] |
AccessoryWidget buttons displayed below the message,
such as feedback buttons. |
citedSource |
commit only |
elements.citedSources[] |
CitedSource footer link displayed below the message
without inline citation tags. |
Handle user-initiated stream cancellation
While a stream is active, users in the space can click Stop on the
streaming message. When a user stops a stream, Chat sends a
system command interaction event containing cancelStream.streamMessage (the
spaces/{space}/streamMessages/{stream_message} resource name of the stopped
stream) to your Chat app endpoint.
The structure of the incoming JSON payload depends on whether you build your Chat app as a Google Workspace add-on or using Chat API interaction events:
Google Workspace add-ons
{
"commonEventObject": {
"hostApp": "CHAT"
},
"chat": {
"user": {
"name": "users/USER_ID"
},
"space": {
"name": "spaces/SPACE_ID"
},
"systemCommandPayload": {
"systemCommandMetadata": {
"cancelStream": {
"streamMessage": "spaces/SPACE_ID/streamMessages/STREAM_ID"
}
}
}
}
}
Google Chat API
{
"type": "SYSTEM_COMMAND",
"space": {
"name": "spaces/SPACE_ID"
},
"user": {
"name": "users/USER_ID"
},
"systemCommandMetadata": {
"cancelStream": {
"streamMessage": "spaces/SPACE_ID/streamMessages/STREAM_ID"
}
}
}
When your Chat app receives a cancelStream event:
- Signal your background worker to stop generating further tokens or running
downstream tools for
streamMessage. - Resolve the active stream using one of the following approaches:
- Keep partial output: Call
spaces.streamMessages.commitwith the latestchunkToken(optionally appending a note such as"\n\n_Generation stopped._"). - Discard the reply: Call
spaces.streamMessages.deleteso nothing is saved to the thread.
- Keep partial output: Call
- Return an empty HTTP
200 OKJSON response ({}) from your interaction event handler.
Limitations
Message streaming has the following limitations:
- Platform availability: Message streaming is visible only in Chat on the web. Support for Android and iOS is coming soon.
- Stream timeout: An active
StreamMessagetimes out 5 minutes after creation. Your Chat app must callspaces.streamMessages.commitorspaces.streamMessages.deletewithin 5 minutes of starting the stream.