Skip to navigation

Streaming

Subscribe to thread updates using bidirectional gRPC streaming or Server-Sent Events (SSE).

Overview

The Thread API provides two methods for streaming real-time thread events: gRPC bidirectional streaming and REST API Server-Sent Events (SSE). Both methods deliver incremental message chunks, artifacts, and completion events as they occur during thread execution.

Choose the method that best fits your infrastructure:

  • gRPC streaming
  • SSE streaming

Server-Sent Events (SSE)

The SSE endpoint (GET /v2/threads/stream/{thread_id}) establishes a persistent HTTP connection that streams events as they occur. Open the stream connection before calling POST /v2/threads/{thread_id}/messages to capture all events.

How SSE streaming works

The server pushes events in this typical flow:

  1. message_work_in_progress logs may appear before or between chunks, indicating background processing.
  2. message_chunk events arrive incrementally as the AI generates its response. Each chunk contains the partial text under message_chunk.message and a final_chunk boolean flag.
  3. When final_chunk: true arrives, the complete message is ready.
  4. message events signal that a complete message has been assembled.
  5. task_status or execution_status events report progress. When status: "Complete" appears, the thread is idle.

Basic usage

import os
import requests
BASE_URL = os.getenv("ALFA_API_BASE_URL", "https://sandbox.api.boosted.ai")
ACCESS_TOKEN = os.environ["ALFA_ACCESS_TOKEN"]
def auth_headers():
return {"Authorization": f"Bearer {ACCESS_TOKEN}"}
def send_message(thread_id: str, message: str) -> dict:
resp = requests.post(
f"{BASE_URL}/v2/threads/{thread_id}/messages",
headers=auth_headers(),
data={"message": message},
timeout=30,
)
resp.raise_for_status()
return resp.json()

If the SSE channel closes before completion, call GET /v2/threads/{thread_id}/progress to check status. Issue DELETE /v2/threads/{thread_id}/task when a user manually stops work.

Complete SSE example

The following example demonstrates the full workflow: creating a thread, establishing an SSE stream, sending a message, and processing real-time events until completion.

The script uses Python’s threading module to establish the SSE stream before sending the message, ensuring all events are captured:

complete_sse_example.py
import json
import os
import sys
import threading
import time
import requests
BASE_URL = "https://sandbox.api.boosted.ai"
ACCESS_TOKEN = os.environ.get("ALFA_ACCESS_TOKEN", "your-access-token-here")
stream_connected = threading.Event()
stream_error = threading.Event()
def create_thread(thread_name):
url = f"{BASE_URL}/v2/threads"
headers = {
"Authorization": f"Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json"
}
data = {"thread_name": thread_name}
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
return response.json()["thread_id"]
def send_message(thread_id, message):
url = f"{BASE_URL}/v2/threads/{thread_id}/messages"
headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
data = {"message": message}
print(f"\nSending message...")
response = requests.post(url, headers=headers, data=data)
response.raise_for_status()
print("Message sent successfully!")
return response.json()
def stream_thread_events(thread_id):
url = f"{BASE_URL}/v2/threads/stream/{thread_id}"
headers = {
"Authorization": f"Bearer {ACCESS_TOKEN}",
"Accept": "text/event-stream",
"Cache-Control": "no-cache"
}
try:
print("\nConnecting to event stream...")
response = requests.get(url, headers=headers, stream=True, timeout=None)
response.raise_for_status()
print("Connected to stream, waiting for events...\n")
stream_connected.set()
for line in response.iter_lines(decode_unicode=True):
if line:
if line.startswith(':'):
continue
if line.startswith('data: '):
data = line[6:]
try:
event_data = json.loads(data)
event_type = event_data.get('event', {}).get('event_type', 'unknown')
if event_type == 'message_chunk':
chunk = event_data['event'].get('message_chunk', {})
sys.stdout.write(chunk.get('message', ''))
sys.stdout.flush()
if event_data['event'].get('final_chunk'):
print("\n-- final chunk --")
elif event_type == 'message':
print(f"\n\nComplete message received")
elif event_type == 'task_status' or event_type == 'execution_status':
status = event_data['event'].get('status', '')
if status == 'Complete':
print(f"\n\nTask completed!")
break
else:
print(f"\n[{event_type}]")
except json.JSONDecodeError:
print(f"Data: {data}")
elif line.startswith('event: '):
event_type = line[7:]
if event_type == 'close':
print("\n\nStream closed by server")
break
except requests.exceptions.RequestException as e:
print(f"\nStream error: {e}")
stream_error.set()
except Exception as e:
print(f"\nUnexpected error: {e}")
stream_error.set()
def main():
try:
print("Creating a new thread...")
thread_id = create_thread("Research Thread")
print(f"Thread created with ID: {thread_id}")
message = input("\nEnter your message: ")
stream_thread = threading.Thread(
target=stream_thread_events,
args=(thread_id,),
daemon=True
)
stream_thread.start()
if not stream_connected.wait(timeout=10):
print("Failed to connect to stream within 10 seconds")
return
if stream_error.is_set():
print("Stream connection failed")
return
time.sleep(0.5)
send_message(thread_id, message)
stream_thread.join(timeout=120)
if stream_thread.is_alive():
print("\n\nStream still active after 120 seconds, exiting...")
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
sys.exit(1)
except KeyboardInterrupt:
print("\n\nInterrupted by user")
sys.exit(0)
if __name__ == "__main__":
if ACCESS_TOKEN == "your-access-token-here":
print("Please set your ALFA_ACCESS_TOKEN environment variable before running.")
sys.exit(1)
main()

