Skip to main content

SSE wire contract

POST /chat/control/stream answers with Server-Sent Events (text/event-stream): a long-lived response that delivers the answer's text as it is written, plus structured events for the places, map actions and routes in the answer.

This page is the contract for that stream. If you use kaleidr.js, the chat panel reads the stream for you and you do not need this page. Read it when you build your own client. Working clients in Node and Python are in Reading the stream; the request body is in Endpoints.

/chat/summary/stream and /chat/button/stream use the same frame format with a smaller set of events. They are described at the end of this page.

Before the stream starts​

A request that is refused never becomes a stream. Authentication, scope, quota, rate-limit, validation and size failures come back as an ordinary HTTP response with a JSON body and a 4xx status (Errors). Check the status first, and parse the body as SSE only when it is 200.

The request has size limits: at most 400 messages, 64,000 characters in one message and 200,000 characters in all messages together. A request over one of them is a 422 with a code, such as conversation_too_long, and a body over 2 MiB is a 413. The full list is in Endpoints.

A successful response has these headers:

HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache, no-transform

Frame format​

A frame is one or two lines followed by a blank line. Lines end with \n.

event: place
data: {"name": "Louvre Museum", "coordinates": {"lat": 48.8606, "lng": 2.3376}}

data: {"text": "The Louvre sits on the right bank"}

: keep-alive

  • A named frame has an event: line, then one data: line.
  • A prose frame has only a data: line. It has no event: line.
  • A comment starts with a colon. Kaleidr sends : keep-alive as a heartbeat. It carries no data.
  • data: is always one complete JSON object on one line. Text is UTF-8.

Network chunks do not line up with frames. One read can deliver half a frame or several. Buffer what you receive, split on line breaks, and act on a frame only when you reach its blank line.

Compatibility rule​

Ignore event names you do not recognize, and ignore fields you do not use. New events and new fields are added to this contract without a version change. A client that fails on an unknown event will break.

Stable events​

event:PayloadMeaning
(unnamed){ text }The next piece of the answer's prose. Append it to what you have. May contain [[Name]] markers around place names.
phase{ type: "phase", phase: "thinking", session_id }The request was accepted and work has started. phase is always "thinking".
start{ type: "start", session_id, purpose: "control" }The prose is about to stream.
place{ name, coordinates?: { lat, lng }, address?, category?, icon?, source?, id?, unverified? }A place in the answer. Merge by name; the first coordinates you receive for a name are final. source: "vendor" marks a place from your organization's own data; source: "map" marks a place the published map being viewed already shows.
place_linkedplace fields plus is_update?More detail for a place already sent. Merge it into the place with the same name; ignore it if you have not seen that name.
early_actions{ type: "early_actions", actions: [ { type, payload } ], places_count, is_partial: true }Map actions you can apply before the answer finishes, sent once two or more places have coordinates.
prompt_options{ type: "prompt_options", schema_version, kind, question, allow_free_text, options: [ { label, send_text } ] }A question with choices, sent when the request was ambiguous. Show the options; send the chosen option's send_text as the next user message.
route_clear{}Remove the routes you have drawn. New routes follow.
route{ type: "route", schema_version, id?, profile, origin, destination, primary, alternatives[], comparison[], stops?, color?, label?, transit?, attribution? }A computed route. Arrives before the prose on a directions question, so the map can draw while the text streams.
end{ type: "end", full_text, places[], actions[], question_type, response_time, total_tokens?, dropped_places? }The answer is complete. This is the authoritative result.
error{ type: "error", message, code?, partial_text }The answer failed. Sent instead of end.
quota{ type: "quota", product, meter, tier, used, limit, reset_at, period } with a server key; { type: "quota", meter, reset_at } with a session tokenYour organization's AI-credit usage after this call was charged. The last event of the stream, after end or error. See quota.
: keep-alivecomment lineHeartbeat, sent after 10 seconds of silence. Ignore it, but count it as activity.

Removed: grounding (2026-09-05). It is no longer sent.

Live-data cards and layers​

A question about live or measured facts can add one of these events. A card arrives before the prose; a layer can arrive during or after it. They are additive: draw the ones you support and ignore the rest. Field sets beyond the ones listed can grow.

