Base URL: http://localhost:3030
All request/response bodies are JSON. All timestamps are ISO 8601 UTC strings.
Returns all stream keys ordered by creation date descending.
ingestUrls ports are derived from MediaMTX global runtime config (GET /v3/config/global/get). If a protocol port cannot be resolved, that protocol URL is returned as null.
Response 200:
[
{
"key": "c1518f5ef0d917ef1b6547d7",
"label": "English Feed",
"createdAt": "2026-04-10T10:59:00.000Z",
"ingestUrls": {
"rtmp": "rtmp://stream.example.com:1935/live/c1518f5ef0d917ef1b6547d7",
"srt": "srt://stream.example.com:8890?streamid=publish:live/c1518f5ef0d917ef1b6547d7"
}
}
]Creates a stream key and registers the corresponding path in MediaMTX.
The MediaMTX path is always provisioned as live/<streamKey>.
When provided, streamKey must match these rules:
- non-empty string (after trimming)
- up to 128 characters
- allowed characters: alphanumeric, underscore (
_), dot (.), hyphen (-) - disallowed values:
.and..
Note: If MediaMTX path registration succeeds but the SQLite insert fails, the API now attempts a compensating MediaMTX path delete before returning
500.
Request body:
{
"streamKey": "mystream", // optional — omit for auto-generated 24-char hex
"label": "English Feed" // optional
}Response 201:
{
"message": "Stream key created",
"streamKey": {
"key": "mystream",
"label": "English Feed",
"createdAt": "2026-04-10T10:59:00.000Z",
"ingestUrls": {
"rtmp": "rtmp://stream.example.com:1935/live/mystream",
"srt": "srt://stream.example.com:8890?streamid=publish:live/mystream"
}
}
}Errors: 409 key already exists; 500 MediaMTX registration failed, SQLite insert failed, or rollback failed.
Updates the label of an existing stream key.
Request body:
{ "label": "Spanish Feed" }Response 200:
{ "message": "Stream key updated", "streamKey": { ... } }Errors: 404 key not found.
Deletes a stream key and removes its path from MediaMTX.
Note: If MediaMTX path deletion fails the request returns
500. The DB row is not deleted until MediaMTX confirms success.
If MediaMTX deletion succeeds but the SQLite delete fails, the API attempts a compensating path re-add before returning
500.
Response 200:
{ "message": "Stream key deleted" }Errors: 404 key not found; 500 MediaMTX deletion failed, SQLite delete failed, or rollback failed.
Returns all pipelines.
Response 200:
[
{
"id": "a1b2c3d4e5f6a7b8",
"name": "Pipeline 1",
"streamKey": "c1518f5ef0d917ef1b6547d7",
"encoding": null,
"inputEverSeenLive": 1,
"createdAt": "2026-04-10T11:00:00.000Z",
"updatedAt": null
}
]Creates a pipeline.
If streamKey is provided, it follows the same validation rules as POST /stream-keys.
If inputSource is omitted or empty, the MediaMTX path uses the default publisher source and
waits for RTMP/SRT ingest. If inputSource is a supported MediaMTX source URL, Restream patches
that path's source so MediaMTX actively pulls the input.
Request body:
{
"name": "Pipeline 1",
"streamKey": "c1518f5ef0d917ef1b6547d7", // optional
"inputSource": "rtsp://camera.example/live", // optional; empty means publisher ingest
"encoding": null // reserved, not used at runtime
}Response 201:
{ "message": "Pipeline created", "pipeline": { ... } }Errors: 400 missing name.
Updates pipeline fields.
If streamKey is provided in the request body, it follows the same validation rules as POST /stream-keys.
If inputSource is provided, it must be empty or a supported MediaMTX source URL.
Request body: same shape as create (all fields optional).
Response 200:
{ "message": "Pipeline updated", "pipeline": { ... } }Errors: 404 pipeline not found; 409 stream key change blocked while outputs are running.
Returns append-only pipeline history events for UI consumption. This stream includes:
- pipeline config mutations (
[config] ...) - input state transitions (
[input_state] off -> on,on -> warning,warning -> error, etc.)
Pipeline-level history entries are stored in job_logs with:
pipeline_id = :pipelineIdoutput_id IS NULL- typed
eventTypevalues such aspipeline.config.stream_key_changedandpipeline.input_state.transitioned
Response 200:
{
"pipelineId": "a1b2c3d4e5f6a7b8",
"logs": [
{
"ts": "2026-04-16T05:30:00.000Z",
"eventType": "pipeline.input_state.transitioned",
"eventData": { "from": "off", "to": "on" },
"message": "[input_state] off -> on"
},
{
"ts": "2026-04-16T05:25:00.000Z",
"eventType": "pipeline.config.stream_key_changed",
"eventData": { "fromMasked": "ab...cd", "toMasked": "ef...12" },
"message": "[config] stream_key changed from ab...cd to ef...12"
}
]
}Errors: 404 pipeline not found.
Deletes a pipeline. All running output jobs are stopped and the API waits for those processes to exit before deletion. Outputs and jobs cascade-delete via SQLite FK only after teardown completes.
Response 200:
{ "message": "Pipeline <id> deleted" }Errors: 404 pipeline not found; 409 one or more running outputs failed to stop before delete.
Creates an output for a pipeline.
Request body:
{
"name": "YouTube",
"url": "rtmp://a.rtmp.youtube.com/live2/xxxx-xxxx-xxxx",
"encoding": "source" // system: source | vertical-crop | vertical-rotate | 720p | 1080p
// or any custom encoding key from GET /encodings
}url supports rtmp://, rtmps://, srt://, and http:// / https:// HLS playlist upload targets. For HLS uploads, use either a direct .m3u8 playlist URL or a query-based upload URL whose playlist parameter resolves to .m3u8.
Response 201:
{
"message": "Output created",
"output": {
"id": "f8e7d6c5b4a3f2e1",
"pipelineId": "a1b2c3d4e5f6a7b8",
"name": "YouTube",
"url": "rtmp://...",
"desiredState": "stopped",
"encoding": "source",
"createdAt": "2026-04-10T11:05:00.000Z"
}
}Errors: 400 missing/invalid name/url/encoding; 404 pipeline not found.
Updates an output.
Request body: same shape as create.
Response 200:
{ "message": "Output updated", "output": { ... } }Errors: 400 invalid encoding; 404 output or pipeline not found; 409 cannot change URL/encoding while running.
FFmpeg output settings vary by protocol: SRT outputs use -f mpegts; RTMP/RTMPS outputs use -f flv with RTMP flags; HLS outputs use -f hls -method PUT -http_persistent 1 with a rolling live playlist (-hls_time 2 -hls_list_size 5 -hls_flags delete_segments). Non-source HLS transcodes also use libx264 -preset veryfast -tune zerolatency with the configured bitrate profile.
Operational note: on FFmpeg 6.1.x, HLS uploads using -http_persistent 1 can hit an upstream hlsenc retry bug when the HTTP sink disappears mid-segment. In practice, HLS source copy outputs usually exit with a normal muxer failure, while transcoded HLS outputs can terminate with SIGSEGV before Restream's retry logic restarts them. FFmpeg 7.1+ contains the upstream fix and is recommended for HLS upload deployments.
Deletes an output. If a job is running, the API sends a stop signal and waits for the process to exit before removing the DB row.
Response 200:
{ "message": "Output <outputId> from pipeline <pipelineId> deleted" }Errors: 404 output or pipeline not found; 409 running process did not stop before delete.
Sets this output's desired state to running and reconciles runtime toward that state. The full call flow is:
- Persist
outputs.desiredState='running'. - Acquire an in-memory start lock for
(pipelineId, outputId)if a start is needed. - Validate pipeline + output exist.
- Check for an existing running job.
- Confirm the MediaMTX path for
pipeline.streamKeyis available via/v3/paths/list. - Build pull URL from the active ingest protocol: RTMP ingest uses
rtmp://localhost:1935/live/<streamKey>; SRT ingest usessrt://localhost:10080?streamid=read:live/<streamKey>. - Spawn FFmpeg with
-progress pipe:3for the selected output encoding:
source: codec copy (-c:v copy -c:a copy)vertical-crop:-vf scale=720:1280:force_original_aspect_ratio=increase,crop=720:1280+ H.264/AAC encodevertical-rotate:-vf transpose=1,scale=720:1280:force_original_aspect_ratio=increase,crop=720:1280+ H.264/AAC encode720p:-vf scale=-2:720+ H.264/AAC encode1080p:-vf scale=-2:1080+ H.264/AAC encode
- Persist job row in DB, return after 250 ms stability check.
Request body: none
Response 201:
{
"message": "Output started",
"desiredState": "running",
"job": {
"id": "3f2e1d0c9b8a7f6e",
"pipelineId": "a1b2c3d4e5f6a7b8",
"outputId": "f8e7d6c5b4a3f2e1",
"pid": 12345,
"status": "running",
"startedAt": "2026-04-10T11:10:00.000Z",
"endedAt": null,
"exitCode": null,
"exitSignal": null
}
}Response 409:
{
"error": "Pipeline input is not available yet",
"message": "Output desired state set to running; waiting for input",
"desiredState": "running",
"detail": "Publisher connected, stream not ready yet"
}Errors:
400no input URL available404pipeline or output not found409start already in progress for this output409pipeline input not available yet; desired state is still updated torunning500FFmpeg failed to start (includes last 100 log lines)
Sets this output's desired state to stopped, clears pending retry timers, and reconciles runtime toward that state. If a process is running, it is stopped via SIGTERM with a 5 s SIGKILL escalation.
Response 200:
{
"message": "Output desired state set to stopped",
"desiredState": "stopped",
"previousState": "running",
"jobId": "3f2e1d0c9b8a7f6e",
"result": {
"stopped": true,
"reason": "signal-sent" // "signal-sent" | "marked-stopped" | "signal-failed"
}
}If no process is running, the output intent is still updated and the response remains 200:
{
"message": "Output desired state set to stopped",
"desiredState": "stopped",
"previousState": "running",
"jobId": null,
"result": {
"stopped": false,
"reason": "already_stopped"
}
}Errors: 404 output or pipeline not found.
Returns recent job logs for a specific output. This endpoint is intended for diagnostics and output history UI.
Query params:
limitoptional; default200; min1; max1000.filteroptional; set tolifecycleto return only lifecycle events.sinceoptional; inclusive lower timestamp bound (ISO 8601).untiloptional; exclusive upper timestamp bound (ISO 8601).orderoptional;ascordesc.prefixoptional; comma-separated or repeated list of message families:lifecycle,stderr,exit,control,config,input_state.
Ordering behavior:
- Default path (
filteromitted): newest first. - Lifecycle path (
filter=lifecycle): oldest first, full lifecycle sequence for timeline rendering.
Filtering behavior:
sinceanduntilconstrain the returned time window.prefixfilters by message prefix whenfilteris omitted.filter=lifecycleis equivalent to a lifecycle-only prefix filter and ignoresprefix.
Guardrails:
- Any requested history window is capped server-side to 24 hours.
- Requests that include high-volume families (
stderr,exit,control) are capped to a 10 minute window when bothsinceanduntilare provided.
Response 200:
{
"pipelineId": "a1b2c3d4e5f6a7b8",
"outputId": "f8e7d6c5b4a3f2e1",
"logs": [
{
"ts": "2026-04-15T12:20:57.098Z",
"eventType": "lifecycle.exited",
"eventData": {
"status": "failed",
"requestedStop": false,
"exitCode": 255,
"exitSignal": null
},
"message": "[lifecycle] exited status=failed requestedStop=false exitCode=255 exitSignal=null"
},
{
"ts": "2026-04-15T12:20:57.098Z",
"eventType": "output.exit",
"eventData": { "code": 255, "signal": null },
"message": "[exit] code=255 signal=null"
},
{
"ts": "2026-04-15T12:20:56.971Z",
"eventType": "output.stderr",
"eventData": null,
"message": "[stderr] [flv @ ...] Non-monotonic DTS ..."
}
]
}Errors: 400 invalid limit, since, until, order, or prefix, or requested window too large; 404 output or pipeline not found.
History enrichment: timeline-relevant rows now include stable
eventTypecodes and structuredeventDatapayloads. The human-readablemessageis still stored and returned for raw log inspection, but clients should key timeline behavior off the typed fields instead of parsing the prose message.
Dashboard/API routes require a valid dashboard session cookie, except for /api/auth/*,
/login, /healthz, and /internal/mediamtx/auth. Unauthenticated API requests return 401.
Logs in with the dashboard password and sets an HttpOnly session cookie.
Request body:
{ "password": "admin" }Response 200:
{ "ok": true }Errors: 401 incorrect password.
Clears the dashboard session cookie.
Response 200:
{ "ok": true }Changes the dashboard password for the authenticated session.
Request body:
{
"currentPassword": "admin",
"newPassword": "new-password"
}Response 200:
{ "ok": true }Errors: 400 empty new password; 401 unauthenticated; 403 current password is incorrect.
Returns the full state snapshot used by the dashboard. Reads directly from SQLite on every request.
Response 200:
{
"serverName": "My Server",
"ingestSecurity": {
"failureLimit": 10,
"failureWindowMs": 60000,
"banMs": 600000,
"trackedIpLimit": 10000
},
"pipelines": [
{
"id": "a1b2c3d4e5f6a7b8",
"name": "Pipeline 1",
"streamKey": "c1518f5ef0d917ef1b6547d7",
"inputSource": null,
"ingestUrls": {
"rtmp": "rtmp://stream.example.com:1935/live/c1518f5ef0d917ef1b6547d7",
"srt": "srt://stream.example.com:8890?streamid=publish:live/c1518f5ef0d917ef1b6547d7"
}
}
],
"outputs": [ ... ],
"jobs": [ ... ]
}Each output includes desiredState, the persistent operator intent (running or stopped).
ingestUrls hostnames are set to localhost by the backend; the dashboard frontend rewrites them to the browser's current hostname before displaying them to users.
Updates server settings.
Request body:
{
"serverName": "My Server",
"ingestSecurity": {
"failureLimit": 10,
"failureWindowMs": 60000,
"banMs": 600000,
"trackedIpLimit": 10000
}
}Response 200:
{
"serverName": "My Server",
"ingestSecurity": {
"failureLimit": 10,
"failureWindowMs": 60000,
"banMs": 600000,
"trackedIpLimit": 10000
}
}All ingestSecurity fields are positive integer values stored in SQLite meta.
Errors: 400 serverName is empty or invalid; ingestSecurity is invalid.
Returns the latest server-side health snapshot. A periodic collector refreshes this snapshot in the background by calling MediaMTX endpoints in parallel: /v3/paths/list, /v3/rtmpconns/list, and /v3/srtconns/list, then merging that runtime state with DB job state, FFmpeg progress data, and input lifecycle bookkeeping. The collector interval defaults to 2000 ms and can be overridden with HEALTH_SNAPSHOT_INTERVAL_MS.
GET /health itself does not call MediaMTX. It returns the most recent cached snapshot immediately.
Response 200:
{
"generatedAt": "2026-04-10T11:31:36.879Z",
"ageMs": 412,
"status": "ready",
"mediamtx": {
"pathCount": 2,
"rtmpConnCount": 1,
"srtConnCount": 0,
"ready": true
},
"pipelines": {
"<pipelineId>": {
"input": {
"status": "on",
"publishStartedAt": "2026-04-10T09:00:00.000Z",
"streamKey": "c1518f5ef0d917ef1b6547d7",
"readers": 3,
"bytesReceived": 358000000,
"bytesSent": 320000000,
"video": {
"codec": "H264",
"width": 1920,
"height": 1080,
"profile": "High",
"level": "4",
"fps": 30,
"bw": null
},
"audio": {
"codec": "aac",
"channels": 2,
"sample_rate": 48000,
"profile": "LC",
"bw": null
}
},
"outputs": {
"<outputId>": {
"status": "on",
"jobId": "3f2e1d0c9b8a7f6e",
"totalSize": 120000000,
"bitrate": "1842.5kbits/s",
"bitrateKbps": 1842.5,
"progressFrame": 397,
"progressFps": 29.97,
"mediaSource": "ffmpeg",
"media": {
"video": {
"codec": "h264",
"width": 1280,
"height": 720,
"fps": 30,
"profile": null,
"level": null
},
"audio": {
"codec": "aac",
"sample_rate": 48000,
"channels": 2
}
}
}
}
}
}
}During startup, before MediaMTX is ready, /health returns an initialization snapshot:
{
"generatedAt": "2026-04-10T11:31:36.879Z",
"ageMs": 87,
"status": "initializing",
"mediamtx": {
"pathCount": 0,
"rtmpConnCount": 0,
"srtConnCount": 0,
"ready": false
},
"pipelines": {}
}If a collector cycle fails after startup, /health returns a degraded snapshot and may retain the last known pipeline data:
{
"generatedAt": "2026-04-10T11:31:36.879Z",
"ageMs": 1011,
"status": "degraded",
"mediamtx": {
"pathCount": 2,
"ready": true
},
"pipelines": {}
}Input status values:
| Status | Meaning |
|---|---|
on |
pathInfo.available === true (fallback: deprecated ready) |
warning |
pathInfo.online === true but path is not yet available |
error |
Stream key is configured, path is neither online nor available, and the pipeline has previously been seen live |
off |
No path info, or path is neither online nor available |
Output status values:
| Status | Meaning |
|---|---|
on |
Job running and FFmpeg has emitted progress data via fd3 |
warning |
Job running but no FFmpeg progress data received yet |
error |
Latest job status is failed |
off |
No running job |
For each output, ffmpeg runtime progress contributes:
totalSizefrom ffmpegtotal_size, normalized to a numeric byte count ornullbitratefrom ffmpegbitrate(raw ffmpeg rate string, for example1842.5kbits/s, kept for debugging, ornullwhen unavailable)bitrateKbpsserver-normalized numeric bitrate in Kbps for UI consumptionprogressFramefrom ffmpegframe, normalized to an integer ornullprogressFpsfrom ffmpegfps, normalized to a numeric FPS value ornull
When ffmpeg reports N/A for progress values, the backend normalizes them to null before
emitting /health. This is common for HLS uploads: bitrate and totalSize are often
unavailable, and HLS source copy outputs may also omit progressFrame and progressFps.
Output media metadata is server-resolved and included as:
mediaSource:ffmpeg|fallback-source|fallback-profile|unknownmedia.video/media.audio: codec/geometry/audio fields used by the dashboard
When FFmpeg has not emitted full Output #0 stream info yet, backend fallback rules apply:
fallback-source: copies media metadata from pipeline input forsourceoutputsfallback-profile: derives expected media from selected transcode profile
Readiness endpoint used by launch scripts and infra probes.
200with{ "status": "ok" }when MediaMTX is reachable.503with{ "status": "not_ready" }when MediaMTX is not ready.
Returns host system metrics from a fixed background sampler. Throughput and CPU values are computed against the previous timer sample, not against the previous HTTP request, so concurrent clients see the same rates for the same sample window. The sampler interval defaults to 5000 ms and can be overridden with SYSTEM_METRICS_SAMPLE_INTERVAL_MS.
Response 200:
{
"generatedAt": "2026-04-10T11:35:00.000Z",
"cpu": {
"usagePercent": 12.34,
"cores": 4,
"load1": 0.85
},
"memory": {
"totalBytes": 8589934592,
"usedBytes": 3000000000,
"freeBytes": 5589934592,
"usedPercent": 34.92
},
"disk": {
"totalBytes": 107374182400,
"usedBytes": 50000000000,
"freeBytes": 57374182400,
"usedPercent": 46.57
},
"network": {
"downloadBytesPerSec": 35000.00,
"uploadBytesPerSec": 90000.00,
"downloadKbps": 280.00,
"uploadKbps": 720.00
}
}Custom FFmpeg encoding presets. System encodings (source, vertical-crop, vertical-rotate, 720p, 1080p) are always available and cannot be modified or deleted.
Returns all encodings — system encodings first, then custom ones ordered by creation.
Response 200:
[
{ "id": null, "key": "source", "ffmpegArgs": null, "isSystem": true },
{ "id": null, "key": "vertical-crop", "ffmpegArgs": null, "isSystem": true },
{ "id": null, "key": "vertical-rotate", "ffmpegArgs": null, "isSystem": true },
{ "id": null, "key": "720p", "ffmpegArgs": null, "isSystem": true },
{ "id": null, "key": "1080p", "ffmpegArgs": null, "isSystem": true },
{
"id": "a1b2c3d4e5f6a7b8",
"key": "vertical-blur",
"ffmpegArgs": "-vf scale=720:1280,gblur=sigma=10 -c:v libx264 -preset veryfast -b:v 2500k -c:a aac -b:a 128k",
"isSystem": false
}
]Creates a custom encoding.
key must be lowercase alphanumeric with hyphens (e.g. vertical-blur), max 50 characters, and must not conflict with a system encoding key.
Request body:
{
"key": "vertical-blur",
"ffmpegArgs": "-vf scale=720:1280,gblur=sigma=10 -c:v libx264 -preset veryfast -b:v 2500k -c:a aac -b:a 128k"
}Response 201:
{
"id": "a1b2c3d4e5f6a7b8",
"key": "vertical-blur",
"ffmpegArgs": "-vf scale=720:1280,gblur=sigma=10 -c:v libx264 -preset veryfast -b:v 2500k -c:a aac -b:a 128k",
"isSystem": false
}Errors: 400 invalid key or missing ffmpegArgs; 409 key already exists.
Updates the ffmpegArgs of an existing custom encoding. The key is not editable after creation.
Request body:
{ "ffmpegArgs": "-vf scale=720:1280,gblur=sigma=5 -c:v libx264 -preset veryfast -b:v 3000k -c:a aac -b:a 128k" }Response 200: updated encoding object.
Errors: 400 missing ffmpegArgs; 404 encoding not found.
Deletes a custom encoding. Outputs currently using it fall back to source encoding at their next start.
Response 204: no body.
Errors: 404 encoding not found.
All errors return:
{ "error": "Human-readable description" }| Status | Meaning |
|---|---|
400 |
Validation or missing required field |
404 |
Resource not found |
409 |
Conflict — already running, duplicate key, input not ready |
500 |
Internal server error or MediaMTX communication failure |