Skip to content

Client posts its session telemetry (owner or admin).

POST
/v1/sessions/{id}/stats
curl --request POST \
--url http://localhost:8080/v1/sessions/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/stats \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "client": "browser", "samples": [ { "ts_unix_ms": 1, "metrics": { "fps": 1, "bitrate_kbps": 1, "rtt_ms": 1, "jitter_buffer_ms": 1, "decode_ms": 1, "packets_lost": 1, "frames_dropped": 1, "present_fps": 1, "present_fps_median": 1, "present_interval_median_ms": 1, "present_interval_sd_ms": 1, "present_interval_p95_ms": 1, "present_interval_max_ms": 1, "present_beat_fraction": 1, "present_long_frames": 1, "present_n": 1, "playout_target_ms": 1, "rvfc_capture_time_available": 1, "abs_capture_time_negotiated": 1, "freeze_count": 1, "display_refresh_hz": 1, "glass_to_glass_ms": 1, "rvfc_capture_to_display_ms": 1, "encode_ms": 1, "network_pacing_ms": 1, "decode_display_ms": 1, "is_hidden": 1, "input_backpressure": 1, "input_channel_buffered_bytes": 1, "input_coalesced_per_sec": 1, "input_gamepad_count": 1, "input_gamepad_send_per_sec": 1, "input_mm_per_sec": 1, "input_msg_per_sec": 1, "input_trace": 1 }, "client_health": "smooth", "client_health_reason": "example", "device_key": "example", "codec_mime_type": "example" } ] }'
id
required
string format: uuid
Media typeapplication/json

POST /v1/sessions/{id}/stats body (metrics_handler.go). metrics is filtered to the BrowserMetrics dictionary at ingest.

object
client

Optional reporter discriminator (P9-07). Absent ⇒ browser. Any other value is rejected, not coerced.

string
Allowed values: browser native
samples
required
Array<object>
object
ts_unix_ms
required
integer
metrics
required

Metrics from the browser WebRTC client.

object
fps
number
bitrate_kbps
integer
rtt_ms
number
jitter_buffer_ms
number
decode_ms
number
packets_lost
integer
frames_dropped
integer
present_fps

Presentation fps derived from the MEAN RVFC interval. DEPRECATED as a headline: at source fps == display Hz a single missed vsync doubles one interval and drags the mean (a healthy 1440p120 session read 88-108 on 2026-08-22). Read present_fps_median. Retained unchanged for series continuity.

number
present_fps_median

Presentation fps from the MEDIAN RVFC interval — the fps a viewer perceives, unmoved by an occasional doubled frame. The number to read.

number
present_interval_median_ms

Median frame-to-frame presentation interval (ms).

number
present_interval_sd_ms
number
present_interval_p95_ms
number
present_interval_max_ms

Longest presentation interval in the window (ms) — the real stall length, and the source of client.freeze_detected’s gap_ms.

number
present_beat_fraction

Share (0..1) of the window’s intervals within +/-20% of exactly 2x the median. At source fps == display Hz this is the inherent vsync beat, not stutter; read it beside present_long_frames.

number
present_long_frames

Intervals longer than 2.5x the median — above the doubled band, so genuine stalls rather than the beat. Zero is what makes a beat benign.

integer
present_n

Intervals the window actually contained. Below 5 every other present_* key is omitted rather than computed from a fragment.

integer
playout_target_ms
number
rvfc_capture_time_available

RVFC captureTime has a current validated sample (0/1); stale/missing/invalid captureTime resets it. Not proof of abs-capture-time RTP negotiation.

integer
abs_capture_time_negotiated

Strict abs-capture-time RTP-extension negotiation evidence (0 until SDP/RTP wire proof exists).

integer
freeze_count

CUMULATIVE getStats freezeCount. Its advances drive the client.freeze_detected event; the freeze LENGTH is present_interval_max_ms, never 1000/freezeCount. Stored and taxonomised since P1; declared here 2026-08-23.

integer
display_refresh_hz

The client monitor’s refresh rate, measured from rAF vsync spacing with a MEDIAN (AS10-14). Not RVFC-derived. Stored and taxonomised since AS10-14; declared here 2026-08-23.

number
glass_to_glass_ms

DEPRECATED in favour of rvfc_capture_to_display_ms, which names what is actually measured. Unchanged in value and still posted: RTP capture-time to browser present, MEDIAN over a never-drained ring of up to 600 RVFC samples — minutes of history, not this second, and not true glass-to-glass (it excludes app render and client scan-out).

number
rvfc_capture_to_display_ms

RTP capture-time to browser present (ms), median over a rolling ring of up to 600 RVFC samples. Identical in value to glass_to_glass_ms, which it replaces; both are posted this release so no stored series breaks. Additive, approved by Michael 2026-08-23.

number
encode_ms

Accepted by the browser ingest allow-list and declared here, but no client has ever posted it — the browser cannot observe host encode time. Kept declared so the ingest contract does not change.

number
network_pacing_ms
number
decode_display_ms
number
is_hidden

AS10-11: tab visibility at sample time (0/1).

integer
input_backpressure

AS10-13 input-pipeline health: the DataChannel hit its backpressure threshold. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.

integer
input_channel_buffered_bytes

AS10-13 input-pipeline health: DataChannel bufferedAmount — the backpressure reading. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.

integer
input_coalesced_per_sec

AS10-13 input-pipeline health: pointer samples coalesced away per second (a subset of input_msg_per_sec). POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.

integer
input_gamepad_count

AS10-13 input-pipeline health: connected gamepads. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.

integer
input_gamepad_send_per_sec

AS10-13 input-pipeline health: gamepad state messages sent per second. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.

integer
input_mm_per_sec

AS10-13 input-pipeline health: relative mouse-motion messages sent per second. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.

integer
input_msg_per_sec

AS10-13 input-pipeline health: input DataChannel messages sent per second. POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.

integer
input_trace

AS10-13 input-pipeline health: the input tracer is on (seq + tc stamped). POSTED EVERY SECOND AND SILENTLY DROPPED — it is not in FilterBrowserMetrics’s allow-list, so it is visible live in the diagnostics panel and exists in no stored sample.

integer
client_health

AS10-11: browser-classified client health (sibling of the numeric metrics dict).

string
Allowed values: smooth decode_degrading presentation_degrading backgrounded_or_hidden client_unsupported
client_health_reason
string
device_key
string
codec_mime_type

UI-P6 (additive): the getStats() codec mimeType the receiver reports it is actually decoding, e.g. “video/H264”. A sibling STRING field, not a metrics dictionary key (the dictionary is numeric). Normalised server-side to the wire codec vocabulary and recorded on the session as negotiated_codec; an unparseable value is dropped and the POST still returns 202.

string

Accepted.

Missing/invalid/expired/revoked token.

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
]
}
}

Authenticated but insufficient role / not the owner (precedes resource lookup).

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
]
}
}

Too many requests.

Media typeapplication/json
object
error
required
object
code
required

E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.

string format: uuid
derived_tiles

Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.

Array<object>
object
id
required
string format: uuid
name
required
string
Examplegenerated
{
"error": {
"code": "example",
"message": "example",
"live_sessions": 1,
"session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"derived_tiles": [
{
"id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"name": "example"
}
]
}
}