# rtcStats > AI-powered WebRTC session analysis. Upload a WebRTC dump (webrtc-internals or rtcstats file) or connect via API/MCP. rtcStats reads the dump and returns high level metrics, Observations, Deductions, an Experience Score, and a plain-English AI summary of what went wrong on the call. rtcStats is a SaaS for developers troubleshooting and monitoring WebRTC applications. The collection layer (rtcstats-js + rtcstats-server) is open source and self-hosted. The analysis layer runs on rtcstats.com. No vendor lock-in: customers choose what to send. The pitch: "AI reads the dump so you don't have to." ## Docs - [Getting started](https://rtcstats.com/kb/getting-started): sign up, upload first dump, read your first analysis - [API reference](https://rtcstats.com/api-docs): REST endpoints, auth, error codes, credit consumption rules - [API reference (Markdown)](https://rtcstats.com/api-docs.md): server-rendered Markdown version of the API reference for non-browser clients (no JavaScript required) - [OpenAPI spec](https://rtcstats.com/api/openapi): machine-readable OpenAPI 3.0 JSON (same content as the API reference, parseable by AI agents) - [MCP server](https://rtcstats.com/integrations/mcp): Model Context Protocol (Anthropic's agent standard, not a media control plane) server for WebRTC stats. Streamable HTTP endpoint, tools (get_quota, list_sessions, get_session), Bearer auth. Developer plan. - [Integration guide](https://rtcstats.com/kb/getting-started/integration-guide): connecting rtcstats-js and rtcstats-server to rtcstats.com - [rtcstats-js](https://github.com/rtcstats/rtcstats): client SDK for WebRTC apps - [rtcstats-server](https://github.com/rtcstats/rtcstats): self-hosted collector server ## Quickstart 1. Create an application token in the dashboard under Settings > Applications (Developer plan or above) and send it as `Authorization: Bearer ` on every call. 2. `GET https://api.rtcstats.com/v1.0/quota` to verify the token and read the remaining credits. 3. `POST https://api.rtcstats.com/v1.0/analyze` with the raw dump as the body (`Content-Type: application/octet-stream`, one credit); add `?save=true` to store the session. 4. `GET https://api.rtcstats.com/v1.0/sessions` to list stored sessions, then `GET /v1.0/sessions/{rtcstatsId}` for the full analysis of one. 5. Connect an MCP client to `https://api.rtcstats.com/v1.0/mcp` with the same Bearer token (tools: get_quota, list_sessions, get_session). Full contract: OpenAPI 3.0 JSON at https://rtcstats.com/api/openapi (Markdown rendering at https://rtcstats.com/api-docs.md). ## Guides - [How to debug a webrtc-internals dump with AI](https://rtcstats.com/blog/debug-webrtc-internals-dump-with-ai): upload a webrtc-internals dump and rtcStats returns Observations, an Experience Score, and a plain-English summary of what went wrong on the call, in under 30 seconds. Also covers why pasting a raw dump into a general chatbot fails, and querying analyzed sessions from Claude Code or Cursor over the MCP server (Developer plan). ## Analysis Output Schema The session payload returned by the MCP `get_session` tool (validated against live staging data, schema version 6.3): ```json { "rtcstatsId": "uuid (always present)", "rtcstatsUrl": "string (always present) — dashboard URL to open the rtcstats session", "embedUrl": "string (omitted on non-Enterprise; only present on Enterprise plan, where it is always computed — but access to the embedded viewer additionally requires the embedding feature to be active)", "processorVersion": "string (e.g. '1.9.0'), the @rtcstats/rtcstats-processor version resolved at response time", "data": { "uploadTimestamp": "ISO8601, when the dump was analyzed/processed", "title": "string", "callStart": "ISO8601", "callEnd": "ISO8601", "durationMs": "number", "experienceScore": "float 0.0-100.0 or null", "audioScore": "float or null", "videoScore": "float or null", "connectivityScore": "float or null", "connectivity": "string (e.g. 'DIRECT/UDP')", "connectionIPType": "string ('IPv4' | 'IPv6'), omitted when unresolved", "observationsCount": { "critical": "number", "high": "number", "medium": "number", "low": "number", "info": "number" }, "observations": [ { "type": "string (observation type key, e.g. 'turnNoConfig', 'slowDtlsEstablishment'; full catalog at GET /v1.0/observations)", "severity": "info | low | medium | high | critical", "category": "string (e.g. 'connections', 'streams', 'pairs')", "tags": ["string (e.g. 'network', 'connectivity', 'audio')"], "source": { "pid": "string (peer connection id)", "sid": "string (stream/datachannel report id)", "cpid": "string (candidate-pair id)", "did": "string (device entry id)", "rid": "string (device row index)", "timestamp": "string (device entry time)", "ssrcId": "number (stream SSRC)", "labelId": "string (device name)" }, "label": "string (DEPRECATED, use source.ssrcId / source.sid / source.labelId)", "firstSeenAt": "integer epoch ms, omitted when no source timestamp is known" } ], "deductions": [ { "issue": "string (short problem id, e.g. 'cannot-hear')", "deductions": [ { "family": "string ('audio' | 'video' | 'connectivity' | 'network' | 'datachannel')", "issue": "string (short problem id, e.g. 'cannot-hear')", "cause": "string (cause slug; one or more candidate causes per issue, confidence descending)", "origin": ["string (one or more probable origins, e.g. 'peripheral', 'network', 'bug')"], "investigations": ["string"], "bind": { "tags": ["string (observation tags scoping the media context)"] }, "confidence": "number 0.0-1.0 (author-assigned, informational)", "from": [{ "pid": "string", "sid": "string", "ssrcId": "number" }] } ] } ], "aiSummary": { "model": "string (e.g. 'anthropic/claude-haiku-4.5')", "summary": { "highlight": "markdown string (one-line overall summary)", "network": "markdown string", "networkScore": "string ('excellent' | 'good' | 'acceptable' | 'poor' | 'bad' | 'mediocre')", "media": "markdown string", "mediaScore": "string (same values as networkScore)", "user": "markdown string", "userScore": "string (same values as networkScore)" }, "version": "string (AI prompt configuration version)" }, "pConnectionsNumber": "number", "pConnections": "object keyed by connection id — detailed per-connection WebRTC stats", "aggregatedStats": "object — session-wide aggregated metrics with self-describing keys ___: metric = jitter|rtt|bitrate|packetLoss|mos; scope = in_audio|out_audio|in_video|out_video; agg = avg|min|max|p5|p95|stddev; unit = ms (jitter, rtt), kbps (bitrate), percent (packetLoss), score (mos, 0-4.5, where 0 means unusable transport). Example: rtt_out_audio_avg_ms. Session-level weighted averages: jitter_avg_ms, rtt_avg_ms. Stream counts: in_audio_count, out_audio_count, in_video_count, out_video_count. Since schema 6.9 the former unitless keys (e.g. rtt_out_audio, in_audio) are no longer present", "userAgentData": { "platform": "string", "mobile": "boolean", "brands": [...] }, "... additional raw WebRTC stats fields": "present in the full payload" } } ``` `rtcstatsId` and `rtcstatsUrl` are always present in the response. `embedUrl` is the embeddable viewer URL: it is only present on Enterprise plans (omitted from the response otherwise). On Enterprise it is always computed and returned, but access to the embedded view is authorized only when the embedding feature is active for the account. `aiSummary` is `null` until background generation completes, and always `null` on `POST /analyze` and on plans without the AI feature. Only the fields documented above are stable; other fields may appear and are subject to change without notice. Use them with care. `deductions` is a list of groups, one per issue, each holding one or more root causes (confidence descending, 8 fields per deduction, `from` same shape as an observation's `source`); `[]` on plans without the Deductions feature (Free, Startup), enforced at response time regardless of what was computed. Number formatting: timestamps are integers at millisecond scale; every other number keeps at most 2 decimals, except `audioLevel` values, which keep more precision. `POST /v1.0/analyze` does not store the session by default: pass `?save=true` to persist it (the response then includes `rtcstatsId` and `rtcstatsUrl`). `POST /v1.0/analyze` also speaks the same chunked protocol as `POST /v1.0/upload`, for dumps above the ~4.5MB request-body cap: send `multipart/form-data` chunks (`chunk`, `fileId`, `chunkIndex`; each returns `{"success": true}`, consumes no credit, and must stay under the 10MB request body limit or it returns `413` `file_too_large`), then a JSON assemble request `{"assemble": true, "fileId": "...", "fileName": "optional"}` (at most 1KB). The assembled dump runs through the normal analyze pipeline (one credit). With `?save=true` on the assemble request, `fileName` names the stored session and is mirrored in the response `data.fileName` / `data.title` when provided. Assemble detection is shape-based (small JSON body matching that exact shape), so raw JSON dump bodies keep working unchanged. `POST /v1.0/enrich` is for a rtcstats-server or any server that only needs the observations and scores projection: same Bearer auth and one-credit cost as `analyze`, but it returns only `{ data: { scores, observationsCount, observations, userAgentData }, processorVersion, generatedAt }` and never stores the session. `scores` are the five analyzer scores (`experienceScore`, `audioScore`, `videoScore`, `connectivityScore`, `observationsScore`; each a number or null). `observations` are flat, SQL-friendly trimmed records: only `type` and `severity` are always present, every other field (`category`, `tags`, `firstSeenAt`, `url`, `property`, `valueInCount`, `valueInPercent`, `averageInMs`, `maxInMs`, `durationInMs`, `durationInSeconds`, `durationInPercent`) is omitted when absent, never null. `observationsCount` (critical/high/medium/low/info) sums to `observations.length`. `processorVersion` is the `@rtcstats/rtcstats-processor` version and `generatedAt` is epoch ms. The payload is full and unconditional (not plan-gated). `enrich` uses the same chunked upload protocol as `upload` and `analyze` (multipart chunks then a JSON assemble request `{"assemble": true, "fileId": "..."}`; a raw dump body also works for small files); MOS follows the account token settings. Error codes, the complete `/v1.0/*` vocabulary: `authentication` (401), `forbidden` (403), `invalid_request` (400), `no_credits` (402), `file_too_large` (413), `parsing_issue` (415), `not_found` (404), `reprocess_failed` (422), `other_issue` (415 or 500), `rate_limited` (429); every error body is `{ error, errorCode }`, branch on `errorCode`, never on `error`. Rate limits, per account over 60 s windows: 30 ingest requests per minute (`upload` assemble, `analyze` and `enrich` raw or assemble), 120 read requests (`sessions` list/get/delete, `quota`, `observations`, MCP tool calls) and 600 multipart chunks; over the limit REST returns `429` with `errorCode` `rate_limited`, `Retry-After` (seconds) and `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset` (the three headers are on every response emitted after the check, never on `401`/`403`, and absent when the limiter fails open), MCP tool calls return an `isError` result, `initialize` and `tools/list` are never limited. ## MCP Connect an MCP client to `https://api.rtcstats.com/v1.0/mcp` (POST, stateless JSON-RPC). Include `Authorization: Bearer ` on requests that invoke tools. Requires Developer plan or above. Get a token at Settings > Applications. Tools: - `get_quota` — returns `{allowedCredits, remainingCredits}`. `remainingCredits` is the combined monthly + purchased top-up pool. - `list_sessions` — returns `{total, data: [{rtcstatsId, createdAt, sessionStart, sessionEnd, title, rtcstatsUrl, embedUrl, abstract}]}`. `rtcstatsId` and `rtcstatsUrl` are always present. `sessionStart` and `sessionEnd` can be null. `embedUrl` is omitted on non-Enterprise plans. `abstract` is the denormalized session summary used to drive the filters (browser/browserVersion/os/osVersion, userId/conferenceId/sessionId, experienceScore, scoreBand `"low"|"medium"|"high"|"unrated"`, connectivity, observationTypes/observationTags, observationsCritical/High/Medium counts). `observationTypes` matches the `observationTypes` filter. Every key is always present when `abstract` is non-null; `abstract` itself is `null` for rows not yet indexed. Optional filters: `name` (array, case-insensitive substring on the session title, ANY of, up to 20), `observationTypes` (array, exact observation type names, ANY of, up to 20; only critical/high/medium observations are indexed; full catalog at GET /v1.0/observations), `observationTags` (array, ANY of: connectivity/security/audio/video/datachannel/outbound/inbound/peripheral/behavior/network/configuration/cpu/bug; tags of critical/high/medium observations only), `os`, `browser`, `browserVersion` (major version only, e.g. "142"), `userId`, `conferenceId`, `sessionId` (exact match), `hasCritical`/`hasHigh`/`hasMedium` (boolean, sessions with that observation severity), `hasLowScore` (experience score < 60) / `hasMediumScore` (60-79) / `hasHighScore` (>= 80). Different filters are AND-combined; multiple values within one filter are OR-combined. Score bands match the dashboard score colors. The REST endpoint `GET /v1.0/sessions` accepts the same filters as query params (array filters repeated or comma-separated; booleans as `?hasCritical=true`). - `get_session` — input `{rtcstatsId: UUID}`, returns `{rtcstatsId, rtcstatsUrl, embedUrl, data}`. `rtcstatsId` and `rtcstatsUrl` are always present. `embedUrl` is the embeddable viewer URL — only present on Enterprise plans (omitted otherwise). `data` contains the full analysis: summary, observations, deductions, experience score, AI summary. MCP tools are read-only and do not consume analyze credits. The REST endpoint `GET /v1.0/observations` (Bearer, same plan gating as quota, no credits) returns the catalog of observation types the analyzer can emit: `{total, data: [{type, title, severity: [...], tags: [...]}]}` where `type` matches `data.observations[].type` and the `observationTypes` filter, and `severity`/`tags` are the full arrays of values each type can carry. **Token setup:** Settings is in the left-hand sidebar. Go to the Applications tab and create a new application. The token is shown once at creation and cannot be read back — copy it immediately. Store it as your `Authorization: Bearer` value. If lost, create a new application. ## Pricing One credit = one file analyzed. Monthly credits do not roll over; purchased credits do. | Plan | Price | Credits/mo | Team | Retention | Full analysis | |------------|-----------|------------|------|-----------|----------------| | Free | $0 | 10 | 3 | 1 month | Month 1 only | | Developer | $239/mo | 500 | 10 | 3 months | Yes | | Enterprise | $1,199/mo | 5,000 | 50 | 3 months | Yes + embeds | Annual pricing: Developer $199/mo, Enterprise $999/mo. Top-up credits (carry over): $100/5K, $500/50K, $2,500/500K. API access requires Developer plan or above. MCP requires Developer plan or above. ## OSS + SaaS Architecture Open source (self-hosted by the customer): - rtcstats-js: `npm install @rtcstats/rtcstats-js` — client SDK for WebRTC apps - rtcstats-server: github.com/rtcstats/rtcstats — collector server SaaS (rtcstats.com): - Receives sessions from rtcstats-server, manual file upload, or direct API - Runs analysis pipeline: Observations → Deductions → Experience Score → AI Summary - Customer controls sampling: what gets sent, when, for which users