Skip to content

API Reference

Complete REST API reference for bilbycast-edge. The API server binds the addresses configured in server.listen_addrs (dual-stack, precedence over the legacy single server.listen_addr) on port server.listen_port (default 8080). New installs bind loopback only127.0.0.1 and [::1] — so the API is not reachable off-box out of the box. LAN exposure requires launching with --bind-addrs 0.0.0.0,[::] (or setting server.listen_addrs accordingly) and enabling authentication.

Inputs, outputs, and flows are independent first-class entities. Inputs and outputs are top-level, individually CRUDable resources; a flow references them by ID via its input_ids and output_ids arrays rather than embedding their definitions. Internally the edge resolves those IDs into a ResolvedFlow (dereferencing every input/output) before the engine runs the flow.

All successful responses use a standard envelope:

{
"success": true,
"data": { ... }
}

All error responses use the same envelope without data:

{
"success": false,
"error": "Human-readable error message"
}


Lightweight health check suitable for load balancers, orchestrators, and monitoring probes. This endpoint is always public (no authentication required).

Response:

{
"status": "ok",
"version": "0.1.0",
"uptime_secs": 3661,
"active_flows": 2,
"total_flows": 3
}
Field Type Description
status string Always "ok" when the server is responsive
version string Application version from Cargo.toml
uptime_secs integer Seconds since the application started
active_flows integer Number of flows currently running
total_flows integer Total flows defined in configuration

curl example:

Terminal window
curl http://localhost:8080/health

Browser-based initial provisioning for edge nodes deployed on COTS hardware. All setup endpoints are public (no authentication required). Access is controlled by the setup_enabled config flag (default: true).

Serves the setup wizard HTML page. Returns the wizard form when setup_enabled is true, or a “Setup Disabled” page when false.

Returns the current setup-relevant configuration as JSON for pre-filling the form. The registration_token is always null in responses — secrets are never exposed via this endpoint.

Response:

{
"listen_addr": "0.0.0.0",
"listen_port": 8080,
"manager_url": null,
"accept_self_signed_cert": false,
"registration_token": null,
"device_name": null,
"setup_enabled": true
}

Validates and saves setup configuration. Returns 403 if setup_enabled is false.

Request body:

{
"listen_addr": "0.0.0.0",
"listen_port": 8080,
"manager_url": "wss://manager.example.com:8443/ws/node",
"accept_self_signed_cert": false,
"registration_token": "token-from-manager",
"device_name": "Studio-A Encoder"
}
Field Type Required Constraints
listen_addr string No API server bind address
listen_port u16 No 1-65535
manager_url string Yes Must start with wss://, max 2048 chars
accept_self_signed_cert bool No Default: false
registration_token string No Max 4096 chars
device_name string No Max 256 chars

Success response (200):

{
"success": true,
"message": "Configuration saved. Restart the bilbycast-edge service to apply the new settings."
}

Error response (400/403):

{
"success": false,
"error": "Manager URL must start with wss:// (TLS required)"
}

OAuth 2.0 Client Credentials token endpoint. Always public (no Bearer token required). Returns a signed JWT that must be included as a Bearer token in subsequent requests to protected endpoints.

Accepts both application/x-www-form-urlencoded and application/json request bodies.

Request body (JSON):

{
"grant_type": "client_credentials",
"client_id": "admin-client",
"client_secret": "super-secret-admin-password-here"
}

Request body (form-urlencoded):

grant_type=client_credentials&client_id=admin-client&client_secret=super-secret-admin-password-here
Field Type Required Description
grant_type string Yes Must be "client_credentials"
client_id string Yes Registered client identifier
client_secret string Yes Client secret

Success response (200):

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbi1jbGllbnQiLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MDk4MjAwMDAsImV4cCI6MTcwOTgyMzYwMCwiaXNzIjoiYmlsYnljYXN0LWVkZ2UifQ.signature",
"token_type": "bearer",
"expires_in": 3600,
"role": "admin"
}
Field Type Description
access_token string Signed HS256 JWT token
token_type string Always "bearer"
expires_in integer Token lifetime in seconds
role string Role granted: "admin" or "monitor"

Error responses:

Status Condition
400 Auth not enabled, unsupported grant_type, invalid credentials, unparseable body

curl examples:

Terminal window
# JSON body
curl -X POST http://localhost:8080/oauth/token \
-H "Content-Type: application/json" \
-d '{"grant_type":"client_credentials","client_id":"admin-client","client_secret":"super-secret-admin-password-here"}'
# Form-urlencoded body
curl -X POST http://localhost:8080/oauth/token \
-d "grant_type=client_credentials&client_id=admin-client&client_secret=super-secret-admin-password-here"

JWT Claims structure:

The returned JWT contains these claims:

Claim Type Description
sub string The client_id
role string "admin" or "monitor"
iat integer Issued-at timestamp (Unix epoch seconds)
exp integer Expiration timestamp (Unix epoch seconds)
iss string Always "bilbycast-edge"

Inputs are top-level, first-class entities, independent of any flow. A flow references inputs by ID via its input_ids array; the same input can be referenced by more than one flow. Create/read/update/delete them here, then wire them into flows.

The request/response body is an InputDefinition whose type field selects the variant. type is a free-form string: rtp, rtp_audio, srt, rist, rtmp, rtsp, webrtc, whep, bonded, media_player, test_pattern, replay, st2110_20, st2110_23, st2110_30, st2110_31, st2110_40 (plus mxl_video / mxl_audio / mxl_anc under the mxl feature, and sdi under sdi-decklink). See the Configuration Guide for per-type fields.

