Streams API
Create and manage live streams. Protocols: RTMP (default), SRT, WebRTC, HTTP (pull), FILE (stream from video). You receive protocol-specific ingest URLs and stream keys from the API; playback is HLS through the CDN. Use Recording for DVR-style capture.
Supported Protocols
| Protocol | Description | Requirements | Ingest URL Type |
|---|---|---|---|
RTMP | Real-Time Messaging Protocol - industry standard for live streaming | None (default) | RTMP URL + Stream Key |
SRT | Secure Reliable Transport - ultra-low latency, tolerant of packet loss | Optional: srtConfig (mode, latency, passphrase) | SRT URL + Stream ID |
WebRTC | Web Real-Time Communication - real-time browser streaming | Optional: isCamera (true for browser camera, false for external source) | WHIP/WHEP URLs |
HTTP | HTTP Pull - pull from external HTTP/RTSP source | Required: httpSourceUrl | HTTP source URL |
FILE | FILE - stream from uploaded video file | Required: fileSourceUrl — a public http(s) URL, checked when the stream starts | RTMP URL (for file streaming) |
Visibility Options
A stream has five access states. Send either spelling on POST /streams or PATCH /streams/:id (case-insensitive); responses carry the stored name. Any other value is refused 400 INVALID_VISIBILITY. Paid access and subscriptions are separate (isPaid, price) — SUBSCRIBERS and TIER_EXCLUSIVE are video visibilities, not stream ones.
| Stored | Also accepted | Meaning |
|---|---|---|
PUBLIC | public | Anyone can watch. Default on create. |
LINK_ONLY | unlisted | Anyone with the link can watch. |
ACCESS_KEY | bykey | The viewer must enter the access key. Requires accessKey in the same request (400 INVALID_ACCESS_KEY otherwise). |
REGISTERED | registered | The viewer fills in a registration form first. |
PRIVATE | private | Only the owner. |
Create a Stream
Example - RTMP (default)
POST https://api.dcast.pro/api/v1/streams Authorization: Bearer YOUR_API_KEY Content-Type: application/json {"title": "My Live Stream", "description": "Optional", "visibility": "PUBLIC", "protocol": "RTMP"}Example - SRT
POST https://api.dcast.pro/api/v1/streams Authorization: Bearer YOUR_API_KEY Content-Type: application/json {"title": "SRT Stream", "protocol": "SRT", "srtConfig": {"mode": "caller", "latency": 500, "passphrase": "optional_secret"}}srtConfig.latency is optional and is measured in milliseconds. Omit it and the card is created with the value the connection settles on; send a smaller one and it is raised to that value rather than refused, so read srtConfig.latency back off the response instead of assuming what you sent. The band in force is published in the OpenAPI document — https://api.dcast.pro/api/v1/docs/openapi.json, undersrtConfig.latency (default and maximum) — and that document is where the numbers on this page come from.
Example - WebRTC
POST https://api.dcast.pro/api/v1/streams Authorization: Bearer YOUR_API_KEY Content-Type: application/json {"title": "WebRTC Stream", "protocol": "WebRTC", "isCamera": false}Example - HTTP Pull
POST https://api.dcast.pro/api/v1/streams Authorization: Bearer YOUR_API_KEY Content-Type: application/json {"title": "HTTP Pull Stream", "protocol": "HTTP", "httpSourceUrl": "https://example.com/stream.m3u8"}Example - FILE
POST https://api.dcast.pro/api/v1/streams Authorization: Bearer YOUR_API_KEY Content-Type: application/json {"title": "File Stream", "protocol": "FILE", "fileSourceUrl": "https://example.com/video.mp4"}title is required; you may send name instead (same meaning). protocol: RTMP, SRT, WEBRTC, HTTP (requires httpSourceUrl), FILE (requires fileSourceUrl). Response includes ingest targets for the chosen protocol.
Ingest URL formats — what to give your encoder
GET /streams/:id returns ingest targets under data.ingest. Hostnames are the canonical DNS pool entrypoint a.dcast.pro (no worker hostnames). SRT note: the streamid param is emitted in its literal form (#!::r=live/…,m=publish) — it is not URL-encoded, because libsrt does not URL-decode the streamid value (it forwards the literal bytes to the server). An encoded streamid (%23!%3A%3A…) would reach the server as a literal %23… stream name and silently fail to publish.
{
"ingest": {
"rtmp": "rtmp://a.dcast.pro/live/sk_live_<keysuffix>",
"srt": "srt://a.dcast.pro:10080?streamid=#!::r=live/sk_live_<…>,m=publish&mode=caller&latency=500000",
"srtDisplay": "srt://a.dcast.pro:10080?streamid=#!::r=live/sk_live_<…>,m=publish&mode=caller&latency=500000",
"whip": "https://a.dcast.pro/rtc/v1/whip/?app=live&stream=sk_live_<…>"
}
}ingest.srt and ingest.srtDisplay are present on every GET /streams/:id response and now hold the same publishable literal value (srtDisplay is kept as an explicit alias for backward compatibility). Feed either one straight to your encoder — no decoding or rewriting required.
- RTMP — paste
ingest.rtmpas the Server URL; encoders that split server / stream key should split at the last/(server =rtmp://a.dcast.pro/live, key =sk_live_…). - SRT in encoders (OBS, ffmpeg, vMix, …) — paste
ingest.srt(or the identicalingest.srtDisplay) verbatim. The literalstreamid(#!::r=live/…,m=publish) is what must end up on the wire; encoders do not URL-decode it, so do not percent-encode the#/!/:/,characters yourself — an encoded streamid (%23%21%3A%3A…) reaches the server as a literal stream name and silently fails to publish (no signal). - WHIP —
POSTSDP offers toingest.whip; OBS 30+ supports this natively (Settings → Stream → Service: WHIP).
SRT quick-test with ffmpeg:
# Paste ingest.srt verbatim — the literal streamid must reach the server un-encoded.
ffmpeg -re -f lavfi -i testsrc2=size=1280x720:rate=30 \
-f lavfi -i sine=frequency=440:sample_rate=44100 \
-c:v libx264 -preset veryfast -b:v 1500k -g 60 \
-c:a aac -b:a 96k \
-f mpegts "srt://a.dcast.pro:10080?streamid=#!::r=live/sk_live_<…>,m=publish&mode=caller&latency=500000"The latency token of an SRT URL is read by ffmpeg and libsrt as microseconds, which is why it is not the same digits as the milliseconds you send in srtConfig. Paste ingest.srtverbatim and you never have to convert it yourself.
SRTLA (bonded)
SRTLA bonds several internet connections — for example two cellular modems, or cellular plus Wi-Fi — into one SRT stream. If one connection drops, the broadcast continues over the others. SRTLA is another way into an ordinary SRT stream: create the stream with protocol SRT, nothing else needs to be enabled. From the receiver on it is a normal SRT publish: preview as soon as the signal arrives, viewer HLS after POST /streams/:id/start.
Where to take the address
In the stream settings of an SRT stream, the connection block has a “SRTLA (bonding)” section: the SRTLA address and, under it, the “Moblin” form of the same address. Copy them from there. The port is 5000/UDP, not the SRT port 10080; the stream ID is the same publish ID as in the stream's SRT address.
The API does not return a separate SRTLA field. Build the address from ingest.srt: the scheme srtla://, the same host, port 5000 and the same streamid. The first line below is the plain form; the second is the same address with the stream ID percent-encoded, for Moblin.
srtla://a.dcast.pro:5000?streamid=#!::r=live/<key>,m=publish
srtla://a.dcast.pro:5000?streamid=%23%21%3A%3Ar%3Dlive%2F<key>%2Cm%3DpublishPhone: Moblin and IRL Pro
- Moblin (iOS) sends SRTLA natively. Paste the Moblin form of the address: Moblin reads # as the start of a URL fragment, so the plain form would leave the stream ID empty.
- IRL Pro (Android) sends SRTLA natively. Use the srtla:// address in Caller mode; if the app asks for the stream ID in a separate field, enter it in the plain form (#!::r=live/…,m=publish).
Computer: OBS
OBS does not bond connections by itself. It can send SRT to a local srtla_send (srt://127.0.0.1:<local port>, with the stream's stream ID), and srtla_send spreads that stream over your connections to the host from the address, port 5000. A computer sender is coming with our bonding app.
Set the SRT latency to 2000–3000 ms: bonded cellular connections need the larger buffer.
Related Documentation
For monetization (tiers, checkout sessions, purchases, read-only payout history) see Monetization API. For analytics and metrics see Analytics API.
Endpoints
Streams
| Method | Endpoint | Description |
|---|---|---|
| GET | /streams | List streams (query: page, limit; status filter: LIVE | OFFLINE (by isLive, as the rows report) or a stored state INACTIVE | HAS_SIGNAL | STARTING | ACTIVE | STOPPING | ERROR; any other value is refused with 400) |
| POST | /streams | Create stream |
| GET | /streams/:id | Details and ingest URLs. status is canonical LIVE | OFFLINE (sourced from isLive; matches the value returned by /streams/:id/status). |
| GET | /streams/:id/status | Lightweight status (LIVE | OFFLINE, viewers, duration). Also returns hasSignal (SRS-detected ingest) and isLive — the pair drives the 3-state go-live button. See Going live below. |
| GET | /streams/:id/viewers | Current viewer count + last-N session snapshot (country / deviceType / watchDuration) |
| GET | /streams/:id/preview-url | Live HTTP-FLV preview URL for flv.js playback (the same URL the cabinet renders). See Live preview player below. |
| GET | /streams/:id/thumbnail | Live preview thumbnail (Content-Type: image/jpeg). See Thumbnail below. |
| PATCH | /streams/:id | Update title, description, visibility, isPaid, price (legacy aliases paidAccess, paidAccessPrice also accepted; both branches converge on the same fields). Empty body returns 400 BAD_REQUEST. Returns the same external shape as GET /streams/:id (id, title, status, isPaid, price, ingest, playback, thumbnails, …) so a PATCH→store→render round-trip uses identical keys to the initial GET. requiredTierId / donationsEnabled on streams are not yet writable — a schema migration is required. |
| POST | /streams/:id/start | Go live — start the multibitrate-HLS broadcast so viewers' master.m3u8 serves (parity with the cabinet «В эфир» button). Preview works without this; viewer playback requires it. See Going live below. |
| POST | /streams/:id/end | Stop the broadcast (flips isLive=false). Same handler as POST /streams/:id/stop; both check the streams:operate permission. |
| DELETE | /streams/:id | Permanently delete the stream and all recordings (see Delete below) |
| POST | /streams/:id/record/start | Start recording |
| POST | /streams/:id/record/stop | Stop recording |
| GET | /streams/:id/record/status | Recording status |
| GET | /streams/:id/chat | List recent chat messages (paginated read) |
| POST | /streams/:id/chat | Post a chat message as the API key owner |
| DELETE | /streams/:id/chat/:messageId | Hard-delete a single chat message (author or stream owner) |
| GET | /streams/:id/chat/stream | Server-Sent Events live tail (heartbeat 30s) |
Update stream metadata — PATCH /streams/:id
PATCH is partial: send only the fields you want to change. Writable fields are title, description, visibility (see Visibility Options above), isPaid (boolean), and price (number, USD). Legacy aliases paidAccess and paidAccessPrice are accepted and converge on the same canonical fields. The response is the same external shape as GET /streams/:id so a PATCH→store→render round-trip uses identical keys to the initial GET.
curl -s -X PATCH \
-H "Authorization: Bearer pk_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"isPaid": true, "price": 9.99}' \
https://api.dcast.pro/api/v1/streams/{streamId}
# Response (truncated)
{
"success": true,
"data": {
"id": "stream_id",
"title": "My Stream",
"status": "OFFLINE",
"isPaid": true,
"price": 9.99,
"ingest": { "...": "..." },
"playback": { "...": "..." }
}
}Empty body — canonical “no writable field” envelope
If you PATCH with an empty body or only unsupported keys, the response is 400 BAD_REQUEST with an explicit writables-list plus a deferred-features note. This is the canonical shape for “no updatable field in body” across the partner API — you can use the message string verbatim to drive a tooltip or inline form-error in your integration.
# Request
curl -s -X PATCH \
-H "Authorization: Bearer pk_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{}' \
https://api.dcast.pro/api/v1/streams/{streamId}
# Response (HTTP 400)
{
"success": false,
"error": {
"code": "BAD_REQUEST",
"message": "No updatable fields in body. Writable: title, description, visibility, isPaid, price (aliases: paidAccess, paidAccessPrice). requiredTierId / donationsEnabled on streams require a schema migration — not yet supported."
}
}requiredTierId (tier-gated streams) and donationsEnabled are deferred: they are valid on videos but require a schema migration before they are writable on streams. They will surface as new fields in this same envelope once shipped — no breaking change to existing integrations.
Delete
DELETE /streams/:id permanently removes the stream and every recording, thumbnail, and chat message attached to it. The operation is irreversible. If the broadcast is still live we stop ingest first, then purge artifacts. Use POST /streams/:id/end if you only want to end the broadcast and keep the stream record for later replay.
curl -s -X DELETE \
-H "Authorization: Bearer pk_YOUR_KEY_HERE" \
https://api.dcast.pro/api/v1/streams/{streamId}
# Response
{
"success": true,
"data": {
"id": "stream_id",
"type": "complete-deletion",
"deletedAt": "2026-05-26T12:34:56.000Z"
}
}type is complete-deletion, or soft-delete (with hasVideoFiles: true and videoFilesCount) when the stream has recordings. When the purge finishes in the background the answer is 202 with type: "accepted" and canonicalCleanup: "in_progress" — the stream is already deleted; do not retry.
Foreign stream id → 404 NOT_FOUND. Subsequent reads of the deleted id also return 404 NOT_FOUND.
Live preview player
GET /streams/:id/preview-url returns the canonical HTTP-FLV preview URL. The hostname is always api.dcast.pro — partners never see a worker hostname. When the player hits that URL we 302-redirect to the current preview worker (worker rebalance is transparent; flv.js / mpegts.js follow redirects natively). The same URL is what the cabinet plays inside its «Preview» pane and what the cabinet WebSocket event stream:preview_ready emits, so partner UIs and the cabinet stay in lockstep.
Preview availability is keyed on SRS-detected ingest, not the user-driven «go live» flag. The moment your encoder starts pushing (RTMP / SRT / WHIP), the backend flips the stream's hasSignal to true and /preview-url starts returning 200. This lets encoder operators frame the camera and check audio levels before publishing the stream to viewers (the typical “preview-before-going-live” UX). You do not need to call any «go live» endpoint for preview to work.
Viewer HLS playback is different. Preview needs no go-live, but for actual viewers to watch — for the public master.m3u8 to go 404→200 — you must call POST /streams/:id/start (the exact parity of the cabinet «В эфир» button). See Going live below. Don't read “preview needs no go-live” as “viewers can watch without /start” — they can't.
| HTTP | data.reason | When you see it |
|---|---|---|
200 | — | SRS detects an active publish (hasSignal=true) AND the preview transcoder mount is up. httpFlvUrl is playable. |
202 | stream_not_publishing | No ingest yet (encoder hasn't started, or stopped pushing >~90s ago). |
202 | preview_warming_up | Ingest received but the worker-side preview transcoder is still spinning up (transient, normally <2s). |
404 | — | Wrong owner or unknown id (single shape; we never reveal whether a stream id exists outside your account). |
NOT tied to stream.isLive. isLive is a user-controlled label («is this broadcast announced to viewers?») and can be false while preview is fully playable. Conversely, isLive=true after the encoder cuts will return 202 stream_not_publishing until ingest resumes.
curl -s -H "Authorization: Bearer pk_YOUR_KEY_HERE" \
https://api.dcast.pro/api/v1/streams/{streamId}/preview-url
# 200 — SRS sees the encoder publish AND the preview transcoder is up.
# (No "go live" call required FOR PREVIEW — the stream's own isLive flag is
# independent. The "isLive": true here means "preview is playable right now".
# For VIEWER HLS playback (master.m3u8 404->200) you still must call
# POST /streams/{streamId}/start — see "Going live" below.)
{
"success": true,
"data": {
"httpFlvUrl": "https://api.dcast.pro/api/v1/streams/{streamId}/preview.flv?token=eyJ...",
"expiresAt": "2026-05-27T17:43:56.103Z",
"isLive": true
}
}
# 202 — no SRS signal yet (encoder hasn't started, or stopped pushing
# more than ~90s ago). Caller may poll every 5–15s.
{
"success": true,
"data": {
"httpFlvUrl": null,
"expiresAt": null,
"isLive": false,
"reason": "stream_not_publishing"
}
}
# 202 — SRS sees the publish but the worker-side preview transcoder has
# not yet published the _preview mount back to SRS (transient, normally
# <2s after the first publish packet). Retry after 1–2s.
{
"success": true,
"data": {
"httpFlvUrl": null,
"expiresAt": null,
"isLive": false,
"reason": "preview_warming_up"
}
}
# 404 — wrong owner or stream does not exist (single shape; we never reveal
# whether a stream id exists when it is not yours).
{ "success": false, "error": { "code": "NOT_FOUND", "message": "Stream not found" } }Token lifetime
The ?token=... in the URL is a short-lived signed JWT (~5 minutes, master-token TTL — not 1 hour, despite the resource name “preview”). Use the returned expiresAt to schedule a refresh; we recommend re-fetching the URL on a ≤ 4-minute cadence for long-running preview sessions. The same JWT secret signs every preview / segment / key token across the platform, so no per-worker handshake is needed.
flv.js example (browser)
import mpegts from 'mpegts.js'; // or 'flv.js' — interchangeable
async function attachLivePreview(videoEl, streamId, apiKey) {
const r = await fetch(
'https://api.dcast.pro/api/v1/streams/' + streamId + '/preview-url',
{ headers: { Authorization: 'Bearer ' + apiKey } }
);
const { data } = await r.json();
if (!data.isLive) {
// Not publishing yet — display placeholder and poll later.
setTimeout(() => attachLivePreview(videoEl, streamId, apiKey), 10_000);
return;
}
const player = mpegts.createPlayer({ type: 'flv', url: data.httpFlvUrl, isLive: true });
player.attachMediaElement(videoEl);
player.load();
player.play();
// Refresh the URL before the JWT expires so playback survives long sessions.
const refreshInMs = Math.max(
60_000,
new Date(data.expiresAt).getTime() - Date.now() - 60_000
);
setTimeout(() => {
player.destroy();
attachLivePreview(videoEl, streamId, apiKey);
}, refreshInMs);
}Works for FULL_ACCESS, STANDARD, and READ_ONLY pk_* keys. Foreign or deleted stream ids return 404 NOT_FOUND.
Going live — POST /streams/:id/start
There are two distinct states for a stream, and they map to two distinct fields:
- Preview is keyed on
hasSignal— SRS-detected ingest. The moment your encoder pushes (RTMP / SRT / WHIP),hasSignalflipstrueand/preview-urlserves. No go-live call is needed for preview. - Going live (viewer HLS) is keyed on
isLive— a user-driven flag set only byPOST /streams/:id/start. This starts the multibitrate-HLS transcoder so the publicmaster.m3u8goes404→200and viewers can actually watch. This is the exact parity of the cabinet «В эфир» button.
POST /streams/:id/start is protocol-agnostic: it is the same call for RTMP, SRT, and WHIP. Every supported protocol is transmuxed to RTMP upstream and feeds the same shared preview + HLS-transcoder start, so there is no per-protocol go-live difference.
curl -s -X POST \
-H "Authorization: Bearer pk_YOUR_KEY_HERE" \
https://api.dcast.pro/api/v1/streams/{streamId}/start
# Response — broadcast started, viewers' master.m3u8 now serves 200
{
"success": true,
"data": {
"id": "stream_id",
"isLive": true,
"hasSignal": true,
"status": "ACTIVE",
"startedAt": "2026-06-04T17:43:56.103Z",
"message": "Broadcast started"
}
}Stop the broadcast with POST /streams/:id/stop (same handler as POST /streams/:id/end; both check the streams:operate permission). It stops the transcoder, flips isLive=false, sets status=INACTIVE, and records endedAt (plus duration if the broadcast had started):
curl -s -X POST \
-H "Authorization: Bearer pk_YOUR_KEY_HERE" \
https://api.dcast.pro/api/v1/streams/{streamId}/stop
# Response
{
"success": true,
"data": {
"message": "Broadcast stopped",
"isLive": false,
"hasSignal": false,
"endedAt": "2026-06-04T17:51:02.000Z"
}
}Note: hasSignal in the stop response may still be true if the encoder is still publishing — in that case the go-live button returns to the “Go Live” (ready) state, not “Waiting for signal”.
The 3-state go-live button
Combine the hasSignal + isLive pair (both surfaced on GET /streams/:id/status, POST /start, POST /stop | /end, and the SSE status payload) to render a 3-state button:
hasSignal / isLive | Button state | Meaning |
|---|---|---|
hasSignal=false | “Waiting for signal” (Go Live disabled) | No encoder yet — nothing to broadcast. |
hasSignal=true, isLive=false | “Go Live” (enabled, ready) | Preview is live; viewers are not yet served. Calling POST /start goes on air. |
isLive=true | “On Air / Stop” | Broadcasting; viewers' master.m3u8 is 200. POST /stop ends it. |
Error states
400 BAD_REQUEST— the stream has nostreamKey(nothing to transmux/transcode).404 NOT_FOUND— foreign or unknown stream id (owner-scoped; we never reveal whether an id exists outside your account).502withcode: TRANSCODER_START_FAILED— the multibitrate-HLS transcoder would not start. Retry; if it persists, check that ingest is healthy (hasSignal=true).
Thumbnail
GET /streams/:id/thumbnail returns the live preview frame, Content-Type: image/jpeg. Query params:
?file=thumbnail.jpg|thumbnail_6.jpg|thumbnail_11.jpg— named variant?variant=0|1|2— index (0 = instant, 1 = ~6s ago, 2 = ~11s ago)- Both omitted — the stream's
selectedPreviewIndex
Errors return the canonical envelope:
{ "success": false, "error": { "code": "NOT_FOUND", "message": "Stream thumbnail not available" } }404 NOT_FOUND semantics — the same code covers two states: (a) the stream id is invalid or not owned by the calling key, and (b) the stream exists but no thumbnail file has been captured yet (typical for an INACTIVE stream that has never gone live). Treat 404 as “no image available”; the response body is the same for both cases. Poll GET /streams/:id/status for the canonical live state.
For an OFFLINE stream GET /streams/:id always returns thumbnails: { live: null, latest: null } (never an empty object). When live === null no live preview is available; latest, if non-null, is a CDN URL to the most recent recorded frame.
Viewers
Real-time and historical viewer telemetry for streams you own. Returns aggregate count plus a sanitized session snapshot — no IP, no user-agent, no internal infrastructure identifiers.
curl -s -H "Authorization: Bearer pk_YOUR_KEY_HERE" \
"https://api.dcast.pro/api/v1/streams/{streamId}/viewers?limit=50"
# Response
{
"success": true,
"data": {
"streamId": "...",
"isLive": true,
"currentViewers": 142,
"recentSessions": [
{
"country": "United States",
"countryCode": "US",
"deviceType": "desktop",
"watchDurationSec": 421,
"viewedAt": "2026-05-25T18:42:11.000Z"
}
]
}
}Foreign stream id → 404 NOT_FOUND. limit (1–200, default 50) is the number of most recent sessions returned; there is no pagination.
Live tails (SSE) — viewer count + status
Server-Sent Events streams for partner UIs that need realtime updates without polling. Both endpoints are creator-scoped; foreign / non-existent stream ids return 404 NOT_FOUND. Connection is closed on client disconnect; reconnect with backoff. Heartbeat comment line : heartbeat <ms> every 30s. If the stream stops belonging to the key owner an event: revoked is sent and the connection closes.
GET /streams/:id/status/stream
curl -N -H "Authorization: Bearer pk_YOUR_KEY_HERE" \
"https://api.dcast.pro/api/v1/streams/{streamId}/status/stream"
# Event flow
event: ready
data: {"streamId":"…"}
# First reading, then one event per change (checked every 3s)
event: status
data: {"status":"OFFLINE","isLive":false,"hasSignal":false,"viewersCount":0,"startedAt":null,"endedAt":null,"durationSeconds":null}
# Encoder starts pushing — signal arrives, button flips "waiting" -> "ready"
event: status
data: {"status":"OFFLINE","isLive":false,"hasSignal":true,"viewersCount":0,"startedAt":null,"endedAt":null,"durationSeconds":null}
# After POST /start — broadcast on air, viewers' master.m3u8 serves
event: status
data: {"status":"LIVE","isLive":true,"hasSignal":true,"viewersCount":42,"startedAt":"…","endedAt":null,"durationSeconds":null}
event: status
data: {"status":"OFFLINE","isLive":false,"hasSignal":false,"viewersCount":0,"startedAt":"…","endedAt":"…","durationSeconds":4521}Each status payload now includes hasSignal, and hasSignal is part of the change-signature — so a signal arrive or drop is pushed in realtime (button waiting↔ready), not only an isLive flip. Combine hasSignal + isLive to drive the 3-state go-live button (see Going live). The API never reads a key from the query string, and a browser EventSource cannot send headers: open the tail from your server (with Authorization or X-API-Key) and relay it to the browser.
Prefer push over a held SSE connection? A stream.broadcast_started webhook fires the moment this stream goes live (the same isLive false→true transition driven by POST /streams/:id/start or the cabinet «В эфир» button), and stream.broadcast_stopped on stop. See Webhooks — event catalogue.
GET /streams/:id/viewers/stream
curl -N -H "Authorization: Bearer pk_YOUR_KEY_HERE" \
"https://api.dcast.pro/api/v1/streams/{streamId}/viewers/stream"
event: ready
data: {"streamId":"…"}
event: viewers
data: {"viewers":12}
event: viewers
data: {"viewers":142}Sends the current count on connect, then an event whenever it changes (checked every 5s). Use for live audience dashboards.
Playback URLs for your own video — GET /videos/:id/play
Returns the playback addresses of a video owned by the API key's account: the HLS master address and the embed page. The HLS address carries no token; it is the same playback.hls that GET /videos/:id returns.
curl -s -H "Authorization: Bearer pk_YOUR_KEY_HERE" \
https://api.dcast.pro/api/v1/videos/{videoId}/play
# 200
{
"success": true,
"data": {
"id": "cmpm…",
"hls": "https://api.dcast.pro/api/videos/cmpm…/master",
"embed": "https://dcast.tv/embed/cmpm…",
"duration": 312.4,
"visibility": "PUBLIC"
}
}
# 404 NOT_FOUND - no such video on this account
# 409 NOT_READY - still processing (error.status carries processingStatus)A free public video plays from hls as-is. For anything that needs a pass (paid, tier-gated, REGISTERED, passphrase), the viewer's player first calls GET https://api.dcast.pro/api/videos/:id/access-token and plays the returned masterUrl(master pass 300 s, refresh on a ≤ 4-minute cadence) — see Paid catalog → Option B, which also describes the PRO/VIP requirement for a browser player on your own domain. The embed iframe needs neither.
Live Chat
Read, post, delete, and tail chat for any stream you own. Messages are persisted into the same store as the watch-page chat and broadcast to connected viewers in real time. The source field tags origin so aggregated chat from external platforms (Twitch, YouTube, Facebook) is distinguishable from partner API posts and internal dashboard writes.
Cross-creator access returns 404 NOT_FOUND (not 403): we never reveal whether a stream id exists when it is not yours.
Message shape
{
"id": "cmpi…",
"streamId": "stream_id",
"authorName": "Display name shown next to the message",
"text": "Message body",
"userId": "user_id_or_null",
"source": "partner" | "twitch" | "youtube" | "facebook" | "internal",
"sentAt": "2026-05-25T18:42:11.000Z"
}List messages
GET https://api.dcast.pro/api/v1/streams/{streamId}/chat?limit=50
GET https://api.dcast.pro/api/v1/streams/{streamId}/chat?limit=50&since=2026-05-25T18:42:00.000Z
Authorization: Bearer YOUR_API_KEYDefaults to the newest 50 messages in descending order. Provide since (ISO timestamp) to get messages strictly newer than the cursor in ascending order — convenient for polling consumers. limit is clamped to 1–200. The response includes a pagination.nextCursor you can pass back as since on the next call.
Post a message
POST https://api.dcast.pro/api/v1/streams/{streamId}/chat
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{ "text": "Welcome to the show!", "authorName": "Support Bot" }Body accepts text, content, or message. authorName is optional; it falls back to the API key owner's username (or email local-part). Hard-capped at 10000 characters; longer text is truncated.
Rate limits: 60 messages / minute per (API key, stream) and a softer 1000 messages / day per stream. 429 RATE_LIMITED when either window is exceeded.
Delete a message
DELETE https://api.dcast.pro/api/v1/streams/{streamId}/chat/{messageId}
Authorization: Bearer YOUR_API_KEYHard-delete (not soft-delete). Cabinet viewers receive a chat:message_deleted event and prune the row immediately. 404 NOT_FOUND if the message does not exist on this stream.
Live tail (SSE)
curl -N -H "Authorization: Bearer pk_YOUR_KEY_HERE" \
"https://api.dcast.pro/api/v1/streams/{streamId}/chat/stream"Server-Sent Events stream with event: ready on connect, event: message per new or deleted message, and a heartbeat every 30 seconds. The connection is closed on client disconnect; reconnect with backoff. Each event's data is the JSON message shape above; deleted messages arrive as { id, streamId, deleted: true }.
Browser example (EventSource through your server)
// EventSource cannot send headers and the API never reads a key from the
// query string, so the browser connects to YOUR server, which opens the tail
// with the key (see the Node example below) and relays the events.
const STREAM_ID = 'your-stream-id';
const es = new EventSource('/your-chat-relay?stream=' + encodeURIComponent(STREAM_ID));
es.addEventListener('ready', () => {
console.log('[chat] connected');
});
es.addEventListener('message', (evt) => {
const data = JSON.parse(evt.data);
if (data.deleted) {
console.log('[chat] deleted', data.id);
} else {
console.log('[chat]', data.authorName, data.text);
}
});
es.onerror = () => {
// Reconnect with backoff; browser auto-retries but you may want to gate it.
console.warn('[chat] disconnected; will retry');
};Node example (eventsource package)
import { EventSource } from 'eventsource';
const es = new EventSource(
'https://api.dcast.pro/api/v1/streams/' + STREAM_ID + '/chat/stream',
{ fetch: (url, init) => fetch(url, {
...init,
headers: { ...init.headers, Authorization: 'Bearer pk_YOUR_KEY_HERE' }
})
}
);
es.addEventListener('message', (evt) => {
console.log(JSON.parse(evt.data));
});Security & scope
Stream endpoints are creator-scoped. Owner-only verbs (PATCH, DELETE, record/start, record/stop, chat POST/DELETE, viewers list) return 404 NOT_FOUND when called on a stream you do not own. The SSE chat tail is bound to the connected key's userId — one key cannot subscribe to another creator's private stream chat. pk_* keys cannot manage ingest infrastructure, cannot reroute streams between regions, and cannot read other creators' viewer telemetry.
