Skip to content

HTTP API

Request and response bodies are JSON. Endpoints that take a body expect Content-Type: application/json. The base URL throughout this page is http://headend.local:8080; substitute the address and the port you configured with --port.

Channel identifiers in paths are the numeric id from the configuration file. They are reused after a delete, so a script that caches an identifier across a reconfiguration can act on the wrong channel.

MethodPathPurpose
GET/loginThe login page itself. Of no use to a script; listed so that an unexpected HTML response to an API call is recognisable as a redirect to it.
POST/api/loginExchange username and password for a session cookie. Credentials are the values of CASTMUX_WEB_USERNAME and CASTMUX_WEB_PASSWORD.
POST/api/logoutInvalidate the current session.

With curl, store the cookie in a jar on login and present the jar afterwards:

Terminal window
# log in, keeping the session cookie
curl -s -c cookies.txt \
-H 'Content-Type: application/json' \
-d '{"username":"admin","password":"correct-horse-battery"}' \
http://headend.local:8080/api/login
# now call an authenticated endpoint with the same jar
curl -s -b cookies.txt \
http://headend.local:8080/api/viewers
MethodPathPurpose
GET/api/versionVersion, commit and build time of the running binary. Deliberately unauthenticated, so a monitoring system can confirm what is deployed on every appliance in an estate without being given credentials to change anything on them.
Terminal window
curl -s http://headend.local:8080/api/version
# {"version":"3.1.0","commit":"9f2c41a","built":"2026-04-08T09:12:44Z"}
MethodPathPurpose
POST/api/internal/streams/{id}/restartRestart one channel, for a supervisor running on the appliance itself. The optional ?reason= is written to the log, so a restart there says why it happened. It returns the channel's name and whether a running engine was restarted. The supplied watchdog uses it to recover one stuck channel without restarting the whole container and taking every healthy channel off air with it.

This endpoint needs no login. Instead, it refuses any connection that does not come from a loopback address (127.0.0.1 or ::1), with 403 and a log line naming the caller. That keeps the admin password out of a file on disk for the watchdog to read. Reaching the endpoint already requires a shell on the appliance.