event:PayloadSent for
weather_cardobject with lat, lng, name, temperatures (temp_c, temp_f), conditions, optional forecast[]Current weather at a place.
weather_layerobject with layer, tile_path_template, tile_size, max_zoom, scope, attributionA temperature overlay for a region.
satellite_cardobject with name, norad_id, lat, lng, altitude_km, velocity_kms, observed_atThe position of a named satellite.
constellation_layerobject with group, features[] of { name, norad_id, lat, lng }, optional trackThe satellites around the one in the card.
building_cardobject with lat, lng, optional name, height_m, num_floorsFacts about a building.
trees_cardobject with lat, lng, count, radius_mSurveyed trees around a point.
heritage_cardobject with lat, lng, count, sites[]Heritage sites around a point.
rail_cardobject with lat, lng, lines[], optional nearest_stationRail lines around a point.
arrivals_cardobject with lat, lng, stop, departures[], alerts[], attributionTransit departures at a stop.
vehicles_layerobject with operator, vehicles[], shapes[], attributionTransit vehicle positions for the card's stop or line.
streetview_cardobject with lat, lng, image_id, image_lat, image_lngStreet-level imagery near a place.

end repeats five of these under the same name when they were sent, so a client that missed the event can still read it: weather_card, satellite_card, constellation_layer, weather_layer and vehicles_layer (for example end.weather_card). The other cards are not repeated on end. Live event listings are not delivered through the platform API: see Data licenses.

Diagnostic / experimental (non-contract)​

These can appear on the wire. They are not part of the contract: do not depend on them.

event:PayloadNotes
debug{ scope, … }Internal diagnostic. Not sent on normal requests.
place_name_found{ name, is_early }An early hint that a place name was mentioned. The place event that follows is the one to use.

Order of events​

A stream moves through these stages. Stages marked optional do not appear in every stream.

  1. Optional. place events with source: "vendor": up to three places from your organization's own data, when the key carries the vendor scope. Sent first so the map can draw them at once.
  2. phase. Always the first event from the answer itself.
  3. Optional. : keep-alive comments while the answer is prepared.
  4. Optional. Events that precede the prose: prompt_options, or route_clear followed by route, and a card.
  5. start.
  6. Prose frames, interleaved with place, place_linked, early_actions, and sometimes route_clear, route and layer events. Keep-alives fill any silence.
  7. Optional. Late place, place_linked, layer and route events, once the prose is finished.
  8. end, or error.
  9. Optional. quota.
  10. The server closes the connection.

Two consequences for a client:

  • end is not the last event. Keep reading until the body closes, or you will miss quota.
  • A stream can close with neither end nor error, for example when the network drops. Treat a closed stream with no terminal event as failed.

Payload details​

Prose and markers​

Prose frames carry text only. Structured data never arrives inside the prose. The text can include double-bracket markers around place names, such as near [[Louvre Museum]], which the chat panel turns into links to pins. For plain display, remove the brackets and keep the name.

place​

FieldTypeNotes
namestringThe key you merge on.
coordinatesobject { lat, lng }Decimal degrees. Can be absent. A place the answer names but that could not be located is sent without coordinates and with unverified: true. Do not draw a pin for it.
addressstringOptional.
categorystringOptional.
iconstringOptional. An emoji for the place.
sourcestringOptional. "vendor" for a place from your organization's own data. "map" for a place that the published map the visitor is viewing already shows: the chat panel does not add its own pin for it, because the map draws it.
idstringOptional. Present on places from your own data.
unverifiedbooleanOptional. true when the place has no confirmed location.

A place can carry other fields. Ignore the ones you do not use.

The same name can arrive more than once: first with coordinates, later with more detail. Keep one record per name, never move a pin once it has coordinates, and let later events fill in other fields.

Actions​

early_actions.actions and end.actions are lists of { type, payload }.

Action typePayloadWhat to do
map.fitBounds{ coordinates: [[lng, lat], …], padding, maxZoom, duration }Fit the camera to the coordinates.
places.highlight{ places: [ { name, lat, lng } ], options }Emphasize these places.
map.showRoute{ id, label, profile, coordinates: [[lng, lat], …], distance_m, duration_s, turn_by_turn[], transit?, attribution? }Draw the line. Show attribution with it.
route.clear{ id? }Remove the route with that id, or all routes when there is no id.

Other action types can appear. Skip the ones you do not handle. Note the two coordinate shapes: a place has coordinates: { lat, lng }, while a line is a list of [lng, lat] pairs.

route​

primary holds the line to draw: { coordinates: [[lng, lat], …], distance_m, duration_s, turn_by_turn[] }. Each step in turn_by_turn has text and can have direction_type, distance_m, duration_s, lat and lng. origin and destination each have a label. alternatives[] are other lines with a route_id, and comparison[] gives each alternative's delta_duration_s against the primary. stops[], when present, are ordered { lat, lng, name } points of a multi-stop route.

A road route carries attribution: { text, url, license }. Show text, linked to url, wherever you draw the route. A transit route carries its operator's credit inside transit instead.

Each route event has an id. Drawing a route with an id you already have replaces that route.

end​

