Try it free

REST API quick start

Everything the app does, a program can do too. This page walks one live from start to finish over the API. Every request and response below was made against a running creekX server; only the keys are replaced.

Replace live.example.com with your server's address and YOUR-API-KEY with a key from Settings → Keys for automation (Keys and webhooks). The whole API is described at /openapi.json on your server.

1. Authenticate

Send the key with every request as X-CreekX-Key. Requests are JSON.

export CREEKX=https://live.example.com/v1
export KEY=YOUR-API-KEY
curl -H "X-CreekX-Key: $KEY" $CREEKX/streams

A mistake answers with a problem description that says what to fix. For example, an unknown webhook event:

{
  "title": "Bad request",
  "status": 400,
  "detail": "The request body is invalid.",
  "remedy": "Fix every listed issue. The request shapes are in the OpenAPI document at /openapi.json.",
  "issues": [{
    "path": "events[0]",
    "message": "Invalid enum value. Expected 'stream.live' | 'stream.ended' | 'recording.saved' | ..."
  }]
}

2. Make a stream

curl -X POST $CREEKX/streams \
  -H "X-CreekX-Key: $KEY" -H "content-type: application/json" \
  -d '{"name":"Main stage"}'

The answer (shortened). The key is never returned here, only its last four characters:

{
  "id": "eebc8cd3-14d5-4ea1-b9b0-aa8c483d67a7",
  "name": "Main stage",
  "obs": {
    "server": "rtmp://live.example.com:1935/live",
    "streamKeyMasked": "xxxx-xxxx-xxxx-cOhk"
  },
  "srt": true,
  "rtsp": true,
  "browser": { "whipUrl": "https://live.example.com/live/eebc8cd3-.../browser/whip" },
  "record": true,
  "destinationIds": [],
  "qualities": false,
  "chat": false
}

3. Get its addresses

Revealing the key is a separate call, and it is written to the audit log. The answer gives the stream in the form every tool takes:

curl -X POST $CREEKX/streams/$STREAM_ID/key/reveal -H "X-CreekX-Key: $KEY"
{
  "key": "YOUR-STREAM-KEY",
  "obs":  { "server": "rtmp://live.example.com:1935/live",
            "streamKey": "eebc8cd3-...?user=perpetua&pass=YOUR-STREAM-KEY" },
  "srt":  { "url": "srt://live.example.com:8890?streamid=publish:live/eebc8cd3-...:perpetua:YOUR-STREAM-KEY",
            "passphrase": null },
  "rtsp": { "url": "rtsp://perpetua:[email protected]:8554/live/eebc8cd3-..." },
  "browser": { "whipUrl": "https://live.example.com/live/eebc8cd3-.../browser/whip",
               "bearerToken": "YOUR-STREAM-KEY" }
}

Paste obs.server and obs.streamKey into OBS, or use any of the others from an encoder. Lost it? POST /streams/{id}/key/rotate makes a new one and the old one stops at once.

4. Go live from FFmpeg

The same three ways in we used to test this page. Keyframes every second and no B-frames give viewers the lowest delay.

# RTMP (as OBS sends)
ffmpeg -re -i show.mp4 -c:v libx264 -preset veryfast -tune zerolatency -g 30 -b:v 2500k \
  -c:a aac -b:a 128k -ar 48000 -f flv "rtmp://live.example.com:1935/live/STREAM_ID?user=perpetua&pass=YOUR-STREAM-KEY"

# SRT (better over the internet and mobile networks)
ffmpeg -re -i show.mp4 -c:v libx264 -preset veryfast -tune zerolatency -g 30 -b:v 2500k \
  -c:a aac -b:a 128k -f mpegts "srt://live.example.com:8890?streamid=publish:live/STREAM_ID:perpetua:YOUR-STREAM-KEY"

# RTSP (as an encoder or camera pushes)
ffmpeg -re -i show.mp4 -c:v libx264 -tune zerolatency -g 30 -c:a aac \
  -rtsp_transport tcp -f rtsp "rtsp://perpetua:[email protected]:8554/live/STREAM_ID"

Use the exact addresses from step 3: they already hold your stream's id and key.

5. Check it is live