Set the ALFA_ACCESS_TOKEN environment variable with your OAuth2 access token obtained via token exchange. See the authorization guide for details. The threading synchronization ensures the SSE listener is ready before the message triggers any events, preventing lost chunks.

SSE event types

The stream emits several event types. Here are the most common:

Work in progress logs (background processing updates):

{
"event": {
"event_type": "message_work_in_progress",
"log": "Analyzing financial statements..."
}
}

Message chunks (incremental response text):

{
"event": {
"event_type": "message_chunk",
"message_chunk": {
"message": "Tesla reported revenue of ",
"message_id": "msg_xyz789"
},
"final_chunk": false
}
}

Final chunk (last piece of the message):

{
"event": {
"event_type": "message_chunk",
"message_chunk": {
"message": "$25.2B in Q3 2024.",
"message_id": "msg_xyz789"
},
"final_chunk": true
}
}

Complete message (with metadata and citations):

{
"event": {
"event_type": "message",
"message": {
"message_id": "msg_xyz789",
"message": "Apple reported revenue of $124.3B in Q1 2025, beating analyst expectations.",
"message_metadata": {
"tools_called": {},
"citations": [
{
"citation_id": "cit_abc123",
"citation_type": "web",
"name": "Apple Q1 2025 Revenue Report",
"summary": "Apple exceeded analyst expectations with record Q1 revenue.",
"last_updated_at": "2026-02-19T10:00:00Z",
"link": "https://example.com/aapl-q1-results",
"last_fetched_at": "2026-02-19T09:30:00Z",
"snippet_highlight_start": 120,
"snippet_highlight_end": 350,
"source_type": "web"
}
]
}
}
}
}

Artifacts, when present, are JSON strings embedded in the message text (wrapped in triple backticks). Use GET /v2/threads/{thread_id}/artifacts/{artifact_id} to fetch the actual artifact data.

Task status (progress updates):

{
"event": {
"event_type": "task_status",
"status": "Complete",
"log": "Analysis complete"
}
}

The message text is located at event.message_chunk.message. When final_chunk: true appears, the complete message is ready. The client must parse any artifact markers from the message text to extract artifact_id values. The script continues listening until it receives a task_status event with status: "Complete", signaling that all processing has finished.

SSE citation types

Complete messages include a citations array in message_metadata. Each citation has a citation_type field that determines which additional fields are present. All citations share these common fields:

FieldTypeDescription
citation_idstringUnique identifier for the citation
citation_typestringOne of: web, news_development, text
namestringDisplay name of the source
summarystring | nullBrief summary of the cited content
last_updated_atdatetime | nullWhen the source was last updated
source_categorystring | nullCategory of the source
inline_offsetinteger | nullPosition offset in the message text
is_snippetbooleanWhether the citation references a text snippet

