Live streams

Create a stream, get an RTMP URL, push to it. When the broadcast ends the recording enters your library automatically.

Create a stream

POST/api/public/v1/livestreams/

Body

KeyTypeSampleDescription
titlerequiredstringWeekly webinarShown in the dashboard and carried onto the recording.
scheduled_atoptionalstring (ISO 8601)2026-09-01T10:00:00ZWhen you intend to go live. Informational — it does not gate pushing.
drm_enabledoptionalbooleanfalseThis stream’s own DRM choice for its recording, overriding your organization’s default. Omit it to inherit that default. Sending true when your plan does not include DRM is refused with DRM_NOT_AVAILABLE (409), not silently ignored. Applies to the recording, not the live broadcast itself.
customTagsoptionalobject{"course": "Physics 101"}Your own key/value pairs, up to 20. Stored on the stream and returned with it. Also accepted as tags.
curl
curl -X POST https://api.streamos.in/api/public/v1/livestreams/ \
  -H 'Authorization: Bearer sk_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Weekly webinar",
    "customTags": {
      "course": "Physics 101",
      "batch": "JEE-2026"
    }
  }'
drm_enabledis left out above on purpose — the common case is inheriting your organization’s own default, which means not sending the field at all. See Overriding DRM for one stream below for when and how to send it.

Response — data.stream

KeyTypeSampleDescription
public_idoptionalstringhtUTel8iPA6The id to use everywhere else in this API.
rtmp_urloptionalstringrtmp://livepush-so…/live/EXSO_1786…?txSecret=…Complete push URL, signature included. Paste into OBS as Server and leave Stream Key blank.
stream_keyoptionalstringEXSO_17869989836405The stream identifier. Appears as channel_id on events about this stream.
push_expires_atoptionalstring (ISO 8601)2026-08-18T20:36:23.644ZWhen the push signature dies — 24 hours after creation. Recreate the stream after that.
hls_urloptionalstringhttps://…/EXSO_1786…_singlehls.m3u8Where viewers watch the live broadcast.
statusoptionalstringREADY_TO_STREAMSCHEDULED for a booking, READY_TO_STREAM once created, ENDED afterwards.
rtmp_server_urloptionalstring | nullrtmp://livepush-so…/live/Server portion of the push URL, for encoders that want it split from the key.
rtmp_server_keyoptionalstring | nullEXSO_17869989836405The streaming service’s own id for the push channel.
push_domainoptionalstring | nulllivepush-so…Host the push URL points at.
hls_urlsoptionalobject | null{"origin": "https://…m3u8"}Every playback URL the service returned, by label.
playback_urloptionalstring | nullnullAlways null here — it is filled in once the recording has a durable manifest. Watch the live broadcast on hls_url until then.
latencyoptionalstringNORMALThe delivery profile. Always NORMAL — it is no longer selectable.
drm_for_recordingoptionalbooleanfalseWhether the recording will be DRM-protected.
tagsoptionalobject | null{"course": "Physics 101"}The pairs supplied at creation. Null if none were sent.

That is the whole response. A stream a second old has no duration, no recording and no start or end time, so those are not in it — read them from GET /livestreams/{public_id}/ once there is something to read.

push_expires_at is 24 hours out. A stream created today cannot be pushed to next week — create it close to when you will use it.

Going live

Point any RTMP encoder at rtmp_url. The signature is already in the URL, so in OBS it goes in Server and Stream Key stays empty.

The broadcast is live as soon as the encoder connects. Viewers watch hls_url.

Scheduling for later

POST/api/public/v1/livestreams/schedule/

Books a slot without creating anything upstream. No RTMP URL and no stream key are issued — a push URL expires a few hours after it is minted, so one created a week ahead would be dead long before the broadcast.

Body

KeyTypeSampleDescription
titlerequiredstringWeekly webinarShown in the dashboard.
scheduled_atrequiredstring (ISO 8601)2026-09-01T10:00:00ZMust be in the future.
drm_enabledoptionalbooleanfalseSame override as on Create a stream. Re-checked against your organization’s config again when you actually start the stream, in case it changed since booking.
customTagsoptionalobject{"course": "Physics 101"}Carried through and forwarded when the stream is started. Also accepted as tags.

