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_idWith video_template_id
What Zentag doesStores metadataIngests the feed and runs analysis
url requiredNoYes for recorded and pull input types
Produces clips automaticallyNoYes, when the template has AI enabled
Ready when 201 returnsYesNo - 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

The input_type decides whether you supply a source URL or Zentag supplies you an
ingest target:

input_typeurl on create
RECORDEDRequired - the file to process
RTMP_PULL, SRT_PULL, HLS_PULLRequired, and must use that protocol
RTMP_PUSH, SRT_PUSHNot 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:

ParameterNotes
statusOne of pending, scheduled, processing, completed, failed, cancelled, delayed
is_livetrue or false
video_typelive or recorded
category, competition_idExact match
searchCase-insensitive substring of the title
created_after, created_beforeISO 8601
match_date_after, match_date_beforeISO 8601
sort_bylatest (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 id on the resource, not a Mongo ObjectId. Template ids
    are the id from GET /stream-templates.
  • Every id on a stream is a Zentag id. Third-party sports-data provider identifiers
    are deliberately not exposed.
  • POST /streams/{id}/end marks a stream complete and emits stream.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.

Did this page help you?