Player SDK
In-app contract reference — public distribution unverified

Read the contract
before integration.

The checked-in @streamwake/player-sdk source describes how a future public package would attach to a <video> element, batches the eight event types defined in the zod contract, and POSTs them to /api/v1/telemetry with a bearer API key. The supported surface today is this in-app wire-format reference; public npm and CDN distribution are not currently verified.

PLANNED

Claim status labels: see definitions.

1 · Contract reference

Trace the wire format.

The checked-in source is organized around @streamwake/player-sdk. Its intended browser integration uses hls.js — hls.js is an optional peer dependency, but the init example wires it for the adaptive-bitrate signal you want to capture.

The local source and parity tests are present in this checkout. The types in packages/player-sdk/src/types.ts are the same TelemetryEvent discriminated union the server validates against.

Local source
Contract examples

The implementation and event types live in packages/player-sdk/src . The app-side mirror lives in src/lib/api-spec.ts. Use these local files to evaluate the contract; no public download artifact is implied.

Source
Planned / unverified

Public npm, CDN, and repository distribution were not verified for this deployment. This page documents the supported in-app contract only; use the local source for controlled evaluation until a public artifact is confirmed.

2 · Initialize

Wire a TelemetryHandle.

Pass a TelemetryConfig to createTelemetry() with your bearer API key, the ingest endpoint, and per-session metadata. The factory returns a read-only TelemetryHandle that you attach to the <video> element.

The handle exposes four members — sessionId, emit, attach, end. There is no on() hook — the player fires events internally off the <video> element and (optionally) the hls.js instance you pass via AttachOptions.

Init example
import { createTelemetry } from "@streamwake/player-sdk";
import Hls from "hls.js";

const videoEl = document.querySelector("video");
if (!videoEl) throw new Error("No <video> element");

const handle = createTelemetry({
  apiKey: "<your-api-key>",
  ingestUrl: "https://streamwake.io/api/v1/telemetry",
  session: {
    contentId: "ckq3xepisodeabc123",
    tags: { player: "web", env: "prod" },
  },
  options: { flushIntervalMs: 10_000, maxBatchSize: 50, debug: false },
});

const hls = new Hls();
hls.loadSource("https://example.com/manifest.m3u8");
hls.attachMedia(videoEl);

const detach = handle.attach(videoEl, { hls });
videoEl.addEventListener("ended", () => handle.end("ended"));
window.addEventListener("beforeunload", () => handle.end("unloaded"));
What's in the handle
  • sessionId — read-only id baked into every batch. Stable across the lifetime of one createTelemetry() call.
  • emit(event) — push a custom event into the buffer (e.g. for ads or chapter switches the player doesn't fire natively).
  • attach(videoEl, opts?) — subscribe to the media element + optional hls instance. Returns a detach() cleanup.
  • end(reason?) — flush the buffer and emit a session_end event. Pass 'ended', 'unloaded', or 'error'.
3 · Wire format

Eight events, one discriminated union.

The player emits exactly the events below — one row per EventType value in the closed enum shared by src/lib/contracts/telemetry.ts. The server validates the union on POST; a malformed payload is rejected before any row is persisted.

EventWhenPayload
playback_startPlayer started playback of a media element.
{
  "positionMs": 0,
  "durationMs": 1820000,
  "contentId": "ckq3xepisodeabc123"
}
playback_pauseUser paused playback.
{
  "positionMs": 14280,
  "bufferedMs": 30000
}
playback_resumeUser resumed playback after a pause.
{
  "positionMs": 14280,
  "bufferedMs": 30000
}
rebuffer_startPlayback stalled because the buffer drained below the underrun threshold.
{
  "positionMs": 412910,
  "bufferedMs": 0
}
rebuffer_endPlayback resumed after a rebuffer.
{
  "positionMs": 413280,
  "bufferedMs": 12500
}
quality_changeAdaptive bitrate (ABR) switched rendition — the QoE signal for stalls avoided / taken.
{
  "from": "480p",
  "to": "720p",
  "bitrate": 2400000
}
errorPlayer emitted a media error (decode, network, or src).
{
  "code": "MEDIA_ERR_NETWORK",
  "message": "manifest fetch failed",
  "fatal": false
}
session_endSession closed — natural end, unload, or fatal error.
{
  "reason": "ended",
  "durationMs": 903000
}
4 · The batch envelope

What hits the wire.

Events are batched (defaults: every 10 s or 50 events) and POSTed to /api/v1/telemetry with the bearer API key in the Authorization header. The apiKey inside the body must match the bearer — a mismatch is a 401, so one client can't post events into another client's session.

On success the endpoint returns 204 No Content with an empty body — fire-and-forget. Retries are safe because the persisted key is (sessionId, ts, type), so a duplicated batch is a no-op.

Auth note. The SDK uses a bearer API key because it runs on third-party origins where the dashboard'sbetter-auth.session_token cookie can't reach. The dashboard routes (/api/v1/streams, /api/v1/keys) use the cookie because they live on your domain.
POST /api/v1/telemetry
curl -X POST https://streamwake.io/api/v1/telemetry \
  -H "content-type: application/json" \
  -H "Authorization: Bearer <your-api-key>" \
  -d '{
    "apiKey": "<your-api-key>",
    "sessionId": "ckq3xsessh1",
    "events": [
      {
        "type": "playback_start",
        "ts": "2026-08-04T18:24:11.000Z",
        "payload": { "positionMs": 0, "durationMs": 1820000 }
      }
    ]
  }'