List all configured inputs.

Auth: Requires valid JWT (any role).

Create a new input. The definition is validated and persisted.

Auth: Requires admin role.

Request body:

{
"type": "rtp",
"id": "rtp-in-1",
"name": "Main RTP Input",
"bind_addr": "239.1.1.1:5000",
"interface_addr": "192.168.1.100",
"fec_decode": { "columns": 10, "rows": 10 }
}

Error responses:

Status Condition
400 Validation failure (invalid addresses, empty ID/name, bad FEC params)
409 An input with the same ID already exists
500 Failed to persist config to disk

Retrieve a single input definition.

Auth: Requires valid JWT (any role). Returns 404 if not found.

Replace an input definition. Flows referencing this input pick up the change on their next (re)start; hot-swappable changes are applied surgically without stopping running outputs where possible.

Auth: Requires admin role.

Delete an input. Fails if the input is still referenced by a flow.

Auth: Requires admin role.

Error responses:

Status Condition
404 Input not found
409 Input is still referenced by one or more flows
500 Failed to persist config to disk

List all configured flows. Returns a summary for each flow without full input/output details.

Auth: Requires valid JWT (any role: admin or monitor).

Response (200):

{
"success": true,
"data": {
"flows": [
{
"id": "main-feed",
"name": "Main Program Feed",
"enabled": true,
"input_type": "rtp",
"output_count": 3
},
{
"id": "backup-srt",
"name": "SRT Backup Path",
"enabled": true,
"input_type": "srt",
"output_count": 1
}
]
}
}
Field Type Description
flows[].id string Unique flow identifier
flows[].name string Human-readable display name
flows[].enabled boolean Whether the flow is enabled in config
flows[].input_type string Type of the flow’s first referenced input. A free-form string — one of rtp, rtp_audio, srt, rist, rtmp, rtsp, webrtc, whep, bonded, media_player, test_pattern, replay, st2110_20, st2110_23, st2110_30, st2110_31, st2110_40 (plus mxl_video / mxl_audio / mxl_anc under the mxl feature, and sdi under sdi-decklink).
flows[].output_count integer Number of outputs referenced by the flow

curl example:

Terminal window
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/flows

Retrieve the full configuration of a single flow. A flow references its input(s) and output(s) by ID — the returned object carries input_ids and output_ids, not embedded definitions. Fetch the referenced entities from GET /api/v1/inputs/{id} and GET /api/v1/outputs/{id}.

Auth: Requires valid JWT (any role).

Path parameters:

Parameter Type Description
flow_id string Unique identifier of the flow

Response (200):

{
"success": true,
"data": {
"id": "main-feed",
"name": "Main Program Feed",
"enabled": true,
"input_ids": ["rtp-in-1"],
"output_ids": ["rtp-out-1", "srt-out-1"]
}
}

input_ids is normally a single-element array; it carries multiple IDs for redundancy (SMPTE 2022-7 hitless) and switcher flows. output_ids lists every output the flow fans out to. The engine dereferences these into a ResolvedFlow (pulling each InputDefinition / OutputConfig from the top-level entity tables) before starting the flow.

Error responses:

Status Condition
404 Flow not found

curl example:

Terminal window
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/flows/main-feed

Create a new flow. The flow is validated, persisted to the config file, and (if enabled: true) started immediately. The input_ids and output_ids must reference inputs/outputs that already exist as top-level entities — create them first via POST /api/v1/inputs and POST /api/v1/outputs.

Auth: Requires admin role.

Request body:

{
"id": "new-flow",
"name": "New Feed",
"enabled": true,
"input_ids": ["rtp-in-1"],
"output_ids": ["out-1"]
}

Response (200):

Returns the created flow configuration in the standard envelope.

Error responses:

Status Condition
400 Validation failure (invalid addresses, empty ID/name, bad FEC params)
409 A flow with the same ID already exists
500 Failed to persist config to disk

curl example:

Terminal window
curl -X POST http://localhost:8080/api/v1/flows \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "new-flow",
"name": "New Feed",
"enabled": true,
"input_ids": ["rtp-in-1"],
"output_ids": ["out-1"]
}'

Replace an existing flow’s configuration. The flow ID in the path takes precedence over the id field in the body. If the flow is running, it is stopped, updated, and restarted (if enabled).

Auth: Requires admin role.

Path parameters:

Parameter Type Description
flow_id string Unique identifier of the flow to update

Request body:

Full FlowConfig JSON (same structure as POST /api/v1/flows).

Response (200):

Returns the updated flow configuration.

Error responses:

Status Condition
400 Validation failure
404 Flow not found
500 Failed to persist config to disk

curl example:

Terminal window
curl -X PUT http://localhost:8080/api/v1/flows/main-feed \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "main-feed",
"name": "Main Feed (Updated)",
"enabled": true,
"input_ids": ["rtp-in-1"],
"output_ids": ["out-1"]
}'

Delete a flow. Stops the flow if running, removes it from configuration, and persists the change.

Auth: Requires admin role.

Path parameters:

Parameter Type Description
flow_id string Unique identifier of the flow to delete

Response (200):

{
"success": true,
"data": null
}

Error responses:

Status Condition
404 Flow not found
500 Failed to persist config to disk

curl example:

Terminal window
curl -X DELETE http://localhost:8080/api/v1/flows/old-flow \
-H "Authorization: Bearer $TOKEN"

Start a stopped flow. Reads the flow configuration and starts it in the engine.

Auth: Requires admin role.

Path parameters:

Parameter Type Description
flow_id string Unique identifier of the flow to start

Response (200):

{
"success": true,
"data": null
}

Error responses:

Status Condition
404 Flow not found in configuration
409 Flow is already running
500 Engine failed to start the flow

curl example:

Terminal window
curl -X POST http://localhost:8080/api/v1/flows/main-feed/start \
-H "Authorization: Bearer $TOKEN"

Stop a running flow. The flow configuration is preserved; only the runtime instance is torn down. The flow can be restarted later.

Auth: Requires admin role.

Path parameters:

Parameter Type Description
flow_id string Unique identifier of the flow to stop

Response (200):

{
"success": true,
"data": null
}

Error responses:

Status Condition
404 Flow not found in configuration
409 Flow is not currently running
500 Engine failed to stop the flow

curl example:

Terminal window
curl -X POST http://localhost:8080/api/v1/flows/main-feed/stop \
-H "Authorization: Bearer $TOKEN"

Restart a flow (stop + start). Destroys the running instance (if any) and creates a fresh one from the current configuration. Useful for picking up config changes or recovering from transient errors.

Auth: Requires admin role.

Path parameters:

Parameter Type Description
flow_id string Unique identifier of the flow to restart

Response (200):

{
"success": true,
"data": null
}

Error responses:

Status Condition
404 Flow not found in configuration
500 Engine failed to create the new flow instance

curl example:

Terminal window
curl -X POST http://localhost:8080/api/v1/flows/main-feed/restart \
-H "Authorization: Bearer $TOKEN"

POST /api/v1/flows/{flow_id}/activate-input

Section titled “POST /api/v1/flows/{flow_id}/activate-input”

Switch the live input of a multi-input flow (switcher / redundancy). Selects which of the flow’s input_ids is on-air without restarting the flow.

Auth: Requires admin role.

Request body:

{
"input_id": "backup-srt-in"
}

Error responses:

Status Condition
400 The named input is not one of the flow’s input_ids
404 Flow not found
409 Flow is not running

Hot-swap the PID-bus assembly plan of an assembled (PID-bus / MPTS) flow in place, without tearing the flow down. Used to re-program the ES-to-program mapping of a running assembled flow.

Auth: Requires admin role.

Request body: A flow-assembly plan object (see the Configuration Guide).

Error responses:

Status Condition
400 Assembly plan validation failure
404 Flow not found
409 Flow is not an assembled flow

Like inputs, outputs are top-level, first-class entities; a flow references them by ID via its output_ids array. The body is an OutputConfig whose type field selects the variant — a free-form string: udp, rtp, srt, rist, rtmp, rtsp, webrtc, whip, hls, cmaf, bonded, display, st2110_20, st2110_23, st2110_30, st2110_31, st2110_40 (plus mxl_video / mxl_audio / mxl_anc under the mxl feature, and sdi under sdi-decklink). See the Configuration Guide for per-type fields.

List all configured outputs.

Auth: Requires valid JWT (any role).

Create a new top-level output. Validated and persisted.

Auth: Requires admin role.

Error responses:

Status Condition
400 Output validation failure
409 An output with the same ID already exists
500 Failed to persist config to disk

Retrieve a single output definition. Returns 404 if not found.

Auth: Requires valid JWT (any role).

Replace an output definition.

Auth: Requires admin role.

Delete an output. Fails if the output is still referenced by a flow.

Auth: Requires admin role.

Error responses:

Status Condition
404 Output not found
409 Output is still referenced by one or more flows
500 Failed to persist config to disk

Toggle an output on or off at runtime without removing it from the flow. A disabled output stops emitting but stays configured.

Auth: Requires admin role.

Request body:

{
"active": false
}

Error responses:

Status Condition
404 Output not found

Add a new output to an existing flow. The output is validated, appended to the flow’s output list, and persisted. If the flow is currently running, the output is hot-added without stopping the flow.

Auth: Requires admin role.

Path parameters:

Parameter Type Description
flow_id string Flow to add the output to

Request body:

An OutputConfig object. The type field determines the output kind. See the Configuration Guide for all output types and their fields.

{
"type": "srt",
"id": "srt-backup",
"name": "SRT Backup Output",
"mode": "caller",
"local_addr": "0.0.0.0:0",
"remote_addr": "203.0.113.20:9000",
"latency_ms": 300
}

Response (200):

Returns the created output configuration.

Error responses:

Status Condition
400 Output validation failure
404 Flow not found
409 An output with the same ID already exists in the flow
500 Failed to persist config to disk

curl example:

Terminal window
curl -X POST http://localhost:8080/api/v1/flows/main-feed/outputs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "srt",
"id": "srt-backup",
"name": "SRT Backup Output",
"mode": "caller",
"local_addr": "0.0.0.0:0",
"remote_addr": "203.0.113.20:9000",
"latency_ms": 300
}'

DELETE /api/v1/flows/{flow_id}/outputs/{output_id}

Section titled “DELETE /api/v1/flows/{flow_id}/outputs/{output_id}”

Remove an output from a flow. If the flow is running, the output is hot-removed from the engine first, then removed from config and persisted.

Auth: Requires admin role.

Path parameters:

