Live streams
Live monitoring watches an HLS stream for SSAI ad breaks. The service polls the manifest at poll_interval_s and, whenever a new ad boundary is detected, creates a burst: the analysis of the segment_n content segments that played just before the break. Each burst carries per-segment scenes (caption, transcript, mood, brands, GARM categories) and a burst-level rollup (summary, genre, IAB categories, GARM assessment).
Every example uses https://video.pyler.ai and an API key in $VUF_API_KEY. See Authentication.
Register a stream
POST /api/v1/live/streams registers an HLS manifest URL (master or media playlist). The manifest is validated on registration: an unreachable or invalid playlist returns 422, and a URL that is already registered returns 409.
urlstringbodyrequiredHLS manifest URL, for example https://example.com/live/stream.m3u8.
segment_nintegerbodydefault: 8Number of content segments before an ad break to capture per burst. Determines the analysis window. 1–60.
poll_interval_sintegerbodydefault: 6Seconds between manifest polls. 1–3600.
- cURL
- Python
curl -X POST https://video.pyler.ai/api/v1/live/streams \
-H "Authorization: Bearer $VUF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/live/stream.m3u8", "segment_n": 8, "poll_interval_s": 6}'
import os, requests
BASE = "https://video.pyler.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['VUF_API_KEY']}"}
r = requests.post(f"{BASE}/live/streams", headers=HEADERS, json={
"url": "https://example.com/live/stream.m3u8",
"segment_n": 8,
"poll_interval_s": 6,
})
r.raise_for_status()
stream = r.json()
stream_id = stream["id"]
{
"id": "b7e2c4d6-1f3a-4b5c-9d8e-0a1b2c3d4e5f",
"url": "https://example.com/live/stream.m3u8",
"segment_n": 8,
"aggregate": true,
"status": "active",
"poll_interval_s": 6,
"namespace": null,
"org_id": "org_...",
"segments_seen": 0,
"error": null,
"created_at": "2026-10-20T09:00:00Z",
"updated_at": "2026-10-20T09:00:00Z"
}
The stream record (LiveStreamOut):
statusstringCurrent state: active, paused, stopped or error.
segments_seeninteger | nullTotal media segments observed since creation.
errorstring | nullLast error message, set when status is error.
namespacestring | nullUsable as the namespace filter when listing streams.
The record also carries url, segment_n, poll_interval_s, aggregate, org_id, created_at and updated_at.
List, get, update, delete
| Call | Behaviour | Reference |
|---|---|---|
GET /api/v1/live/streams | Your organization's registered streams, as an array of stream records. Optional ?namespace= filter. | List live streams |
GET /api/v1/live/streams/{stream_id} | One stream by id; 404 if unknown. | Get a live stream |
PATCH /api/v1/live/streams/{stream_id} | Partially update configuration or transition status. | Update a live stream |
DELETE /api/v1/live/streams/{stream_id} | Stop monitoring and remove the stream. 204, no body. | Delete a live stream |
The PATCH body accepts any subset of:
urlstringbodyNew HLS manifest URL. Changing it validates the new manifest synchronously; an unreachable or invalid playlist returns 422.
segment_nintegerbody1–60.
poll_interval_sintegerbody1–3600.
statusstringbodyactive resumes polling, paused stops polling but retains state, stopped ends the stream.
- cURL
- Python
# Pause
curl -X PATCH https://video.pyler.ai/api/v1/live/streams/$STREAM_ID \
-H "Authorization: Bearer $VUF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "paused"}'
# Resume with a wider analysis window
curl -X PATCH https://video.pyler.ai/api/v1/live/streams/$STREAM_ID \
-H "Authorization: Bearer $VUF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "active", "segment_n": 12}'
# Delete
curl -X DELETE https://video.pyler.ai/api/v1/live/streams/$STREAM_ID \
-H "Authorization: Bearer $VUF_API_KEY"
requests.patch(f"{BASE}/live/streams/{stream_id}", headers=HEADERS,
json={"status": "paused"}).raise_for_status()
requests.patch(f"{BASE}/live/streams/{stream_id}", headers=HEADERS,
json={"status": "active", "segment_n": 12}).raise_for_status()
requests.delete(f"{BASE}/live/streams/{stream_id}", headers=HEADERS).raise_for_status()
Deleting a stream permanently deletes all of its bursts and scenes (cascade). Read what you need first.
Bursts
A burst (LiveBurstOut) is the analysis of the content captured before one ad break.
{
"id": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"stream_id": "b7e2c4d6-1f3a-4b5c-9d8e-0a1b2c3d4e5f",
"boundary_sequence": 1842,
"break_detected_at": "2026-10-20T09:14:02Z",
"completed_at": "2026-10-20T09:14:21Z",
"status": "done",
"segment_count": 8,
"wall_ms": 18700,
"summary": "A studio panel discusses the evening's match before the break.",
"genre": ["sports"],
"iab_content_leaf": ["Sports"],
"iab_audience_interest_leaf": [],
"iab_audience_purchase_intent_leaf": [],
"iab_ad_product_leaf": [],
"garm_video": [],
"error": null,
"created_at": "2026-10-20T09:14:02Z",
"updated_at": "2026-10-20T09:14:21Z",
"scenes": [
{
"id": "d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f70",
"segment_index": 0,
"segment_url": "https://example.com/live/seg1835.ts",
"duration_ms": 6000,
"caption": "Three presenters at a desk in front of a stadium backdrop.",
"transcript": "...and that brings us to tonight's line-up.",
"text_on_screen": ["LIVE"],
"valence": 3,
"arousal": 3,
"music_mood": null,
"audio_events": ["speech"],
"garm_categories": [],
"garm_reasoning": [],
"brands": [],
"product_categories_visible": [],
"people": [],
"locations": ["studio"],
"action": ["talking"],
"human_activity": ["presenting"],
"weather": null,
"download_ms": 240,
"caption_ms": 1900,
"status": "done",
"error": null
}
]
}
statusstringBurst status: pending, running, done or failed.
break_detected_at / completed_atdate-timeWhen the boundary was found and when processing finished (completed_at is null until then).
wall_msinteger | nullTotal processing wall-clock time in milliseconds.
summarystring | nullAI-generated one-line summary of the burst content.
garm_videoGarmEntry[]GARM brand-safety assessment for the burst: category, level (high / medium / low), reasoning, time_ranges.
scenesLiveSceneOut[]One analyzed segment each: segment_index, segment_url, duration_ms, caption, transcript, text_on_screen, valence, arousal, music_mood, audio_events, garm_categories, garm_reasoning, brands, product_categories_visible, people, locations, action, human_activity, weather, download_ms, caption_ms, status and error.
The burst also carries id, stream_id, boundary_sequence, segment_count, genre, the IAB leaf lists, error, created_at and updated_at.
Stream-scoped burst endpoints
| Call | Behaviour | Reference |
|---|---|---|
GET /api/v1/live/streams/{stream_id}/bursts | All bursts for the stream, newest first, each with its scenes. ?since= (ISO 8601 date-time) returns only bursts created after that point. | List bursts |
GET /api/v1/live/streams/{stream_id}/bursts/latest | The most recently completed burst. 404 if none has completed yet. | Latest burst |
GET /api/v1/live/streams/{stream_id}/bursts/{burst_id} | One burst within the stream. 404 if it does not belong to this stream or does not exist. | Get a burst |
Account-wide burst endpoints
| Call | Behaviour | Reference |
|---|---|---|
GET /api/v1/live/bursts/latest | The most recently created bursts across all of your organization's streams, as an array. ?since= (ISO 8601 date-time) filters by creation time. | Latest bursts |
GET /api/v1/live/bursts/{burst_id} | Any burst by id, regardless of which stream produced it. | Get a burst by id |
- cURL
- Python
# Latest completed burst for one stream
curl -s https://video.pyler.ai/api/v1/live/streams/$STREAM_ID/bursts/latest \
-H "Authorization: Bearer $VUF_API_KEY"
# Bursts created since a point in time, across all of your streams
curl -s "https://video.pyler.ai/api/v1/live/bursts/latest?since=2026-10-20T09:00:00Z" \
-H "Authorization: Bearer $VUF_API_KEY"
latest = requests.get(f"{BASE}/live/streams/{stream_id}/bursts/latest", headers=HEADERS)
if latest.status_code == 404:
print("no completed burst yet")
else:
burst = latest.json()
print(burst["summary"], [g["category"] for g in burst["garm_video"]])
recent = requests.get(f"{BASE}/live/bursts/latest", headers=HEADERS,
params={"since": "2026-10-20T09:00:00Z"}).json()
Events
GET /api/v1/live/streams/{stream_id}/events is a long-lived application/x-ndjson stream, one JSON object per line, delivering real-time events for one stream. Read it with a client that does not buffer the whole response. Each line has an event field:
event | Meaning |
|---|---|
watching | Current stream state snapshot. |
break_detected | New ad boundary found. |
scene | Segment analysis complete. |
aggregate | Burst aggregation updated. |
burst_done | Burst processing finished. |
keepalive | Idle heartbeat; ignore. |
The connection closes when the stream transitions to stopped or is deleted.
- cURL
- Python
curl -N https://video.pyler.ai/api/v1/live/streams/$STREAM_ID/events \
-H "Authorization: Bearer $VUF_API_KEY"
import json
with requests.get(f"{BASE}/live/streams/{stream_id}/events", headers=HEADERS, stream=True) as r:
r.raise_for_status()
for line in r.iter_lines():
if not line:
continue
evt = json.loads(line)
if evt["event"] == "keepalive":
continue
print(evt["event"], evt)
Events tell you when something happened; bursts hold the data. On burst_done, fetch the burst with GET /api/v1/live/streams/{stream_id}/bursts/latest or GET /api/v1/live/bursts/{burst_id}.
Errors
| Status | When |
|---|---|
404 | Unknown stream_id or burst_id; a burst requested under a stream it does not belong to; bursts/latest before any burst has completed. |
409 | POST /api/v1/live/streams with a URL that is already registered. |
422 | Body failed validation, or the manifest URL (on create or on PATCH with a new url) is unreachable or not a valid playlist. |
All error bodies use the common envelope described in Errors.