The stream comes back with status: "SCHEDULED" and an empty rtmp_url. Start it on the day:

POST/api/public/v1/livestreams/{public_id}/start/

This is the call that reserves a channel and mints the push credentials. It takes no body, returns the same stream with READY_TO_STREAM and a real rtmp_url, and is safe to repeat — a stream that has already started is returned as-is rather than given a second channel.

Trial and concurrency limits apply here, not at scheduling. A booking reserves nothing, so a calendar full of them cannot lock you out of streaming today.

Overriding DRM for one stream

drm_enabled (on both Create a stream and Scheduling for later) is a per-stream override of your organization’s own DRM default, applied to the recording — not the live broadcast, which goes out the same way either way. Omit it and the stream gets whatever your organization is set to. The one direction that is refused: sending truewhen your organization’s plan does not include DRM at all, which comes back DRM_NOT_AVAILABLE (409) rather than being created unprotected.

On a booking made with Scheduling for later, that choice is re-checked again when you actually call start— not just trusted from when you booked it. A stream booked with DRM on stays refused at start time if your organization’s plan no longer includes DRM by then; one booked with no override at all picks up whatever your organization’s default is on the day you start it, same as before this override existed.

curl — create now, this stream's recording only
curl -X POST https://api.streamos.in/api/public/v1/livestreams/ \
  -H 'Authorization: Bearer sk_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Weekly webinar",
    "drm_enabled": true
  }'
curl — book for later, same override
curl -X POST https://api.streamos.in/api/public/v1/livestreams/schedule/ \
  -H 'Authorization: Bearer sk_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Weekly webinar",
    "scheduled_at": "2026-09-25T10:00:00Z",
    "drm_enabled": true
  }'

When the stream ends

A stream ends on its own when the encoder stops pushing. You can also end it from your side:

POST/api/public/v1/livestreams/{public_id}/end/

This moves our record to ENDED and frees the concurrency slot. It does not stop a broadcast that is still being pushed — stop that in your encoder. Repeating the call is a no-op.

A stream nobody ever pushed to is closed automatically 24 hours after it was created, so an abandoned one does not hold a slot forever.

Two things then happen, minutes apart:

Sequence

KeyTypeSampleDescription
LIVESTREAM_RECORDING_READYoptionaleventrecording.asset_id = "MflonNqhUg9"The stream is marked ENDED and its recording enters the library. Still processing.
VIDEO_TRANSCODING_COMPLETEDoptionaleventvideo.kind = "LIVESTREAM_RECORDING"The recording finished transcoding and is playable at its own quality ladder. stream.id on this event is the stream it came from, so you can match it without keeping your own asset-to-stream map.
The recording is a normal asset. Fetch it with GET /assets/{recording.asset_id}/ like any uploaded video.

Encoding settings

drmis no longer part of these requests. Whether a recording is protected — along with its resolution ladder, subtitles and downloads — is a setting on your organization, configured once by the StreamOS team and used by every stream recording and every upload alike.

This is a breaking change. A drm field left in your request is ignored rather than rejected. Remove it.

An organization with no encoding settings cannot start a stream — TRANSCODE_NOT_CONFIGURED (409), and nothing is created. Scheduling one is still allowed; the check happens when it is started, which is when the stream is actually created upstream.

Listing and fetching

GET/api/public/v1/livestreams/
GET/api/public/v1/livestreams/{public_id}/
curl
curl https://api.streamos.in/api/public/v1/livestreams/htUTel8iPA6/ \
  -H 'Authorization: Bearer sk_live_…'

Where to find a stream’s id

Every stream endpoint takes the public_id — the short id the create call returned, like htUTel8iPA6. In the dashboard it is on Live, under the Live, Scheduled and Completed tabs — open a stream and copy the id from its page, or copy it straight from the ID column.

stream_key is not the id. It looks like one — EXSO_17883416719038 — but it belongs to the encoder: it is what you push with, and what arrives as channel_id on webhook events. Putting it in a URL returns 404 NOT_FOUND, because nothing is addressed by it. The id is the short public_id.

Status values

SCHEDULED → READY_TO_STREAM → ENDED. There is no LIVE status: nothing tells us when an encoder connects, so a stream in progress still reads READY_TO_STREAM.