MethodPathPurpose
Lifecycle
POST/api/channelsCreate a channel. The body is the same shape as one entry under streams in the configuration file. The assigned identifier is returned.
PUT/api/channels/{id}Replace the settings of an existing channel. A running channel is restarted if the change affects how it runs.
DELETE/api/channels/{id}Remove a channel. Stops it first. The identifier becomes available for reuse.
POST/api/channels/{id}/restartRestart one channel without touching the others. The first thing to try when a single channel is misbehaving and the rest are healthy.
State
POST/api/streams/{id}/activeEnable or disable a channel. A disabled channel keeps its configuration but is never started.
POST/api/streams/{id}/force-always-onToggle the always-on override. On, the channel runs continuously; off, it runs when viewer detection sees somebody watching.
Inspection
GET/api/channelsA short list of every channel: id, name, destination address and port, and epg_id. Enough to discover which channels exist. It does not return a channel's full settings — source, engine, profile, supervision — so a script that needs those reads the configuration file.
GET/api/channels/{id}/ts-qualityTransport-stream quality for one channel: the latest sample, running totals of continuity, transport and PCR errors, and the last 30 minutes of history at 5-second intervals. Also says whether monitoring is running for the channel and how its PCR timing is measured. See Transport-stream quality.
GET/api/channels/{id}/logsRecent engine output for one channel. This is where a source error appears in the words the origin actually used, rather than as a generic failure.
GET/api/channels/resolutionsThe resolution currently selected for each channel, and whether it is an exact match for the requested profile or the closest rung the origin offered. Served from cache; it does not block on origin fetches.
Preview
POST/api/channels/{id}/previewStart a browser-viewable preview of a channel.
POST/api/channels/{id}/preview/stopStop it. Previews consume real CPU, so stop them explicitly rather than relying on closing the page.
Terminal window
# create a channel
curl -s -b cookies.txt -X POST \
-H 'Content-Type: application/json' \
-d '{
"name": "Example One",
"description": "Main lounge line-up",
"active": true,
"force_always_on": false,
"engine": "tsduck",
"resolution_profile": "hd",
"source": {"type": "hls", "url": "https://origin.example.net/one/master.m3u8"},
"destination": {"address": "239.10.0.1", "port": 5000}
}' \
http://headend.local:8080/api/channels
MethodPathPurpose
Sources
POST/api/epg/sourcesAdd an XMLTV source. Body: {"name": "...", "url": "..."}. Returns the new source's id. The source's channel list is fetched in the background.
PUT/api/epg/sources/{id}Rename a source or point it at a new URL, with the same body. Stored data is kept and replaced on the next sync.
DELETE/api/epg/sources/{id}Remove a source and its stored guide data. Channels mapped to it keep their epg_id.
POST/api/epg/sources/{id}/syncFetch the source now and store programmes for mapped channels. Synchronous: the response reports the real result, and on a large guide it can take minutes.
Mapping and data
GET/api/epg/channelsEvery source, keyed by its id, with its name, URL, last fetch and status, and the guide identifiers it offers.
POST/api/epg/mapMap a channel to a guide identifier. Body: {"stream_id": 3, "tvg_id": "rtl.de"}. An empty tvg_id clears the mapping. Saved to the channel's epg_id.
GET/api/epg/nowWhat is on now and what is next, for every mapped channel in one call. A mapped channel with nothing airing is present with now empty, which is different from an unmapped channel, which is absent.
MethodPathPurpose
GET/api/licenceThe appliance's licence state: state with a readable message and detail, can_stream, the device_code support will ask for, and a support contact. If the last sync refused channels the licence server sent, the reasons are included too. The licence token itself is never returned.
POST/api/licence/retryCheck in with the licence server now instead of waiting for the next scheduled attempt. Use it after fixing whatever blocked the licence, such as a lapsed subscription or a firewall rule. It returns at once; poll /api/licence for the outcome.
MethodPathPurpose
GET/api/viewersViewer state for every channel in one call. Use this for polling rather than looping over channels, which multiplies the request count by the size of the line-up.
GET/api/streams/{id}/viewersDetail for one channel: the list of client addresses currently joined, the count, a flag for whether an IGMP querier is present on the segment, and a flag for whether detection is degraded.
MethodPathPurpose
Measurement
GET/api/metricsSystem metrics and per-channel metrics in one document: CPU, memory, uptime, per channel state and throughput. Cached for about two seconds server-side, so polling faster than that returns the same numbers and only costs you request handling.
GET/api/network-statsInterface-level counters and reachability results for the hosts in ping_hosts.
GET/api/udp-bitrateMeasured bitrate actually leaving the network port, per channel. This is the engine-agnostic liveness signal: it reflects what is on the wire rather than what a decoder claims it is producing.
Administration
PUT/api/settingsChange global settings. Send only the keys to change; anything omitted keeps its current value. PUT only — there is no GET variant, so read current settings from the configuration file.
GET/api/network-interfacesEvery IPv4 address on the appliance, for choosing udp_device: address, interface, whether the interface is up, its subnet, and whether the address was assigned by DHCP. Loopback and link-local addresses are left out, because neither is ever the right choice. Down interfaces are listed, not hidden, because a down interface is exactly what you need to see.
POST/api/restart-allRestart every channel. Starts are staggered by startup_stagger_seconds, so the whole line-up does not come back at once and fail together.
POST/api/restart-appRestart the Castmux application. It replies first, then shuts down cleanly, and the container's restart policy starts it again within a few seconds. Every channel is briefly off air. Unlike /api/reboot, it needs nothing on the host, so it works in any container deployment.
POST/api/rebootReboot the appliance. Every channel is off air for the duration; this is not a remedy for a channel fault.
MethodPathPurpose
Multiplexes
GET/api/dvb/muxesList the configured multiplexes with their RF parameters and current state. Unlike channels, multiplexes do have a read endpoint.
POST/api/dvb/muxesCreate a multiplex.
PUT/api/dvb/muxes/{id}Update RF parameters or signalling identifiers. Changing frequency, modulation or symbol rate interrupts the carrier.
DELETE/api/dvb/muxes/{id}Remove a multiplex. The channels it carried are not deleted.
POST/api/dvb/muxes/{id}/activeStart or stop transmission of the carrier.
Contents and operations
POST/api/dvb/muxes/{id}/channelsAssign the channels carried, and their order. The order determines service identifiers, so reordering re-identifies services and can require televisions to re-tune. Append rather than insert where you have the choice.
GET/api/dvb/muxes/{id}/logsMultiplexer and modulator output for this carrier.
POST/api/dvb/muxes/{id}/hot-swapExchange the contents of a running multiplex without dropping the carrier, so sets stay tuned rather than reporting no signal during the change.
POST/api/dvb/muxes/{id}/suspendSuspend the multiplex temporarily, for maintenance on the modulator or the cable network, without deleting the configuration.
MethodPathPurpose
POST/api/hdmi/activeEnable or disable the HDMI output.
POST/api/hdmi/configSet which channel is presented on the HDMI port and how it is rendered.
GET/api/hdmi/outputsWhat this appliance can drive: vos (video drivers), drm_devices (display devices) and audio_devices (each a value and label, as the player itself lists them), plus the current selection. The values are exactly what /api/hdmi/config accepts.
GET/api/hdmi/metricsState and throughput of the HDMI output.
GET/api/hdmi/logsOutput from the HDMI rendering process.