Web citation (citation_type: "web"):

References a web page with a direct link and snippet highlight positions.

{
"citation_id": "cit_001",
"citation_type": "web",
"name": "Apple Q1 2025 Revenue Report",
"summary": "Apple exceeded analyst expectations with record Q1 revenue.",
"last_updated_at": "2026-02-19T10:00:00Z",
"link": "https://example.com/aapl-q1-results",
"last_fetched_at": "2026-02-19T09:30:00Z",
"snippet_highlight_start": 120,
"snippet_highlight_end": 350,
"source_type": "web"
}

News development citation (citation_type: "news_development"):

References a news development that aggregates multiple related articles.

{
"citation_id": "cit_002",
"citation_type": "news_development",
"name": "Apple Announces New MacBook Pro Lineup",
"summary": "Coverage of Apple's latest product announcement spanning 15 articles.",
"last_updated_at": "2026-02-18T14:00:00Z",
"num_articles": 15,
"development_start": "2026-02-15T08:00:00Z"
}

Document citation (citation_type: "text"):

References a document such as an SEC filing, with snippet highlight positions.

{
"citation_id": "cit_003",
"citation_type": "text",
"name": "AAPL 10-K Filing 2025",
"summary": "Annual report filing for Apple Inc.",
"last_updated_at": "2026-01-30T12:00:00Z",
"snippet_highlight_start": 500,
"snippet_highlight_end": 780,
"is_snippet": true
}

gRPC bidirectional streaming

Protocol buffer definition

Download and compile the following protocol buffer file to generate client stubs:

thread_service.proto
syntax = "proto3";
package thread_service.v1;
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";
option java_package = "com.gbi.protobuf_defs.generated.thread_service.v1";
option go_package = "github.com/GBI-Core/protobuf-defs/gobuild/thread_service/v1";
service ThreadService {
/////////////////////////////////////////////////////////////////
// Thread API
/////////////////////////////////////////////////////////////////
// Streams thread events from multiple threads
rpc StreamThreadEvents(stream StreamThreadEventsRequest)
returns (stream StreamThreadEventsResponse);
}
/////////////////////////////////////////////////////////////////
// Common messages
/////////////////////////////////////////////////////////////////
message UUID {
string id = 1;
}
message Citation {
message WebCitation {
string link = 1;
google.protobuf.Timestamp last_fetched_at = 2;
int64 snippet_highlight_start = 3;
int64 snippet_highlight_end = 4;
string source_type = 5;
}
message NewsDevelopmentCitation {
int64 num_articles = 1;
google.protobuf.Timestamp development_start = 2;
}
message NewsArticleCitation {
string link = 1;
UUID article_id = 2;
}
message DocumentCitation {
int64 snippet_highlight_start = 1;
int64 snippet_highlight_end = 2;
bool is_snippet = 3;
}
message FiscalQuarterEarningsCitation {
int64 snippet_highlight_start = 1;
int64 snippet_highlight_end = 2;
bool is_snippet = 3;
google.protobuf.Timestamp call_datetime = 4;
google.protobuf.Timestamp report_datetime = 5;
google.protobuf.Int32Value fiscal_year = 6;
google.protobuf.Int32Value fiscal_quarter = 7;
}
message VariableCitation {
enum VariablePeriodicity {
VARIABLE_PERIODICITY_UNSPECIFIED = 0;
VARIABLE_PERIODICITY_DAILY = 1;
VARIABLE_PERIODICITY_MONTHLY = 2;
VARIABLE_PERIODICITY_QUARTERLY = 3;
VARIABLE_PERIODICITY_YEARLY = 4;
}
message FunctionMetadata {
string name = 1;
string description = 2;
}
message VariableMetadata {
string name = 1;
string description = 2;
string var_id = 3;
}
string description = 1;
string data_provider = 2;
google.protobuf.Timestamp as_of_timestamp = 3;
google.protobuf.Timestamp date_range_start = 4;
google.protobuf.Timestamp date_range_end = 5;
string period_start = 6;
string period_end = 7;
string formula = 8;
repeated FunctionMetadata functions_used = 9;
repeated VariableMetadata components_used = 10;
VariablePeriodicity variable_periodicity = 11;
}
UUID citation_id = 1;
string name = 2;
string citation_type = 3;
google.protobuf.Timestamp last_updated_at = 4;
string summary = 8;
oneof citation {
WebCitation web_citation = 5;
NewsDevelopmentCitation news_development_citation = 6;
DocumentCitation document_citation = 7;
NewsArticleCitation news_article_citation = 9;
FiscalQuarterEarningsCitation fiscal_quarter_earnings_citation = 10;
VariableCitation variable_citation = 11;
}
}
message ThreadData {
enum ErrorType {
ERROR_TYPE_UNKNOWN = 0;
ERROR_TYPE_INTERNAL = 1;
}
message MessageEvent {
UUID message_id = 1;
string message = 2;
bool is_user_message = 3;
google.protobuf.Timestamp message_time = 4;
repeated Citation citations = 5;
}
message MessageChunkEvent {
UUID message_id = 1;
string message = 2;
bool final_chunk = 3;
repeated Citation citations = 4;
}
message WorkInProgressEvent {
string log = 1;
}
message ThreadNameEvent {
string thread_name = 1;
}
message WorkerErrorEvent {
ErrorType error_type = 1;
}
message MessageCancelledEvent {
string cancellation_message = 1;
}
oneof payload {
MessageEvent message_event = 1;
MessageChunkEvent message_chunk_event = 2;
WorkInProgressEvent work_in_progress_event = 3;
ThreadNameEvent thread_name_event = 4;
WorkerErrorEvent worker_error_event = 5;
MessageCancelledEvent message_cancelled_event = 6;
}
}
/////////////////////////////////////////////////////////////////
// REQUEST/RESPONSE MESSAGES
/////////////////////////////////////////////////////////////////
message StreamThreadEventsRequest {
// The token for the user that is requesting access to the thread
string user_token = 1;
// The thread id the user is requesting access to
UUID thread_id = 2;
bool subscribe = 3;
}
message StreamThreadEventsResponse {
// The user(s) that requested access to the thread
repeated UUID user_ids = 1;
// The thread id the data corresponds to
UUID thread_id = 2;
// The data for the thread
ThreadData thread_data = 3;
}

