Streams
Register a stream for metadata, or hand Zentag a feed to ingest and analyse.
A stream is the match or session that clips and highlights belong to. POST /streams
supports two quite different uses, and which one you get depends on a single field.
Without video_template_id | With video_template_id | |
|---|---|---|
| What Zentag does | Stores metadata | Ingests the feed and runs analysis |
url required | No | Yes for recorded and pull input types |
| Produces clips automatically | No | Yes, when the template has AI enabled |
Ready when 201 returns | Yes | No - provisioning continues |
Both require the streams:write scope.
Metadata-only streams
Use this when you are producing clips yourself and need something to attach them to,
or when you are mirroring a fixture list into Zentag.
curl -X POST "https://studio.zentag.ai/api/v1/partner/streams" \
-H "Authorization: Bearer $ZENTAG_SECRET_KEY" \
-H "Idempotency-Key: stream-fixture-88213" \
-H "Content-Type: application/json" \
-d '{
"title": "Rovers vs City",
"category": "football",
"team1_id": "team_rovers",
"team2_id": "team_city",
"competition_id": "comp_prem",
"match_date": "2026-09-14T14:00:00.000Z"
}'The stream is created with status: "pending" and stays there. Nothing is ingested,
and no clips appear on their own.
team1_id, team2_id and competition_id are Zentag catalog ids. Create or look
them up through GET /teams and GET /competitions first — they are returned on
read, so you can rely on them round-tripping.
Match ids
Search fixtures with GET /matches, then pass match_id, team1_id, and team2_id
on stream create or update. This is the same fixture id Studio stores when you pick a
match in Add Video Feed.
curl "https://studio.zentag.ai/api/v1/partner/matches?competition_id=comp_prem&search=rovers" \
-H "Authorization: Bearer $ZENTAG_SECRET_KEY"{
"object": "list",
"data": [
{
"object": "match",
"match_id": "12345678",
"name": "Rovers vs City · 2026-09-14",
"team1_id": "team_rovers",
"team2_id": "team_city"
}
],
"has_more": false,
"next_cursor": null
}Fetch one fixture by id (recommended when you already know the match):
curl "https://studio.zentag.ai/api/v1/partner/matches?competition_id=comp_prem&match_id=12345678" \
-H "Authorization: Bearer $ZENTAG_SECRET_KEY"curl -X POST "https://studio.zentag.ai/api/v1/partner/streams" \
-H "Authorization: Bearer $ZENTAG_SECRET_KEY" \
-H "Idempotency-Key: stream-fixture-88213" \
-H "Content-Type: application/json" \
-d '{
"title": "Rovers vs City",
"category": "football",
"team1_id": "team_rovers",
"team2_id": "team_city",
"competition_id": "comp_prem",
"match_id": "12345678",
"match_date": "2026-09-14T14:00:00.000Z"
}'Prefer competition_id on the search call so results come from that competition's
season schedule (with team ids). Without it, Zentag searches cached match metadata
and streams that already have a match_id.
Ingested streams
Supplying video_template_id asks Zentag to take the feed and process it.
1. Pick a template
A template fixes the input type, resolution, frame rate, audio tracks and whether AI
analysis runs. List the ones available to you:
curl "https://studio.zentag.ai/api/v1/partner/stream-templates" \
-H "Authorization: Bearer $ZENTAG_SECRET_KEY"{
"object": "list",
"data": [
{
"object": "stream_template",
"id": "665f1a2b3c4d5e6f70819200",
"name": "Football 1080p30 - RTMP push",
"input_type": "RTMP_PUSH",
"width": 1920,
"height": 1080,
"fps": 30,
"ai_enabled": true,
"audio_languages": [{ "code": "en", "label": "English", "track": 1 }]
}
],
"has_more": false,
"next_cursor": null
}Only templates belonging to your organization are listed, and only listed templates
are accepted on create. Ask your Zentag contact if you need one added or changed.
2. Match url to the template's input type
url to the template's input typeThe input_type decides whether you supply a source URL or Zentag supplies you an
ingest target:
input_type | url on create |
|---|---|
RECORDED | Required - the file to process |
RTMP_PULL, SRT_PULL, HLS_PULL | Required, and must use that protocol |
RTMP_PUSH, SRT_PUSH | Not used - read GET /streams/{id}/ingest instead |
A url whose protocol does not match the input type is rejected with 400, so this
surfaces immediately rather than as a silent ingest failure later.
3. Create the stream
For a live feed, also send the schedule. Zentag brings the pipeline up shortly before
scheduled_start_at and tears it down after the end:
curl -X POST "https://studio.zentag.ai/api/v1/partner/streams" \
-H "Authorization: Bearer $ZENTAG_SECRET_KEY" \
-H "Idempotency-Key: stream-fixture-88213-live" \
-H "Content-Type: application/json" \
-d '{
"title": "Rovers vs City",
"category": "football",
"video_template_id": "665f1a2b3c4d5e6f70819200",
"video_type": "live",
"scheduled_start_at": "2026-09-14T14:00:00.000Z",
"scheduled_duration_hours": 2,
"timezone": "Europe/London"
}'Send scheduled_end_at or scheduled_duration_hours (1–8), not necessarily both.
timezone is an IANA name recorded for display; the schedule itself is resolved from
the absolute instants, so the times above mean the same thing regardless of what you
send for timezone.
Omit video_type (or send recorded) for a file, in which case no schedule is needed
and processing starts right away.
4. Wait for provisioning
The 201 does not mean the stream is ready. Provisioning and analysis continue
after the response returns, so the stream comes back with a non-terminal status:
{
"object": "stream",
"id": "s_9f2c1d4e",
"title": "Rovers vs City",
"status": "scheduled",
"is_live": false,
"video_type": "live",
"scheduled_start_at": "2026-09-14T14:00:00.000Z",
"scheduled_end_at": "2026-09-14T16:00:00.000Z",
"timezone": "Europe/London",
"clips_count": 0,
"highlights_count": 0
}Subscribe to stream.started and stream.ended, or poll GET /streams/{id} and read
status. Do not treat the create response as confirmation that ingest is running.
5. Push the feed, for push input types
Once provisioning finishes, fetch your ingest target:
curl "https://studio.zentag.ai/api/v1/partner/streams/s_9f2c1d4e/ingest" \
-H "Authorization: Bearer $ZENTAG_SECRET_KEY"{
"object": "stream_ingest",
"stream_id": "s_9f2c1d4e",
"input_type": "RTMP_PUSH",
"protocol": "RTMP",
"ingest_server": "rtmp://ingest.zentag.ai/live",
"stream_key": "sk_live_9f2c1d4e7a8b",
"ingest_url": "rtmp://ingest.zentag.ai/live/sk_live_9f2c1d4e7a8b",
"pull_source_url": null
}This endpoint requires streams:write rather than streams:read, because
stream_key authorizes publishing to the stream. Treat it as a secret.
The fields are null until provisioning completes retry rather than treating an
empty response as an error.
Reading streams
GET /streams supports filtering and ordering:
| Parameter | Notes |
|---|---|
status | One of pending, scheduled, processing, completed, failed, cancelled, delayed |
is_live | true or false |
video_type | live or recorded |
category, competition_id | Exact match |
search | Case-insensitive substring of the title |
created_after, created_before | ISO 8601 |
match_date_after, match_date_before | ISO 8601 |
sort_by | latest (default) or oldest |
Prefer status over is_live when you care about progress: is_live is false for a
stream that finished, one that failed and one that has not started, which are three
very different situations.
Streams are also available through GET /media, alongside clips and highlights, when
you want one list across all three. See Media library filters.
Notes
- Stream ids are the short
idon the resource, not a Mongo ObjectId. Template ids
are theidfromGET /stream-templates. - Every id on a stream is a Zentag id. Third-party sports-data provider identifiers
are deliberately not exposed. POST /streams/{id}/endmarks a stream complete and emitsstream.ended. It is
idempotent: ending an already-ended stream does not emit a second event.- Deleted streams are not returned, and cannot be updated, ended, or used as the
target for a new clip or highlight.
Updated about 2 months ago