Parameter Type Description
flow_id string Flow containing the output
output_id string Output to remove

Response (200):

{
"success": true,
"data": null
}

Error responses:

Status Condition
404 Flow not found, or output not found within the flow
500 Failed to persist config to disk

curl example:

Terminal window
curl -X DELETE http://localhost:8080/api/v1/flows/main-feed/outputs/srt-backup \
-H "Authorization: Bearer $TOKEN"

Read and update the node’s PTP (Precision Time Protocol) settings, used by the master-clock discipline for ST 2110 / MXL and PTP-locked flows. The edge reports the live lock/offset view separately via FlowStats.ptp_state.

Retrieve the current PTP configuration and status.

Auth: Requires valid JWT (any role).

Update the PTP configuration (e.g. clock domain, interface).

Auth: Requires admin role.


Retrieve aggregated system-wide and per-flow statistics. Running flows include live counters; configured-but-stopped flows are included with zeroed counters.

Auth: Requires valid JWT (any role).

Response (200):

{
"success": true,
"data": {
"system": {
"uptime_secs": 86400,
"total_flows": 3,
"active_flows": 2,
"version": "0.1.0"
},
"flows": [
{
"flow_id": "main-feed",
"flow_name": "Main Program Feed",
"state": "Running",
"health": "Healthy",
"uptime_secs": 86350,
"input": {
"input_type": "udp",
"state": "receiving",
"packets_received": 15234567,
"bytes_received": 20113628844,
"bitrate_bps": 50000000,
"packets_lost": 12,
"packets_filtered": 0,
"packets_recovered_fec": 8,
"redundancy_switches": 0,
"srt_stats": null,
"srt_leg2_stats": null
},
"outputs": [
{
"output_id": "rtp-out-1",
"output_name": "Local Playout",
"output_type": "udp",
"state": "active",
"packets_sent": 15234555,
"bytes_sent": 20113612680,
"bitrate_bps": 50000000,
"packets_dropped": 0,
"fec_packets_sent": 3046911,
"srt_stats": null,
"srt_leg2_stats": null
}
],
"tr101290": {
"ts_packets_analyzed": 106641969,
"pat_count": 17280,
"pmt_count": 17280,
"sync_loss_count": 0,
"sync_byte_errors": 0,
"cc_errors": 0,
"pat_errors": 0,
"pmt_errors": 0,
"tei_errors": 0,
"pcr_discontinuity_errors": 0,
"pcr_accuracy_errors": 0,
"priority1_ok": true,
"priority2_ok": true,
"tr07_compliant": false,
"jpeg_xs_pid": null
},
"media_analysis": {
"protocol": "srt",
"payload_format": "raw_ts",
"fec": null,
"redundancy": null,
"program_count": 1,
"video_streams": [
{
"pid": 256,
"codec": "H.264/AVC",
"stream_type": 27,
"resolution": "1920x1080",
"frame_rate": 29.97,
"profile": "High",
"level": "4.0",
"bitrate_bps": 5000000
}
],
"audio_streams": [
{
"pid": 257,
"codec": "AAC-LC",
"stream_type": 15,
"sample_rate_hz": 48000,
"channels": 2,
"language": "eng",
"bitrate_bps": 128000
}
],
"total_bitrate_bps": 5200000
},
"iat": {
"min_us": 120.5,
"max_us": 180.3,
"avg_us": 142.7
},
"pdv_jitter_us": 15.2
}
]
}
}

System stats fields:

Field Type Description
uptime_secs integer Application uptime in seconds
total_flows integer Total configured flows
active_flows integer Currently running flows
version string Application version

Per-flow fields:

Field Type Description
flow_id string Flow identifier
flow_name string Display name
state string "Idle", "Starting", "Running", "Error", or "Stopped"
health string "Healthy", "Warning", "Error", or "Critical" (RP 2129 M6)
uptime_secs integer Seconds since the flow was started
input object Input leg statistics (see below)
outputs array Per-output statistics (see below)
tr101290 object/null TR-101290 analysis (present when running)
media_analysis object/null Media content analysis — codec, resolution, frame rate, audio format, per-PID bitrate (present when running and media_analysis config is true)
iat object/null Inter-arrival time stats in microseconds
pdv_jitter_us float/null Packet delivery variation (jitter) in microseconds
bandwidth_exceeded boolean true if the flow’s input bitrate currently exceeds the configured bandwidth_limit. Omitted when false.
bandwidth_blocked boolean true if the flow is currently gated (packets dropped) due to bandwidth limit enforcement. Omitted when false.
bandwidth_limit_mbps float/null Configured bandwidth limit in Mbps (for display). Absent if no limit configured.

Input stats fields:

Field Type Description
input_type string Input variant — a free-form string such as rtp, rtp_audio, srt, rist, rtmp, rtsp, webrtc, whep, bonded, media_player, test_pattern, replay, or st2110_20 / st2110_23 / st2110_30 / st2110_31 / st2110_40 (plus mxl_* / sdi under their features)
state string Connection state (e.g., "receiving", "connecting")
packets_received integer Total RTP packets received
bytes_received integer Total bytes received
bitrate_bps integer Current bitrate in bits/sec
packets_lost integer Packets lost (sequence gaps)
packets_filtered integer Packets dropped by ingress filters
packets_recovered_fec integer Packets recovered via FEC
srt_stats object/null SRT leg 1 stats (if SRT input)
srt_leg2_stats object/null SRT leg 2 stats (if redundancy enabled)
redundancy_switches integer SMPTE 2022-7 leg switch count