Building the client

Generate client code from the proto file using the gRPC tools for your language.

Python:

python -m grpc_tools.protoc \
-I./proto \
--python_out=. \
--grpc_python_out=. \
--pyi_out=. \
<path_to_proto_file>

This generates:

  • thread_service_pb2.py: Protocol buffer message classes
  • thread_service_pb2_grpc.py: gRPC service stub classes
  • thread_service_pb2.pyi: Type stubs for type checking

Go:

protoc \
-I./proto \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
<path_to_proto_file>

This generates Go code locally. Update the go_package option in the proto file to match your module path, or import the generated code using the path specified in go_package.

gRPC connection setup

Establish a secure gRPC channel with keepalive settings to maintain the connection:

import grpc
GRPC_SERVER = "sandbox.api.boosted.ai:443"
SERVICE_ACCOUNT_TOKEN = "<YOUR SERVICE ACCOUNT TOKEN>"
# Create secure channel with keepalive options
credentials = grpc.ssl_channel_credentials()
options = [
('grpc.keepalive_time_ms', 10000), # Send keepalive ping every 10 seconds
('grpc.keepalive_timeout_ms', 5000), # Wait 5 seconds for ping ack
('grpc.keepalive_permit_without_calls', True), # Allow keepalive pings when no calls
('grpc.http2.max_pings_without_data', 0), # No limit on pings without data
('grpc.http2.min_time_between_pings_ms', 10000), # Min 10 seconds between pings
('grpc.http2.min_ping_interval_without_data_ms', 5000), # Min 5 seconds between pings without data
]
channel = grpc.secure_channel(GRPC_SERVER, credentials, options=options)
stub = thread_service_pb2_grpc.ThreadServiceStub(channel)
metadata = [("authorization", f"Bearer {SERVICE_ACCOUNT_TOKEN}")]

The keepalive settings ensure the connection remains active even during periods of inactivity, preventing timeouts.

