Stream messages using the Google Chat API

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:

  1. Start a stream (spaces.streamMessages.create): Your Chat app creates a StreamMessage in a threaded space, referencing the user interaction message it's replying to. The API displays a live progress indicator in the thread and returns a StreamMessage resource name and an initial nextChunkToken.
  2. Update the stream (appendStreamMessageContent or patch): As your Chat app generates output, it sends sequential updates using the latest nextChunkToken:
    • Call spaces.streamMessages.appendStreamMessageContent to concatenate new text chunks, append intermediate steps, add inline citations, or update progressText.
    • Call spaces.streamMessages.patch to replace the streamed content chunks or update progressText in place.
  3. Finalize or discard the stream (commit or delete):
    • Call spaces.streamMessages.commit to finalize the stream and convert it into a permanent Message resource. You can also attach commit-only elements such as cards, accessory widgets, and footer sources when committing.
    • Call spaces.streamMessages.delete to discard an active stream without saving a message in the space.

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:
  • 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-auth
    from 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 as spaces/{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 as referencedMessage.
  • 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 as MARKUP_SYNTAX_MARKDOWN or MARKUP_SYNTAX_CHAT (defaults to the Chat app's configured syntax if MARKUP_SYNTAX_UNSPECIFIED). Set MARKUP_SYNTAX_MARKDOWN if you plan to include inline citations.
  • streamContentChunks (Optional): Initial text, step, or citation chunks 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 nextChunkToken in chunkToken. Each successful call returns a new nextChunkToken that you must pass on your next update or commit request.
  • text chunks concatenate directly onto previously streamed text. Include spaces or newlines where needed between chunks.
  • step chunks append to the list of intermediate steps shown on the message.
  • progressText is optional. If you omit progressText or set it to "", Chat keeps displaying the previously set progressText.

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 updateMask to a comma-separated list of fields to update: progressText, streamContentChunks, or progressText,streamContentChunks.
  • Include the latest nextChunkToken inside the StreamMessage request body, and record the new nextChunkToken returned in the response.
  • When streamContentChunks is in updateMask, the provided chunks replace previously streamed content instead of appending to it.
  • When progressText is in updateMask, progressText must 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 streamContentChunks in the commit request, including commit-only chunk types (card, accessoryWidget, and citedSource).
  • If you already streamed non-empty content in earlier calls, you can omit streamContentChunks when 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:

  1. Signal your background worker to stop generating further tokens or running downstream tools for streamMessage.
  2. Resolve the active stream using one of the following approaches:
    • Keep partial output: Call spaces.streamMessages.commit with the latest chunkToken (optionally appending a note such as "\n\n_Generation stopped._").
    • Discard the reply: Call spaces.streamMessages.delete so nothing is saved to the thread.
  3. Return an empty HTTP 200 OK JSON 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 StreamMessage times out 5 minutes after creation. Your Chat app must call spaces.streamMessages.commit or spaces.streamMessages.delete within 5 minutes of starting the stream.