Output stats fields:

Field Type Description
output_id string Output identifier
output_name string Display name
output_type string Output variant — a free-form string such as udp, rtp, srt, rist, rtmp, rtsp, webrtc, whip, hls, cmaf, bonded, display, or st2110_20 / st2110_23 / st2110_30 / st2110_31 / st2110_40 (plus mxl_* / sdi under their features)
state string Connection state
packets_sent integer Total packets sent
bytes_sent integer Total bytes sent
bitrate_bps integer Current bitrate in bits/sec
packets_dropped integer Packets dropped (channel full)
fec_packets_sent integer FEC packets sent
wire_pacing_tier string Active wire-pacing release tier — "so_txtime", "clock_nanosleep_fifo", or "clock_nanosleep". Set once at output startup. Default is "clock_nanosleep_fifo"; the SO_TXTIME tier activates only when BILBYCAST_ENABLE_TXTIME=1 is set on the edge process AND the kernel accepts the setsockopt (requires CAP_NET_ADMIN). See Wire-Time Precision. Absent on outputs that don’t own a UDP socket directly (SRT, RIST, RTMP, HLS, CMAF, WebRTC).
wire_pacing_late integer Datagrams the kernel rejected as late on the SO_TXTIME path (target tx time landed in the past — EOVERFLOW). Always 0 on userspace-sleep release paths (no errqueue).
srt_stats object/null SRT leg 1 stats (if SRT output)
srt_leg2_stats object/null SRT leg 2 stats (if redundancy)

SRT leg stats fields:

Field Type Description
state string SRT socket state (e.g., "connected", "broken")
rtt_ms float Round-trip time in milliseconds
send_rate_mbps float Estimated send rate in Mbps
recv_rate_mbps float Estimated receive rate in Mbps
pkt_loss_total integer Total packets lost
pkt_retransmit_total integer Total retransmitted packets
uptime_ms integer Socket uptime in milliseconds

curl example:

Terminal window
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/stats

Retrieve statistics for a single flow. Returns live stats if running, or zeroed stats if the flow is configured but stopped.

Auth: Requires valid JWT (any role).

Path parameters:

Parameter Type Description
flow_id string Flow to retrieve stats for

Response (200):

Returns a single FlowStats object (same structure as entries in the flows array above).

Error responses:

Status Condition
404 Flow not found in engine or configuration

curl example:

Terminal window
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/stats/main-feed

Retrieve the running application configuration with infrastructure secrets stripped. Returns the operational config including flow definitions with all user-configured parameters (SRT passphrases, RTSP credentials, RTMP stream keys, bearer tokens). Infrastructure secrets (node_secret, tunnel encryption keys, JWT secrets, client credentials, TLS config) are never included.

Auth: Requires valid JWT (any role).

Response (200):

{
"success": true,
"data": {
"version": 2,
"server": {
"listen_addrs": ["127.0.0.1", "[::1]"],
"listen_port": 8080
},
"monitor": {
"listen_addr": "0.0.0.0",
"listen_port": 9090
},
"flows": [ ... ]
}
}

curl example:

Terminal window
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/config

Replace the entire application configuration atomically. Stops all running flows, replaces the in-memory config, persists to disk (flow configs including user parameters to config.json, infrastructure secrets to secrets.json), and starts all flows with enabled: true.

Auth: Requires admin role.

Request body:

A complete AppConfig JSON object (see Configuration Guide). Flow parameters (SRT passphrases, RTSP credentials, RTMP keys, etc.) are stored in config.json. Infrastructure secrets (auth config, TLS) are stored in secrets.json.

Response (200):

Returns the new configuration with infrastructure secrets stripped (flow parameters preserved).

Error responses:

Status Condition
400 Config validation failure (duplicate flow IDs, invalid addresses, etc.)
500 Failed to persist config or individual flows failed to start

curl example:

Terminal window
curl -X PUT http://localhost:8080/api/v1/config \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @config.json

Reload configuration from disk (config.json + secrets.json). Stops all running flows, reads and validates both files, merges secrets into the config, replaces the in-memory state, and starts all enabled flows. Useful after manual edits to the config files.

Auth: Requires admin role.

Request body: None.

Response (200):

Returns the reloaded configuration with secrets stripped.

Error responses:

Status Condition
400 Loaded config fails validation
500 Config file cannot be read or parsed

curl example:

Terminal window
curl -X POST http://localhost:8080/api/v1/config/reload \
-H "Authorization: Bearer $TOKEN"

Prometheus-compatible metrics endpoint. Returns metrics in the Prometheus text exposition format (text/plain; version=0.0.4).

Auth: Public by default when public_metrics: true (the default). When public_metrics: false, requires a valid JWT (any role).

Metric families:

Application-level gauges:

Metric Type Description
bilbycast_edge_info{version="..."} gauge Application version (always 1)
bilbycast_edge_uptime_seconds gauge Seconds since startup
bilbycast_edge_flows_total gauge Total configured flows
bilbycast_edge_flows_active gauge Currently running flows

Per-flow input metrics (labeled by flow_id):