Complete gRPC example

The following example demonstrates subscribing to thread events via gRPC streaming:

grpc_streaming_example.py
import time
from collections.abc import Iterator
import grpc
from thread_service.v1 import thread_service_pb2, thread_service_pb2_grpc
def test_streaming():
# Configuration - update these
GRPC_SERVER = "sandbox.api.boosted.ai:443"
SERVICE_ACCOUNT_TOKEN = "<YOUR SERVICE ACCOUNT TOKEN>"
THREAD_ID = "<YOUR THREAD ID>"
USER_TOKEN = "<YOUR USER TOKEN>"
# Connect
credentials = grpc.ssl_channel_credentials()
options = [
('grpc.keepalive_time_ms', 10000),
('grpc.keepalive_timeout_ms', 5000),
('grpc.keepalive_permit_without_calls', True),
('grpc.http2.max_pings_without_data', 0),
('grpc.http2.min_time_between_pings_ms', 10000),
('grpc.http2.min_ping_interval_without_data_ms', 5000),
]
channel = grpc.secure_channel(GRPC_SERVER, credentials, options=options)
stub = thread_service_pb2_grpc.ThreadServiceStub(channel)
metadata = [("authorization", f"Bearer {SERVICE_ACCOUNT_TOKEN}")]
print(f"Connecting to {GRPC_SERVER}")
def request_generator():
"""Send subscription request."""
yield thread_service_pb2.StreamThreadEventsRequest(
thread_id=thread_service_pb2.UUID(id=THREAD_ID),
user_token=USER_TOKEN,
subscribe=True,
)
# Keep stream alive
while True:
time.sleep(60)
try:
# Start streaming
stream: Iterator[thread_service_pb2.StreamThreadEventsResponse] = stub.StreamThreadEvents(
request_generator(), metadata=metadata
)
print(f"Subscribed to thread {THREAD_ID}")
print("Listening for events... (Ctrl+C to stop)")
# Receive events
for response in stream:
print("\nEvent received!")
print(f" Thread: {response.thread_id.id}")
print(f" Users: {[u.id for u in response.user_ids]}")
print(f" Data: {response.thread_data}")
except KeyboardInterrupt:
print("\nStopped")
except grpc.RpcError as e:
print(f"Error: {e.code()}: {e.details()}")
finally:
channel.close()
if __name__ == "__main__":
test_streaming()

How gRPC streaming works

  1. Create a secure channel with SSL credentials and keepalive options to maintain the connection.
  2. Create a request generator that yields the initial subscription request with thread_id, user_token, and subscribe=True. The generator continues running to keep the stream alive.
  3. Call the streaming RPC using the generated stub, passing the request generator and authentication metadata.
  4. Iterate over responses as they arrive. Each StreamThreadEventsResponse contains:
    • thread_id: The thread the event belongs to
    • user_ids: List of user IDs that requested access
    • thread_data: A ThreadData message containing the event payload

The thread_data field is a ThreadData message with a oneof payload that can be one of:

  • message_event: A complete message with citations
  • message_chunk_event: An incremental message chunk with optional citations (check final_chunk for completion)
  • work_in_progress_event: Background processing status
  • thread_name_event: Thread name updates
  • worker_error_event: Error notifications
  • message_cancelled_event: Cancellation confirmations

The request generator must continue yielding or sleeping to keep the bidirectional stream alive. If the generator exits, the stream will close.

gRPC citation types

Complete messages and message chunks both include a citations array. Each citation carries common metadata plus a type-specific object determined by citation_type:

FieldTypeDescription
citation_idUUIDUnique identifier for the citation
namestringDisplay name of the source
citation_typestringOne of: web, news_development, news_article, document, fiscal_quarter_earnings, variable
last_updated_atTimestampWhen the source was last updated
summarystringBrief summary of the cited content

Web citation (web_citation):

References a web page with a direct link and snippet highlight positions.