FieldTypeNotes
full_textstringThe complete answer. Prefer it over your concatenated prose frames.
placesarrayThe final place list, in order. Merge it over the places you collected, by name.
actionsarrayThe final map actions. Apply these; they supersede early_actions.
dropped_placesarray of stringsOptional. Names sent earlier that are not part of the final answer. Remove their pins.
question_typestringA label for the kind of question. Informational.
response_timenumberSeconds the answer took.
total_tokensnumberOptional. The tokens the answer used, which is what the call is charged.

error​

event: error
data: {"type": "error", "code": "upstream_stall", "message": "The AI service stopped responding. Please try again.", "partial_text": "The Louvre sits"}

message is safe to show to a user. partial_text is the prose produced before the failure. code is present for one case only:

CasecodeWhat to do
The answer stopped making progress, or the stream reached its maximum length"upstream_stall"Offer a retry.
Any other failure while answeringabsentShow message; a retry may succeed.

After error there is no end. A quota event can still follow.

quota​

The fields depend on the credential that opened the stream.

FieldTypeServer key or legacy keySession token
typestring, "quota"YesYes
meterstringYesYes
reset_atstring, ISO 8601 with offsetYesYes
productstringYesNo
periodstringYesNo
tierstringYesNo
usednumberYesNo
limitnumberYesNo

A session token is what a page's visitors call with: one exchanged from a publishable key, or a published map's visitor session. Its quota event says when the period resets and nothing about the organization's plan, usage or limit. A browser client must not expect used or limit. The same rule applies to the 429 quota_exceeded body: see Quota and rate limits.

Keep-alives and timeouts​

BehaviorValueWhat your client should do
Heartbeat on the chat streamA : keep-alive comment after 10 seconds without other outputReset your idle timer on any bytes received, comments included.
Idle connectionA connection silent for about a minute can be closed in transitSet an idle (read) timeout of 30 to 60 seconds. Do not set a short total timeout.
Time to first proseCan be tens of seconds on a hard questionShow progress from phase; do not treat the wait as a failure.
Maximum stream lengthAbout 10 minutes, after which the server sends error with upstream_stallSet a total timeout a little above 10 minutes as a backstop.

Aborting, and how a stream is charged​

Close the connection to abort. The server notices within about ten seconds and stops the answer.

  • A stream that completes is charged the tokens the answer used, reported as end.total_tokens.
  • A stream that is aborted or fails before completing is charged an estimate. It is at least the size of the conversation you sent in the request, and otherwise follows the amount of text already streamed. Because of the first part, it can be more than the hold placed when the request was admitted: closing the connection at the first frame does not make a long conversation free.
  • Places from your own data (source: "vendor") are not counted toward the charge.

A retry is a new request and is charged again. See Quota and rate limits.

Summary and button streams​

These two endpoints back the place card in Kaleidr Chat. They use the same frame format, the same unnamed prose frames and the same final quota event. They differ from the chat stream in three ways: they send no phase event, no place or action events, and no keep-alives. Use a 60-second idle timeout.

Both refuse a request field that is too long with 422 field_too_long before the stream starts, and ignore fields they do not list: see Endpoints. When one of these streams is aborted or fails, the charge is an estimate of the text already streamed and is never more than the hold.

POST /chat/summary/stream, for a place:

  1. Optional. address with payload { type: "address", address, source }
  2. start with payload { type: "start", poi_type }
  3. Optional, possibly more than once. url with payload { type: "url", website, source }
  4. Prose frames with payload { text }
  5. end with payload { type: "end", full_text, summary, poi_type, category, city, country, sources[], website?, total_tokens? }, or error with payload { type: "error", message, partial_text }
  6. Optional. quota

POST /chat/summary/stream, for a building (type: "building"):

  1. start with payload { type: "start", address }
  2. Prose frames
  3. end with payload { type: "end", full_text, parsed, total_tokens? }, or error
  4. Optional. quota

POST /chat/button/stream:

  1. start with payload { type: "start", query_type }
  2. Prose frames
  3. end with payload { type: "end", full_text, reply, query_type, first_token_ms, total_ms, total_tokens? }
  4. Optional. quota

The button stream has no error event. If it fails after it has started, the connection closes without end. A failure before it starts is an HTTP 500 with {"detail": "Button chat stream error"}.

POST /spot/overview/stream sends the frames of a place summary, above, after a place frame with payload { type: "place", name }: the full name of the place or region described, such as "Springfield, Ohio, United States of America", for labeling or copying it. It comes before every other frame and can be absent. It is not the chat stream's place event and carries no coordinates. A ref it cannot describe gets one error frame with payload { type: "error", message, ref } in place of the summary. See Endpoints.