Metric Type Description
bilbycast_edge_flow_input_packets_total counter RTP packets received
bilbycast_edge_flow_input_bytes_total counter Bytes received
bilbycast_edge_flow_input_bitrate_bps gauge Input bitrate (bits/sec)
bilbycast_edge_flow_input_packets_lost counter Packets lost
bilbycast_edge_flow_input_fec_recovered_total counter Packets recovered via FEC
bilbycast_edge_flow_input_redundancy_switches_total counter SMPTE 2022-7 leg switches
bilbycast_edge_flow_input_packets_filtered counter Packets dropped by ingress filters
bilbycast_edge_flow_pdv_jitter_us gauge PDV jitter in microseconds
bilbycast_edge_flow_iat_avg_us gauge Average inter-arrival time in microseconds

Per-output metrics (labeled by flow_id, output_id):

Metric Type Description
bilbycast_edge_flow_output_packets_total counter Packets sent
bilbycast_edge_flow_output_bytes_total counter Bytes sent
bilbycast_edge_flow_output_bitrate_bps gauge Output bitrate (bits/sec)
bilbycast_edge_flow_output_packets_dropped counter Packets dropped
bilbycast_edge_flow_output_fec_sent_total counter FEC packets sent

SRT metrics (labeled by flow_id, optionally output_id and leg):

Metric Type Description
bilbycast_edge_srt_rtt_ms gauge SRT round-trip time in ms
bilbycast_edge_srt_loss_total counter SRT total packet loss

TR-101290 metrics (labeled by flow_id):

Metric Type Description
bilbycast_edge_tr101290_ts_packets_total counter TS packets analyzed
bilbycast_edge_tr101290_sync_byte_errors_total counter Sync byte errors
bilbycast_edge_tr101290_cc_errors_total counter Continuity counter errors
bilbycast_edge_tr101290_pat_errors_total counter PAT timeout errors
bilbycast_edge_tr101290_pmt_errors_total counter PMT timeout errors
bilbycast_edge_tr101290_tei_errors_total counter Transport error indicator errors
bilbycast_edge_tr101290_pcr_discontinuity_errors_total counter PCR discontinuity errors
bilbycast_edge_tr101290_pcr_accuracy_errors_total counter PCR accuracy errors

Media analysis metrics (labeled by flow_id and pid):

Metric Type Description
bilbycast_edge_media_video_info info Video stream info (labels: codec, resolution, profile, level)
bilbycast_edge_media_video_framerate gauge Video frame rate in fps
bilbycast_edge_media_audio_info info Audio stream info (labels: codec, sample_rate, channels, language)
bilbycast_edge_media_pid_bitrate_bps gauge Per-PID bitrate in bits/sec (label: type = video or audio)
bilbycast_edge_media_total_bitrate_bps gauge Total TS bitrate in bits/sec

Only metrics for currently running flows are emitted.

curl example:

Terminal window
curl http://localhost:8080/metrics

List all active IP tunnels.

Auth: Requires valid JWT (any role) when auth is enabled.

Response (200 OK):

{
"tunnels": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Stadium to Studio",
"protocol": "udp",
"mode": "relay",
"direction": "egress",
"state": "Connected",
"local_addr": "0.0.0.0:9000",
"relay_addrs": [
"relay-primary.example.com:4433",
"relay-backup.example.com:4433"
],
"active_relay_idx": 0,
"active_relay_addr": "relay-primary.example.com:4433",
"stats": { "packets_sent": 0, "packets_received": 0, "bytes_sent": 0, "bytes_received": 0 }
}
]
}

active_relay_idx and active_relay_addr only appear for running relay-mode tunnels and reflect which relay is currently carrying traffic (0 = primary). When the primary is unreachable and failover occurs, these flip to 1 / the backup address.

Get status of a specific tunnel.

Auth: Requires valid JWT (any role) when auth is enabled.

Response (200 OK):

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Stadium to Studio",
"protocol": "udp",
"mode": "relay",
"direction": "egress",
"state": "Connected",
"local_addr": "0.0.0.0:9000",
"relay_addrs": [
"relay-primary.example.com:4433",
"relay-backup.example.com:4433"
],
"active_relay_idx": 1,
"active_relay_addr": "relay-backup.example.com:4433",
"stats": { "packets_sent": 0, "packets_received": 0, "bytes_sent": 0, "bytes_received": 0 }
}

Response (404 Not Found):

{
"error": "Tunnel not found"
}

Create a new IP tunnel. The tunnel configuration is validated before creation.

Auth: Requires valid JWT with admin role when auth is enabled.

Request body: A TunnelConfig JSON object. See Tunnel Configuration for all fields.

Example — relay mode UDP tunnel:

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Stadium to Studio",
"enabled": true,
"protocol": "udp",
"mode": "relay",
"direction": "egress",
"local_addr": "0.0.0.0:9000",
"relay_addrs": [
"relay-primary.example.com:4433",
"relay-backup.example.com:4433"
],
"tunnel_encryption_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}

relay_addrs is an ordered list (primary first, optional backup second). A single-relay tunnel uses a one-element list. The legacy single-field "relay_addr": "host:port" form is still accepted and migrated automatically.

Response (201 Created):

{
"status": "created"
}

Response (400 Bad Request):

{
"error": "tunnel_encryption_key is required for relay mode"
}

Destroy a tunnel and clean up its connections.

Auth: Requires valid JWT with admin role when auth is enabled.

Response (200 OK):

{
"status": "deleted"
}

Response (404 Not Found):

{
"error": "Tunnel not found"
}

WebSocket endpoint for real-time statistics streaming. Upgrades the HTTP connection to a WebSocket and pushes JSON stats messages at approximately 1-second intervals.

