Real-time notification stream (SSE)
Stream new in-app notifications to foregrounded clients via Server-Sent Events.
GET /v1/me/notifications/stream pushes each new in-app Notification row to
connected clients as soon as it is persisted, without polling. Use this endpoint
when the app is foregrounded so travelers see disruption alerts instantly.
Every frame carries a cursor, so a client that reconnects picks up exactly
where it left off.
Authentication
Same requirement as all other /v1 endpoints: sign the request with the
consumer HMAC and pass a valid traveler JWT in the
Authorization: Bearer <token> header.
A missing or invalid token returns 401.
Protocol
The endpoint uses Server-Sent Events (text/event-stream). Clients should:
- Open the connection with
Accept: text/event-stream(most SSE client
libraries set this automatically). - Handle each event named
notificationas it arrives, and remember its
id. - Reconnect automatically when the connection closes, sending the last
id
received asLast-Event-ID— use a fixed back-off strategy. See
Reconnect and replay.
GET /v1/me/notifications/stream HTTP/1.1
Authorization: Bearer <token>
Accept: text/event-stream
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Client-library examples
The raw HTTP shape above works with any SSE-capable client. Most stacks have
a dedicated client library so you do not have to parse event:/data:
frames by hand.
Python (httpx-sse)
httpx-sse)import httpx
from httpx_sse import connect_sse
with httpx.Client(
base_url="https://api.travel.tech",
headers={
"Authorization": f"Bearer {traveler_jwt}",
"TravelTech-Consumer": consumer,
"TravelTech-Timestamp": timestamp,
"TravelTech-Signature": signature,
},
) as client:
with connect_sse(client, "GET", "/v1/me/notifications/stream") as event_source:
for sse in event_source.iter_sse():
if sse.event == "notification":
last_event_id = sse.id # send as Last-Event-ID on reconnect
notification = sse.json()
print(notification["title"], notification["body"])
elif sse.event == "done":
break # server is shutting down; reconnect with backoffhttpx-sse raises on a non-2xx response before any event is read, so a
401 from a missing or invalid token surfaces as an exception rather than
silently yielding zero events. httpx-sse does not reconnect for you: when you
open the next connection, add "Last-Event-ID": last_event_id to its headers.
TypeScript / JavaScript (browser EventSource)
EventSource)Native EventSource cannot set the Authorization header the consumer
HMAC and bearer credentials need, so most browser clients instead use a
library that supports custom headers, such as
@microsoft/fetch-event-source.
It sends Last-Event-ID on each reconnect by itself:
import { fetchEventSource } from "@microsoft/fetch-event-source";
await fetchEventSource("https://api.travel.tech/v1/me/notifications/stream", {
headers: {
Authorization: `Bearer ${travelerJwt}`,
"TravelTech-Consumer": consumer,
"TravelTech-Timestamp": timestamp,
"TravelTech-Signature": signature,
},
onmessage(msg) {
if (msg.event === "notification") {
const notification = JSON.parse(msg.data);
console.log(notification.title, notification.body);
} else if (msg.event === "done") {
throw new Error("stream closing"); // triggers the library's own retry
}
},
onerror(err) {
console.error("notification stream error", err);
// rethrow to stop retrying, or return to let the library back off and reconnect
},
});Event format
notification — new in-app row
notification — new in-app rowEmitted for every new streamable Notification row persisted for the
authenticated traveler (see Inbox-only by design for
the ones that are not). The data field is a JSON-serialised
NotificationOut object
(the same shape returned by GET /v1/me/notifications).
The id field is an opaque cursor. Store the last one you receive and send it
back on reconnect; do not parse it or build one yourself.
id: AQAGUqHcVHoAAY8sPhI0eryN7wAAAAAAAQ
event: notification
data: {"id":"...","event_type":"segment.delayed","severity":"warning","title":"Delay — Flight","body":"Your trip is delayed by 45m.",...}
event_type is always the dotted wire value — segment.delayed,
booking_lifecycle.confirmed, leg.fields_updated, assistant_reply. It is
the same string the push notification carries in its payload metadata and the
same string GET /v1/me/notifications returns, so a client can branch on one
value regardless of how the notification reached it. See
Event types for every value the stream can deliver; the API
reference for this endpoint shows one complete example frame per value.
done — stream closing
done — stream closingEmitted when the server initiates a graceful shutdown. After receiving this
event the client should reconnect if it still needs real-time delivery.
event: done
data: {}
Example payload
{
"id": "00000000-0000-4000-8000-000000000006",
"trip_id": "00000000-0000-4000-8000-0000000000a1",
"segment_id": "00000000-0000-4000-8000-0000000000b1",
"event_type": "segment.delayed",
"severity": "warning",
"title": "Delay — Flight",
"body": "Your trip is delayed by 45m.",
"deep_link": "bizaway://trips/00000000-0000-4000-8000-0000000000a1?segment=00000000-0000-4000-8000-0000000000b1",
"channel": "push",
"dispatched_at": "2026-11-03T07:40:00Z",
"read_at": null,
"is_dedup_skipped": false,
"delivery_status": "pending",
"created_at": "2026-11-03T07:40:00Z"
}Payload fields
Every notification event carries the same NotificationOut object,
whatever its event_type. There is no per-event payload: the event-specific
detail is already rendered into title and body, and deep_link points to
the screen that shows the rest.
| Field | Type | Nullable | Meaning |
|---|---|---|---|
id | UUID | No | Notification ID. Use it with the mark-as-read endpoints and to de-duplicate against GET /v1/me/notifications. Not the same value as the frame's SSE id: cursor. |
trip_id | UUID | Yes | Trip the notification is about. null when the notification is not tied to a trip. |
segment_id | UUID | Yes | Trip segment the notification is about. null when the notification is not segment-scoped. |
event_type | string | No | What happened, as a dotted wire value. See Event types. |
severity | string | No | info, warning, or critical. See Notification severity tiers. |
title | string | No | Short, traveler-facing headline. Display it as-is. |
body | string | No | Traveler-facing message text. Display it as-is. |
deep_link | string | Yes | In-app route to open when the traveler taps the notification, e.g. bizaway://trips/<trip_id>?segment=<segment_id>. |
channel | string | No | How the notification was delivered: push (a push notification was also sent) or in_app (inbox and stream only). |
dispatched_at | datetime (UTC) | Yes | When the push was sent. null when no push was sent. |
read_at | datetime (UTC) | Yes | When the traveler marked it read. null on live delivery; a row replayed after a reconnect reports its current value, so it may already be read on another device. |
is_dedup_skipped | boolean | No | Always false on the stream. Duplicates are recorded in the inbox only. |
delivery_status | string | Yes | Push delivery state: pending (push sent, not yet confirmed), delivered, failed, or skipped. In-app-only notifications report delivered. |
created_at | datetime (UTC) | No | When the notification was created. |
Event types
The stream delivers every notification travel.tech raises for a traveler's
trips, bookings, assistant conversations, and Tasks, except the ones listed
under Inbox-only by design. Severity is the default.
A strike-related event (segment.at_risk, segment.disrupted,
segment.cancelled, or segment.delayed) is always critical, whatever its
default.
Trip segments (segment.*)
segment.*)Raised when a booked flight, train, stay, or car segment changes status.
event_type | Severity | Meaning | Example (title / body) |
|---|---|---|---|
segment.checked_in | info | Check-in confirmed. | Checked in — Flight / You have been checked in. |
segment.boarding | info | Boarding has started. | Boarding now — Flight / Boarding is now open. Proceed to your gate. |
segment.departed | info | The service has departed. | Departed — Flight / Your trip segment has departed. |
segment.completed | info | The segment has arrived or ended. | Arrived — Flight / You have arrived at your destination. |
segment.delayed | warning; critical above 60 minutes | The departure is delayed. | Delay — Flight / Your trip is delayed by 45m. |
segment.at_risk | warning | The departure is at risk, e.g. a forecast disruption or strike. | Trip at risk — Flight / Your trip segment is at risk. Check your itinerary. |
segment.connection_at_risk | warning | A connection to the next segment may be missed. | Connection at risk — Flight / Your connection may be at risk due to a delay. |
segment.risk_resolved | info | A previously flagged risk has cleared. | Risk resolved — Flight / The disruption risk for your trip has been resolved. |
segment.disrupted | warning | A service disruption affects the segment, including a diversion. | Trip disrupted — Flight / Your trip has been disrupted. Please review your itinerary. |
segment.platform_changed | info | The train platform has changed. | Platform change — Train / Your departure platform has changed. |
segment.rescheduled | info | The schedule changed; no action needed. | Rescheduled — Flight / Your trip schedule has changed. |
segment.rebooked | info | The traveler was moved to another service. | Rebooked — Flight / Your trip has been rebooked on an alternative. |
segment.partially_cancelled | critical | Part of the segment was cancelled. | Partial cancellation — Flight / Part of your trip has been cancelled. |
segment.cancelled | critical | The segment was cancelled. | Segment cancelled — Flight / This segment has been cancelled. Please check alternatives. |
segment.overbooked | critical | The stay is overbooked. | Overbooking alert — Stay / Your stay has been overbooked by the provider. Please rebook immediately. |
segment.early_checkin_available | info | Early check-in is available at the stay. Stream and inbox only; no push is sent. | Early check-in available — Stay / Your room is ready! Early check-in available from 11:00. |
Live journey details (leg.*)
leg.*)event_type | Severity | Meaning | Example (title / body) |
|---|---|---|---|
leg.fields_updated | info | A live detail changed: gate, terminal, platform, coach, or seat. | Gate change — XY 1234 MXP → LHR / Gate: B12 → B31 |
Booking lifecycle (booking_lifecycle.*)
booking_lifecycle.*)Raised when a booking changes state. segment_id is null; trip_id is set
when the booking already belongs to a trip.
event_type | Severity | Meaning | Example (title / body) |
|---|---|---|---|
booking_lifecycle.submitted | info | The booking was submitted. | Booking submitted — Flight / Your booking has been submitted and is being processed. |
booking_lifecycle.sent_for_approval | info | The booking is waiting for approval. | Sent for approval — Flight / Your booking requires approval. An approver has been notified. |
booking_lifecycle.confirmed | info | The booking is confirmed. | Booking confirmed — Flight / Your booking has been confirmed. Have a great trip! |
booking_lifecycle.rejected | warning | The booking was rejected by an approver. | Booking rejected — Flight / Your booking was rejected by an approver. Please contact your travel manager. |
booking_lifecycle.fare_expired | warning | The fare expired before the booking completed. | Fare expired — Flight / The fare for your booking expired before approval was granted. Please rebook. |
booking_lifecycle.payment_failed | critical | Payment for the booking failed. | Payment failed — Flight / Payment for your booking could not be processed. Please review your payment details. |
Assistant replies and Tasks
event_type | Severity | Meaning | Example (title / body) |
|---|---|---|---|
assistant_reply | info | The assistant replied in a conversation. | New message from Bizzy / Bizzy has a response for you. Tap to continue your conversation. |
live_agent_reply | info | A live travel agent replied in a conversation. | New message from a Travel Expert / A Travel Expert has a message for you. Tap to continue the conversation. |
human_task.created | warning; critical when the Task's deadline is close | The traveler has a new Task that needs their decision. | Action required / You have a new decision to make on your trip. |
Inbox-only by design
These notifications are delivered to the inbox only, by design. They appear in
GET /v1/me/notifications; read the inbox to show them:
- Pre-trip briefings (
pre_trip_briefing.stay,pre_trip_briefing.train,
pre_trip_briefing.car). - Destination advisories (
info_surface.destination_advisory). - Travel-policy notices (
selection_policy,policy_notify,
policy_approval_requested,policy_approval_decided). - Scheduled reminders, such as check-in reminders.
- Notifications grouped into a batch when several updates hit the same
segment in a short window, and duplicates (is_dedup_skipped: true).
Treat unknown event_type values as generic notifications: render title
and body, and follow deep_link. New values may be added without notice.
Reconnect and replay
A stream can be served by any travel.tech server, and every stream receives
every streamable notification for its traveler, whichever server created it.
When the connection drops, reconnect and send the id of the last
notification frame you received:
- in the
Last-Event-IDheader — SSE client libraries that reconnect for you
send it automatically; or - as the
sincequery parameter,GET /v1/me/notifications/stream?since=<id>,
for clients that cannot set the header. If both are sent, the header wins.
The stream first replays every streamable notification created after that
cursor, oldest first, then continues live. A connection opened without a cursor
starts live, and first re-sends the streamable notifications from the last 30
seconds, so one created while you were opening the stream is not lost. Those
may already be in your inbox; de-duplicate them by data.id.
A cursor that the stream did not issue is rejected with 422; reconnect
without it and catch up from the inbox.
Recommended client strategy:
- Persist the last
idand reconnect with it after every disconnect, including
afterevent: done. - De-duplicate by the notification's
data.idwhen you merge stream events
with rows read fromGET /v1/me/notifications. - The inbox stays authoritative. On a cold start, after a long time offline, or
when you have no cursor, readGET /v1/me/notifications, then open the
stream without a cursor.
Relationship to push notifications
When the app is connected via SSE, the client should suppress local push
notification UI for events it receives via the stream to avoid double-alerting
the traveler. The push notification is still sent; the client decides at
render time whether to display it based on SSE connection state.
Error responses
| Status | Condition |
|---|---|
| 401 | Missing or invalid bearer token |
| 422 | Last-Event-ID or since is not a cursor the stream issued |
| 429 | Too many concurrent streams open for this traveler |
| 503 | Notification stream not available (startup in progress) |
Reporting issues
This page is maintained by the BizAway integration team. To request changes or
report an error, contact your BizAway integration contact. Edits made directly
in the ReadMe dashboard are overwritten on the next publish.
Updated 2 days ago