curl -H "X-CreekX-Key: $KEY" $CREEKX/streams/$STREAM_ID/status
{
  "state": "live",
  "since": "2026-10-15T17:14:21.539Z",
  "tracks": ["H264", "MPEG-4 Audio"],
  "bytesReceived": 94579978,
  "viewers": 4,
  "destinations": [
    { "label": "YouTube",  "health": "sending", "problem": null },
    { "label": "Facebook", "health": "sending", "problem": null },
    { "label": "Twitch",   "health": "sending", "problem": null }
  ],
  "backup": null
}

state is live while video arrives. Each platform reports its own health, with the reason when it is not sending.

6. Restream to platforms

Add each platform once. The stream key is encrypted when it arrives and never returned by any read:

curl -X POST $CREEKX/destinations \
  -H "X-CreekX-Key: $KEY" -H "content-type: application/json" \
  -d '{"kind":"youtube","label":"YouTube","ingestUrl":"rtmp://a.rtmp.youtube.com/live2","streamKey":"YOUR-YOUTUBE-KEY"}'

kind is one of youtube, facebook, twitch, kick, rtmp, srt or rtsp. Then choose where a stream goes (up to 10), and turn on its watch link and chat in the same call:

curl -X PATCH $CREEKX/streams/$STREAM_ID \
  -H "X-CreekX-Key: $KEY" -H "content-type: application/json" \
  -d '{"destinationIds":["DESTINATION_ID"],"watch":true,"chat":true}'

The same call takes record, qualities (several sizes for HLS viewers), backupId (another stream to switch to if this one stops), publishFrom and watchFrom (address lists) and pull (an rtsp://, rtmp://, srt:// or .m3u8 address to fetch instead).

7. A private, expiring watch link

curl -X POST $CREEKX/streams/$STREAM_ID/tokens \
  -H "X-CreekX-Key: $KEY" -H "content-type: application/json" \
  -d '{"use":"play","expiresInSeconds":7200,"oneTime":true}'
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOi...",
  "use": "play",
  "oneTime": true,
  "expiresAt": "2026-10-15T19:19:05.187Z",
  "watch": { "watchPath": "/watch/eebc8cd3-.../eyJhbGciOi..." }
}

The token is a signed JWT (HS256) naming the stream, its use, when it expires and whether it is for one viewer. "use":"publish" makes a guest key for someone else's encoder. POST /streams/{id}/tokens/revoke stops every token made so far.

8. Hear about events: webhooks

curl -X POST $CREEKX/webhooks \
  -H "X-CreekX-Key: $KEY" -H "content-type: application/json" \
  -d '{"url":"https://example.com/hooks/creekx","events":["stream.live","stream.ended","recording.saved"]}'
{
  "webhook": {
    "id": "74855578-dee4-4f09-aadb-375800e05ab4",
    "url": "https://example.com/hooks/creekx",
    "events": ["stream.live", "stream.ended", "recording.saved"],
    "lastDelivery": null
  },
  "secret": "whsec_..."
}

Events: stream.live, stream.ended, recording.saved, video.ready, channel.on_air, channel.off_air, viewer.started, viewer.ended. The secret is shown once. Each delivery carries CreekX-Event, CreekX-Delivery and CreekX-Signature: t=…,v1=…, where v1 is the hex HMAC-SHA256 of <t>.<body> keyed with the whole secret. Check it like this (Node.js):

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: the request body exactly as received, before any JSON parsing.
export function fromCreekX(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!(age < 300)) return false; // older than five minutes: refuse replays
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const given = Buffer.from(parts.v1 ?? '', 'hex');
  return given.length === 32 && timingSafeEqual(given, Buffer.from(expected, 'hex'));
}

POST /webhooks/{id}/test sends a test event, and GET /webhooks/{id}/deliveries lists recent deliveries and their answers.

What else the API covers

For Endpoints
Videos POST /uploads (resumable), GET /assets, /assets/{id}/captions, /captions/search
24/7 channels GET /channels, POST /channels/{id}/live (on air), /channels/{id}/schedule, GET /epg
IP cameras POST /cameras/find, PUT /streams/{id}/camera, /camera/move, /camera/presets, /camera/timeline
Viewers and safety GET /viewers, /streams/{id}/sessions, /viewer-blocks, /streams/{id}/srt-passphrase
The server GET /healthz, /admin/system, /support/check