Auth: Requires valid JWT (any role) when auth is enabled. The token can be passed as a standard Authorization: Bearer header on the upgrade request or as a query parameter for browser clients (see Security Guide).

Protocol: This is a server-push channel. The server sends JSON text frames; client-to-server messages are ignored.

Message format:

Each WebSocket text frame contains a JSON array of per-flow stats snapshots:

[
{
"flow_id": "main-feed",
"flow_name": "Main Program Feed",
"state": "Running",
"health": "Healthy",
"uptime_secs": 86350,
"input": { ... },
"outputs": [ ... ],
"tr101290": { ... },
"iat": { ... },
"pdv_jitter_us": 15.2
}
]

The structure of each flow stats object is identical to the entries in GET /api/v1/stats.

Connection behavior:

  • Messages are broadcast on a shared channel. If a client falls behind, messages are skipped (lagged) rather than buffered.
  • The connection closes when the client sends a Close frame, disconnects, or when the broadcast channel is closed.

JavaScript example:

const ws = new WebSocket("ws://localhost:8080/api/v1/ws/stats");
ws.onmessage = (event) => {
const flows = JSON.parse(event.data);
flows.forEach(flow => {
console.log(`${flow.flow_id}: ${flow.input.bitrate_bps} bps, health=${flow.health}`);
});
};
ws.onerror = (err) => console.error("WebSocket error:", err);
ws.onclose = () => console.log("WebSocket closed");

curl example (wscat):

Terminal window
wscat -c "ws://localhost:8080/api/v1/ws/stats" \
-H "Authorization: Bearer $TOKEN"

All API errors return a JSON body with "success": false and an "error" message string.

HTTP Status Error Type Description
400 Bad Request Request body failed validation, malformed JSON, unsupported grant type
401 Unauthorized Missing or invalid Authorization header, expired token, bad signature
403 Forbidden Valid token but insufficient role (admin required)
404 Not Found Flow or output does not exist
409 Conflict Resource already exists, flow already running/stopped
500 Internal Server Error Disk I/O failure, engine error, JWT signing failure

Example error responses:

// 401 Unauthorized
{
"success": false,
"error": "missing Authorization header"
}
// 403 Forbidden
{
"success": false,
"error": "admin role required"
}
// 404 Not Found
{
"success": false,
"error": "Flow 'nonexistent' not found"
}
// 409 Conflict
{
"success": false,
"error": "Flow 'main-feed' is already running"
}

