Appearance
Playback and access sessions
A viewer needs two things to watch a protected stream: an entitlement (the right to watch) and a session (one viewer on one device, watching now). You create the entitlement after a payment. The session is opened every time someone presses play.
Open a session
Open a session for a live stream or an asset with POST /v1/streams/playback_tokens. It does the same as POST /v1/access/sessions and adds the playback link.
From your server, name the customer:
http
POST /v1/streams/playback_tokens
Authorization: Bearer vk_live_...
{ "resource": "ls_...", "customer": "user_42", "device_id": "living-room-tv" }From a browser, send a publishable key and the customer token, and leave customer out. The customer is the token's sub.
http
POST /v1/streams/playback_tokens
Vabu-Publishable-Key: vpk_live_...
Authorization: Bearer <customer token>
{ "resource": "ls_...", "device_id": "browser-1" }Vabu checks the entitlement and its limits, applies the device rules and returns the session:
json
{
"data": {
"id": "as_01J9ZQ7P2H5V8X1A4D6F9G3JKM",
"object": "access_session",
"customer": "user_42",
"device_id": "browser-1",
"state": "active",
"expires_at": "2026-10-11T08:01:30Z",
"heartbeat_interval": 30,
"grant": {
"sources": [{ "provider": "...", "url": "https://.../....m3u8?token=..." }],
"failover": { "max_errors": 3, "window_seconds": 20 },
"heartbeat_interval": 30,
"token_expires_at": "2026-10-11T08:11:30Z"
}
}
}device_id is a string your app makes up and keeps for each device, for example one saved in local storage. The same device_id opening again replaces its own earlier session.
The grant
grant.sources is an ordered list of playback links. Play the first. If the player reports failover.max_errors fatal errors within failover.window_seconds, switch to the next. There is one source today, so the list has one item. Code the switch anyway, so more sources work without a change.
A public stream has a plain link with no token, and token_expires_at is null.
Keep the session alive
Send a heartbeat every heartbeat_interval seconds (30):
http
POST /v1/access/sessions/as_.../heartbeatA session that misses two heartbeats ends, about 90 seconds after the last one. A heartbeat returns the session with a fresh grant.
The link in your player expires. Its token lasts for the session's length plus 10 minutes, so about 11 minutes. Each heartbeat returns a new link with a new token. Swap the player to the newest link every 5 minutes or so. With hls.js that is hls.loadSource(newUrl). The quickstart shows it. A viewer who is watching a live stream rejoins at the live edge when you swap.
End a session when the viewer leaves:
http
DELETE /v1/access/sessions/as_...Device and session limits
The entitlement's limits decide what a second device does. See paid access.
| Limit | Effect |
|---|---|
max_devices | The number of different device_id values that may be used. A new device beyond it gets 403 device_limit_reached. |
max_concurrent_sessions with concurrency_policy: "reject_new" | The new session is refused with 409 concurrency_limit_reached. |
max_concurrent_sessions with concurrency_policy: "replace_oldest" | The new session opens. The oldest one ends, and its next heartbeat gets 403 session_ended. The latest device wins. |
max_views | The number of sessions the customer may open in total. Beyond it, 403 view_limit_reached. |
allowed_countries | Viewers must be in these countries. |
allowed_domains | The page must be on one of these hosts. example.com does not cover www.example.com. |
A browser cannot state its own country or domain. With a publishable key, the domain is the host of the request's Origin header and the country is the ctry claim your server put in the customer token. An entitlement that limits countries or domains refuses a browser request that carries neither.
When a session ends
A heartbeat on an ended session returns 403 session_ended, and error.details.reason says why.
| Reason | Meaning |
|---|---|
replaced_by_newer_session | Another device took over under replace_oldest. |
missed_heartbeats | The page stopped sending heartbeats, for example the tab was closed or went offline. |
entitlement_inactive | You revoked or suspended access, or it expired. |
client | The viewer or your app ended it. |
Show a clear message for each. "You are watching on another device" is much kinder than a frozen picture.
Errors when opening a session
| Code | HTTP | Meaning |
|---|---|---|
access_denied | 403 | No active entitlement. error.details.reason says why. |
device_limit_reached | 403 | max_devices is used up. |
view_limit_reached | 403 | max_views is used up. |
concurrency_limit_reached | 409 | Another session is open and the policy is reject_new. |
stream_disabled | 409 | The live stream is switched off. |
asset_not_ready | 409 | The asset is still processing. |
no_playback_id | 409 | The stream or asset has no way to be played yet. |
invalid_customer_token | 401 | The browser's token was rejected. See customer tokens. |
product_not_enabled | 403 | Streams is not switched on for your account. |
A refused open does not use a view or a device slot. All codes are on the error codes page.