{
"citationId": {"id": "cit-001"},
"name": "Apple Q1 2025 Revenue Report",
"citationType": "web",
"lastUpdatedAt": "2026-02-19T10:00:00Z",
"summary": "Apple exceeded analyst expectations with record Q1 revenue.",
"webCitation": {
"link": "https://example.com/aapl-q1-results",
"lastFetchedAt": "2026-02-19T09:30:00Z",
"snippetHighlightStart": 120,
"snippetHighlightEnd": 350,
"sourceType": "web"
}
}

News development citation (news_development_citation):

References a news development that aggregates multiple related articles.

{
"citationId": {"id": "cit-002"},
"name": "Apple Announces New MacBook Pro Lineup",
"citationType": "news_development",
"lastUpdatedAt": "2026-02-18T14:00:00Z",
"summary": "Coverage of Apple's latest product announcement spanning 15 articles.",
"newsDevelopmentCitation": {
"numArticles": 15,
"developmentStart": "2026-02-15T08:00:00Z"
}
}

News article citation (news_article_citation):

References a specific news article with a direct link and article identifier.

{
"citationId": {"id": "cit-003"},
"name": "Apple Q1 Results Beat Expectations",
"citationType": "news_article",
"lastUpdatedAt": "2026-02-17T16:30:00Z",
"summary": "Apple exceeded analyst expectations for Q1 2026 earnings.",
"newsArticleCitation": {
"link": "https://example.com/aapl-q1-beat",
"articleId": {"id": "art-456"}
}
}

Document citation (document_citation):

References a document such as an SEC filing, with snippet highlight positions.

{
"citationId": {"id": "cit-004"},
"name": "AAPL 10-K Filing 2025",
"citationType": "document",
"lastUpdatedAt": "2026-01-30T12:00:00Z",
"summary": "Annual report filing for Apple Inc.",
"documentCitation": {
"snippetHighlightStart": 500,
"snippetHighlightEnd": 780,
"isSnippet": true
}
}

Fiscal quarter earnings citation (fiscal_quarter_earnings_citation):

References an earnings call transcript or report for a specific fiscal quarter.

{
"citationId": {"id": "cit-005"},
"name": "AAPL Q4 2025 Earnings Call",
"citationType": "fiscal_quarter_earnings",
"lastUpdatedAt": "2026-01-28T18:00:00Z",
"summary": "Apple Q4 2025 earnings call transcript and financial report.",
"fiscalQuarterEarningsCitation": {
"snippetHighlightStart": 200,
"snippetHighlightEnd": 450,
"isSnippet": true,
"callDatetime": "2025-10-30T17:00:00Z",
"reportDatetime": "2025-10-30T16:30:00Z",
"fiscalYear": 2025,
"fiscalQuarter": 4
}
}

Variable citation (variable_citation):

References a computed financial variable or metric, including its formula, data provider, time range, and periodicity.

{
"citationId": {"id": "cit-006"},
"name": "AAPL Revenue Growth",
"citationType": "variable",
"lastUpdatedAt": "2026-02-19T08:00:00Z",
"summary": "Year-over-year revenue growth for Apple Inc.",
"variableCitation": {
"description": "Revenue growth rate",
"dataProvider": "financial_data",
"asOfTimestamp": "2026-02-19T00:00:00Z",
"dateRangeStart": "2025-02-19T00:00:00Z",
"dateRangeEnd": "2026-02-19T00:00:00Z",
"periodStart": "Q1 2025",
"periodEnd": "Q4 2025",
"formula": "revenue_growth(AAPL)",
"functionsUsed": [
{"name": "revenue_growth", "description": "Calculates YoY revenue growth"}
],
"componentsUsed": [
{"name": "Revenue", "description": "Total revenue", "varId": "var-rev-001"}
],
"variablePeriodicity": "VARIABLE_PERIODICITY_QUARTERLY"
}
}

Authentication

gRPC streaming uses Bearer token authentication via metadata. Include your service account token in the metadata:

SERVICE_ACCOUNT_TOKEN = "<YOUR SERVICE ACCOUNT TOKEN>"
metadata = [("authorization", f"Bearer {SERVICE_ACCOUNT_TOKEN}")]
stream = stub.StreamThreadEvents(request_generator(), metadata=metadata)

Obtain your service account token from your Boosted account. See the authorization guide for details. Ensure your token has permission to access the specified thread.