Method Path Auth Role Description
GET /health No - Health check
POST /oauth/token No - Get JWT token
GET /metrics Configurable any Prometheus metrics
GET /api/v1/inputs Yes any List all inputs
POST /api/v1/inputs Yes admin Create input
GET /api/v1/inputs/{input_id} Yes any Get input details
PUT /api/v1/inputs/{input_id} Yes admin Update input
DELETE /api/v1/inputs/{input_id} Yes admin Delete input
GET /api/v1/outputs Yes any List all outputs
POST /api/v1/outputs Yes admin Create output
GET /api/v1/outputs/{output_id} Yes any Get output details
PUT /api/v1/outputs/{output_id} Yes admin Update output
DELETE /api/v1/outputs/{output_id} Yes admin Delete output
POST /api/v1/outputs/{output_id}/active Yes admin Enable/disable output at runtime
GET /api/v1/flows Yes any List all flows
GET /api/v1/flows/{flow_id} Yes any Get flow details
POST /api/v1/flows Yes admin Create flow
PUT /api/v1/flows/{flow_id} Yes admin Update flow
DELETE /api/v1/flows/{flow_id} Yes admin Delete flow
POST /api/v1/flows/{flow_id}/start Yes admin Start flow
POST /api/v1/flows/{flow_id}/stop Yes admin Stop flow
POST /api/v1/flows/{flow_id}/restart Yes admin Restart flow
POST /api/v1/flows/{flow_id}/activate-input Yes admin Switch live input (switcher/redundancy)
PUT /api/v1/flows/{flow_id}/assembly Yes admin Hot-swap PID-bus assembly plan
POST /api/v1/flows/{flow_id}/outputs Yes admin Add output to flow
DELETE /api/v1/flows/{flow_id}/outputs/{output_id} Yes admin Remove output from flow
POST /api/v1/flows/{flow_id}/whip Yes admin WHIP: Accept WebRTC publisher (SDP offer → answer)
DELETE /api/v1/flows/{flow_id}/whip/{session_id} Yes admin WHIP: Disconnect publisher
POST /api/v1/flows/{flow_id}/whep Yes admin WHEP: Accept WebRTC viewer (SDP offer → answer)
DELETE /api/v1/flows/{flow_id}/whep/{session_id} Yes admin WHEP: Disconnect viewer
GET /api/v1/tunnels Yes any List all tunnels
GET /api/v1/tunnels/{id} Yes any Get tunnel status
POST /api/v1/tunnels Yes admin Create tunnel
DELETE /api/v1/tunnels/{id} Yes admin Delete tunnel
GET /api/v1/stats Yes any All statistics
GET /api/v1/stats/{flow_id} Yes any Single flow stats
GET /api/v1/config Yes any Get running config
PUT /api/v1/config Yes admin Replace entire config
POST /api/v1/config/reload Yes admin Reload config from disk
GET /api/v1/ptp Yes any Get PTP configuration + status
PUT /api/v1/ptp Yes admin Update PTP configuration
GET /api/v1/ws/stats Yes any WebSocket stats stream
GET /x-nmos/node/v1.3/ No - NMOS IS-04: Node API root
GET /x-nmos/node/v1.3/self No - NMOS IS-04: Node resource
GET /x-nmos/node/v1.3/devices/ No - NMOS IS-04: List devices
GET /x-nmos/node/v1.3/devices/{id} No - NMOS IS-04: Get device
GET /x-nmos/node/v1.3/sources/ No - NMOS IS-04: List sources
GET /x-nmos/node/v1.3/sources/{id} No - NMOS IS-04: Get source
GET /x-nmos/node/v1.3/flows/ No - NMOS IS-04: List flows
GET /x-nmos/node/v1.3/flows/{id} No - NMOS IS-04: Get flow
GET /x-nmos/node/v1.3/senders/ No - NMOS IS-04: List senders
GET /x-nmos/node/v1.3/senders/{id} No - NMOS IS-04: Get sender
GET /x-nmos/node/v1.3/receivers/ No - NMOS IS-04: List receivers
GET /x-nmos/node/v1.3/receivers/{id} No - NMOS IS-04: Get receiver
GET /x-nmos/connection/v1.1/single/senders/ No - NMOS IS-05: List senders
GET /x-nmos/connection/v1.1/single/senders/{id}/staged No - NMOS IS-05: Get staged params
PATCH /x-nmos/connection/v1.1/single/senders/{id}/staged No - NMOS IS-05: Update staged + activate
GET /x-nmos/connection/v1.1/single/senders/{id}/active No - NMOS IS-05: Get active params
GET /x-nmos/connection/v1.1/single/senders/{id}/transporttype No - NMOS IS-05: Get transport type
GET /x-nmos/connection/v1.1/single/senders/{id}/constraints No - NMOS IS-05: Get constraints
GET /x-nmos/connection/v1.1/single/receivers/ No - NMOS IS-05: List receivers
GET /x-nmos/connection/v1.1/single/receivers/{id}/staged No - NMOS IS-05: Get staged params
PATCH /x-nmos/connection/v1.1/single/receivers/{id}/staged No - NMOS IS-05: Update staged + activate
GET /x-nmos/connection/v1.1/single/receivers/{id}/active No - NMOS IS-05: Get active params
GET /x-nmos/connection/v1.1/single/receivers/{id}/transporttype No - NMOS IS-05: Get transport type
GET /x-nmos/connection/v1.1/single/receivers/{id}/constraints No - NMOS IS-05: Get constraints
GET /x-nmos/channelmapping/v1.0/ No - NMOS IS-08: Channel mapping root
GET /x-nmos/channelmapping/v1.0/io No - NMOS IS-08: List inputs/outputs (ST 2110-30/-31 audio)
GET /x-nmos/channelmapping/v1.0/map/active No - NMOS IS-08: Active channel map (persisted to disk)
GET /x-nmos/channelmapping/v1.0/map/staged No - NMOS IS-08: Staged channel map (in-memory)
POST /x-nmos/channelmapping/v1.0/map/staged No - NMOS IS-08: Stage a new channel map (1024 outputs × 64 ch limit)
POST /x-nmos/channelmapping/v1.0/map/activate No - NMOS IS-08: Activate the staged map and persist

ST 2110-30/-31 inputs are reported as urn:x-nmos:format:audio sources/flows/receivers; ST 2110-40 inputs are reported as urn:x-nmos:format:data. Older mux flows continue to report urn:x-nmos:format:mux so existing NMOS controllers see no behavioural change.

Audio receivers (InputConfig::St2110_30 / St2110_31) include a caps block with a media_types list and a constraint_sets list keyed by urn:x-nmos:cap:format:* URNs:

{
"caps": {
"media_types": ["audio/L16", "audio/L24"],
"constraint_sets": [{
"urn:x-nmos:cap:format:media_type": { "enum": ["audio/L16", "audio/L24"] },
"urn:x-nmos:cap:format:sample_rate": { "enum": [{ "numerator": 48000 }] },
"urn:x-nmos:cap:format:channel_count": { "enum": [2] },
"urn:x-nmos:cap:format:sample_depth": { "enum": [24] }
}]
}
}

ST 2110-40 receivers advertise media_types: ["video/smpte291"]. Non-ST-2110 receivers continue to advertise the historical video/MP2T shape.

When any flow on the node sets clock_domain, the IS-04 /self resource includes a single PTP clock entry (name: "clk0", ref_type: "ptp", version: "IEEE1588-2008"). Sources whose flow has clock_domain set reference this clock by name. The locked field is reported as false until live PTP integration lands; FlowStats.ptp_state.lock_state carries the real view.

The IS-08 endpoints expose every ST 2110-30/-31 audio input and output under /io. The active map is persisted to <config_dir>/nmos_channel_map.json (next to config.json) and reloaded on startup. The endpoints support the standard PUT/POST + activate workflow. Bilbycast does not currently re-route channels internally — the map is a passthrough — but the endpoints exist so external NMOS controllers can stage and activate maps.

Bounds: at most 1024 outputs per map, at most 64 channels per output. A controller exceeding these limits receives a 413 PAYLOAD_TOO_LARGE response.

On startup the edge registers _nmos-node._tcp.local. via the mdns-sd crate. Failures (no multicast on the selected interface, daemon errors) are logged once and swallowed; flow startup is never blocked.