Partner API base path is /api/v1 on https://api.dcast.pro. This site documents the live surface; use the Quickstart QA curls to validate keys and routes.

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

ProtocolDescriptionRequirementsIngest URL Type
RTMPReal-Time Messaging Protocol - industry standard for live streamingNone (default)RTMP URL + Stream Key
SRTSecure Reliable Transport - ultra-low latency, tolerant of packet lossOptional: srtConfig (mode, latency, passphrase)SRT URL + Stream ID
WebRTCWeb Real-Time Communication - real-time browser streamingOptional: isCamera (true for browser camera, false for external source)WHIP/WHEP URLs
HTTPHTTP Pull - pull from external HTTP/RTSP sourceRequired: httpSourceUrlHTTP source URL
FILEFILE - stream from uploaded video fileRequired: fileSourceUrl — a public http(s) URL, checked when the stream startsRTMP 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.

StoredAlso acceptedMeaning
PUBLICpublicAnyone can watch. Default on create.
LINK_ONLYunlistedAnyone with the link can watch.
ACCESS_KEYbykeyThe viewer must enter the access key. Requires accessKey in the same request (400 INVALID_ACCESS_KEY otherwise).
REGISTEREDregisteredThe viewer fills in a registration form first.
PRIVATEprivateOnly 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.rtmp as 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 identical ingest.srtDisplay) verbatim. The literal streamid (#!::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 — POST SDP offers to ingest.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%3Dpublish

Phone: 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

MethodEndpointDescription
GET/streamsList 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/streamsCreate stream
GET/streams/:idDetails and ingest URLs. status is canonical LIVE | OFFLINE (sourced from isLive; matches the value returned by /streams/:id/status).
GET/streams/:id/statusLightweight 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/viewersCurrent viewer count + last-N session snapshot (country / deviceType / watchDuration)
GET/streams/:id/preview-urlLive HTTP-FLV preview URL for flv.js playback (the same URL the cabinet renders). See Live preview player below.
GET/streams/:id/thumbnailLive preview thumbnail (Content-Type: image/jpeg). See Thumbnail below.
PATCH/streams/:idUpdate 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/startGo 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/endStop the broadcast (flips isLive=false). Same handler as POST /streams/:id/stop; both check the streams:operate permission.
DELETE/streams/:idPermanently delete the stream and all recordings (see Delete below)
POST/streams/:id/record/startStart recording
POST/streams/:id/record/stopStop recording
GET/streams/:id/record/statusRecording status
GET/streams/:id/chatList recent chat messages (paginated read)
POST/streams/:id/chatPost a chat message as the API key owner
DELETE/streams/:id/chat/:messageIdHard-delete a single chat message (author or stream owner)
GET/streams/:id/chat/streamServer-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.

HTTPdata.reasonWhen you see it
200—SRS detects an active publish (hasSignal=true) AND the preview transcoder mount is up. httpFlvUrl is playable.
202stream_not_publishingNo ingest yet (encoder hasn't started, or stopped pushing >~90s ago).
202preview_warming_upIngest 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), hasSignal flips true and /preview-url serves. No go-live call is needed for preview.
  • Going live (viewer HLS) is keyed on isLive — a user-driven flag set only by POST /streams/:id/start. This starts the multibitrate-HLS transcoder so the public master.m3u8 goes 404→200 and 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 / isLiveButton stateMeaning
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 no streamKey (nothing to transmux/transcode).
  • 404 NOT_FOUND — foreign or unknown stream id (owner-scoped; we never reveal whether an id exists outside your account).
  • 502 with code: 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_KEY

Defaults 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_KEY

Hard-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.

Streams API — dcast.pro API docs