204 Response
HTTP/2 204 No Content
Sample batch — three events, one session
{
  "apiKey": "<your-api-key>",
  "sessionId": "ckq3xsessh1",
  "events": [
    {
      "type": "playback_start",
      "ts": "2026-08-04T18:24:11.000Z",
      "payload": {
        "positionMs": 0,
        "durationMs": 1820000
      }
    },
    {
      "type": "quality_change",
      "ts": "2026-08-04T18:24:18.000Z",
      "payload": {
        "from": "auto",
        "to": "720p",
        "bitrate": 2400000
      }
    },
    {
      "type": "session_end",
      "ts": "2026-08-04T18:39:14.000Z",
      "payload": {
        "reason": "ended",
        "durationMs": 903000
      }
    }
  ]
}
5 · Get an API key

Mint once, store it forever.

API keys are minted through the same cookie-authed session you already use on the dashboard. POST a label to /api/v1/keys — the response contains the raw swk_… key exactly once. The server stores a SHA-256 hash; subsequent reads only expose the metadata.

For the full key surface — listing, soft-revoking via revokedAt, and the rest of the keys lifecycle — see the API reference.

Raw key returned ONCE — store it now
POST /api/v1/keys
curl -X POST https://streamwake.io/api/v1/keys \
  -H "content-type: application/json" \
  -b "better-auth.session_token=<your-session-cookie>" \
  -d '{ "label": "Production web player" }'
201 Response — rawKey returned once
{
  "id": "ckq3xkeyabc123",
  "label": "Production web player",
  "rawKey": "swk_<your-raw-key>",
  "createdAt": "2026-08-04T18:21:02.000Z"
}
6 · Errors

The three response codes you'll see.

401

Invalid API key

Authorization header missing, malformed, or the apiKey in the body doesn't match the bearer. Body is the verbatim { "error": "Invalid API key" }.

400

Validation

Zod rejected the batch envelope or one of the events. Body is { "errors": { "apiKey": "...", "sessionId": "...", "events": "..." } } — keys mirror the TelemetryBatchCreate contract.

500

Internal Server Error

Unexpected server failure. Body is { "error": "Internal Server Error" }. Safe to retry — events are idempotent on (sessionId, ts, type).

StatusBodyWhen
401
{
  "error": "Invalid API key"
}
Authorization header missing, malformed, or the apiKey does not match any active record. The same body is returned when the apiKey in the body and the bearer in the header disagree.
400
{
  "errors": {
    "events": "Array must contain at least 1 element(s)",
    "apiKey": "String must contain at least 1 character(s)",
    "sessionId": "String must contain at least 1 character(s)"
  }
}
Zod validation failed on the batch envelope or on an individual event. Keys mirror the TelemetryBatchCreate contract: apiKey, sessionId, events.
500
{
  "error": "Internal Server Error"
}
Unexpected server failure on the ingest path. Safe to retry the batch; events are idempotent by (sessionId, ts, type).
Ready to wire it up?

From the player,
to the agent feed.

The SDK only POSTs to /api/v1/telemetry. Streams themselves are registered from the dashboard via the cookie-authed POST /api/v1/streams — mint a key here, drop it into your player, and the agent feed on /app/agents starts ingesting the moment the first batch lands.