A worked example: log in, create three channels, create a multiplex, and assign the channels to it in the order they should appear. This is the whole commissioning sequence for a small site, and it is worth scripting because typing three channels into a form is where transposed multicast addresses come from.

#!/usr/bin/env bash
set -euo pipefail
BASE="http://headend.local:8080"
JAR="$(mktemp)"
# 1. log in ------------------------------------------------------------
curl -sf -c "$JAR" -H 'Content-Type: application/json' \
-d "{\"username\":\"$CASTMUX_USER\",\"password\":\"$CASTMUX_PASS\"}" \
"$BASE/api/login" > /dev/null
# 2. create the channels ------------------------------------------------
# name | multicast group | source playlist
CHANNELS=(
"Example One|239.10.0.1|https://origin.example.net/one/master.m3u8"
"Example Two|239.10.0.2|https://origin.example.net/two/master.m3u8"
"Example News|239.10.0.3|https://origin.example.net/news/master.m3u8"
)
IDS=()
for row in "${CHANNELS[@]}"; do
IFS='|' read -r NAME GROUP URL <<< "$row"
ID=$(curl -sf -b "$JAR" -X POST -H 'Content-Type: application/json' \
-d "{
\"name\": \"$NAME\",
\"active\": true,
\"force_always_on\": true,
\"engine\": \"tsduck\",
\"resolution_profile\": \"hd\",
\"source\": {\"type\": \"hls\", \"url\": \"$URL\"},
\"destination\": {\"address\": \"$GROUP\", \"port\": 5000}
}" \
"$BASE/api/channels" | sed -n 's/.*"id":\([0-9]*\).*/\1/p')
echo "created $NAME as id $ID"
IDS+=("$ID")
# respect the stagger: three HLS origins opened at once is the
# failure this setting exists to prevent
sleep 3
done
# 3. create the multiplex ------------------------------------------------
MUX=$(curl -sf -b "$JAR" -X POST -H 'Content-Type: application/json' \
-d '{
"name": "Mux A",
"device": 0,
"frequency": 474000000,
"qam": 256,
"symbol_rate": 6900,
"network_id": 1,
"ts_id": 1,
"network_name": "Seaview Court",
"active": true
}' \
"$BASE/api/dvb/muxes" | sed -n 's/.*"id":\([0-9]*\).*/\1/p')
# 4. assign the channels, in line-up order -------------------------------
# this order fixes the service IDs; changing it later means a re-tune
LIST=$(IFS=,; echo "${IDS[*]}")
curl -sf -b "$JAR" -X POST -H 'Content-Type: application/json' \
-d "{\"channel_ids\": [$LIST]}" \
"$BASE/api/dvb/muxes/$MUX/channels"
rm -f "$JAR"

Two details in that script are not incidental. The sleep 3 between creations matches startup_stagger_seconds: three HLS origins opened simultaneously is the exact condition the stagger exists to avoid, and a build script is the easiest way to recreate it. And the channels are assigned in one call rather than three, because assignment order sets the service identifiers — building the list first and posting it once means the line-up is numbered as intended on the first attempt.

  • Errors are JSON. A failed request returns an appropriate status code and a JSON body describing the problem. An HTML response means the request was not authenticated and was redirected to the login page — check the cookie jar before debugging the body.
  • PUT /api/settings merges. Send only the settings you want to change; anything omitted keeps its current value. A change to the engine parameters, udp_device, default_engine, enable_dummy_listeners, overwrite_srt_latency or overwrite_pkt_size restarts every active channel, because those settings shape how each engine runs. Other settings apply without a restart.
  • Rate limiting is not a substitute for cached data. /api/metrics is cached for about two seconds; a dashboard polling it every 250 ms produces eight identical answers and no extra information.

A full read API, whole-configuration export and import, and API tokens in place of session cookies are all roadmap. They are listed with the rest on the roadmap page.