---
name: rtcstats
description: Use when a user needs to debug a WebRTC call from a webrtc-internals or rtcstats dump, find bad calls among their stored rtcStats sessions, or explain an rtcStats observation. Covers the hosted MCP server and the REST API at api.rtcstats.com.
metadata:
  publisher: rtcStats
  source: https://rtcstats.com/.well-known/agent-skills/rtcstats/SKILL.md
  api: https://rtcstats.com/api/openapi
  docs: https://rtcstats.com/api-docs
---

# rtcStats

rtcStats analyzes WebRTC sessions. A dump (a `webrtc-internals` export from Chrome, or an rtcstats event stream) goes in; an Experience Score, per-dimension scores, Observations (the problems found) and, on stored sessions, an AI summary come out.

Two surfaces, one token:

- **MCP** at `https://api.rtcstats.com/v1.0/mcp`: Streamable HTTP, stateless, JSON responses (no SSE). Four read-only tools. Use it to find and read sessions.
- **REST** at `https://api.rtcstats.com/v1.0`: everything MCP does, plus submitting dumps (`upload`, `analyze`, `enrich`) and `deleteSession`. Contract: `https://rtcstats.com/api/openapi`.

## Auth

- Send `Authorization: Bearer <application token>` on **every** request, MCP `initialize` included. Missing or bad token: HTTP 401 with `WWW-Authenticate: Bearer realm="rtcstats"`. A plan without API access: HTTP 403 `forbidden`.
- Tokens come from **Settings > Applications** in the dashboard. API access needs the Developer plan or above.
- A token is **read-only** or **full**. Read-only can call every read and every MCP tool, and is the scope to put in an MCP client config or give an autonomous agent. Writes (`upload`, `analyze`, `enrich`, `deleteSession`) need full scope; a read-only token gets 403 `insufficient_scope` before anything is spent. Scope cannot be changed on an existing token: revoke and issue a new one.
- The token is bound to one account. There is no account header and no way to act for another account.

## MCP tools

All four are reads and spend no credits.

1. `get_quota`: no input. Returns `accountName`, `accountPlan`, `allowedCredits`, `remainingCredits`, `maxFileSizeBytes` (null when unlimited) and `maxChunkSizeBytes`.
2. `list_sessions`: optional filters `name`, `observationTypes`, `observationTags`, `os`, `browser`, `browserVersion`, `userId`, `conferenceId`, `sessionId`, `hasHigh`, `hasMedium`, `hasLowScore`, `hasMediumScore`, `hasHighScore`, plus `limit` (1 to 1000, default 100) and `page` (from 1). Different filters AND together; values inside one filter OR together. Newest first. Each row carries `rtcstatsId`, `title`, timestamps, `rtcstatsUrl` and `abstract` (browser/OS, observation types and tags, severity counts, score band).
3. `get_session`: `{ "rtcstatsId": "<uuid>" }`. Returns `rtcstatsUrl`, `processorVersion` and `data`, the full analysis.
4. `get_observation_explanation`: `{ "type": "<observation type>" }`. Returns what the observation means and what to check, as Markdown sections.

## Workflows

**Triage bad calls.** `list_sessions` with `hasHigh: true` or `hasLowScore: true`, narrowed by `browser`, `os`, `conferenceId` or `userId` when the user names one. Read `abstract` on each row to pick the worst, then `get_session` on those only. Page with `page` while `hasMore` is true.

**Explain one call.** `get_session`, then read in this order: `data.experienceScore` (0 to 100) and `audioScore` / `videoScore` / `connectivityScore` (1 to 5); `data.observations`, worst `severity` first (`high`, `medium`, `low`, `info`); `data.aiSummary` if present. Severity is impact, not cause: before you call the worst observation the cause, check whether it is a consequence of another one. For any observation `type` you cannot explain from its fields, call `get_observation_explanation` with that exact type. Tie an observation to its connection or stream with `source.pid` (peer connection) and `source.sid` (stream), which key `data.pConnections` and `data.streams` (`${pid}-${sid}`).

**Analyze a new dump (REST, full-scope token).**
1. `GET /v1.0/quota`. Stop if `remainingCredits` is 0 or the file is larger than `maxFileSizeBytes`.
2. Send the file in `multipart/form-data` chunks to `POST /v1.0/analyze`: fields `chunk` (1 MB recommended, 3 MB maximum), `fileId` (one id for the whole file), `chunkIndex` (from 0). Each answers `{ "success": true }` and spends nothing.
3. Send `{ "fileId": "<same id>", "assemble": true }` as `application/json` to `POST /v1.0/analyze`. This runs the analysis and spends one credit. Add `?save=true` to store the session and get an `rtcstatsId` back (no extra credit). A small dump can instead be sent whole as the raw request body.
- `upload` stores without returning the analysis; `enrich` returns only scores and a flat observation list and never stores. Same chunk protocol, one credit each.

## Rules

- **Never call `deleteSession` without the user confirming that exact session.** It permanently removes the dump and its analysis. There is no undo and no dry run.
- **Only a stored assemble is retry-safe.** Repeating the chunked assemble (`{ "fileId": "<same id>", "assemble": true }`) of the same `fileId` within 24 hours on `upload` or `analyze?save=true` returns the original response with no second session or credit. `analyze` without `save`, `enrich` and a whole-body request have no replay and accept no Idempotency-Key, so after a lost response check `remainingCredits` before resending.
- **Ask before a batch.** Several dumps spend several credits; confirm the count against `remainingCredits` with the user first.
- **Observation types are an open set.** There is no list endpoint (`GET /v1.0/observations` answers 410 Gone). Build `observationTypes` filters from the types your own sessions return (`abstract.observationTypes`, `data.observations[].type`). Ignore a type you do not recognise. Tags, severities and score names are closed sets, listed in `https://rtcstats.com/schemas/vocabulary.json`.
- **Only high and medium observations are indexed** for the `observationTypes` and `observationTags` filters, so they will not find a session by a low or info observation.
- **`aiSummary: null` is not "no problems".** It is null while generation is pending, on plans without the AI feature, and always on `analyze` responses. Read the observations instead, or call `get_session` again later.
- **`abstract` can be null** on rows not indexed yet; check before reading `abstract.scoreBand`.
- `embedUrl` is present only on the Enterprise plan.

## Errors and limits

- REST errors are JSON `{ "error": "...", "errorCode": "..." }`. The ones to handle: 402 `no_credits`, 413 `file_too_large` (stop uploading that `fileId`), 415 `parsing_issue` or `other_issue` (not a WebRTC dump, or an export from before Chromium 138; no credit spent), 429 `rate_limited` (wait `Retry-After` seconds).
- MCP tool failures, rate limits included, come back as HTTP 200 with `isError: true` in the tool result. Check `isError`, not the status code.
- Rate limits per account per minute: 120 reads (MCP tool calls count here), 30 dump submissions, 600 chunks.
