---
title: "Streaming API v2: Build the playback token"
description: "Build playback tokens in the app with AES-GCM and HMAC, with test vectors, and keep them in sync with the panel clock."
---

## 7.2 Build the playback token

```
key     = base64url_decode(playback_key)                          // 16 bytes
payload = {"exp": server_now + 300, "sid": session_id, "str": "<type>/<id>/<ext>"}
iv      = 12 random bytes                                         // new for every token
ct, tag = AES-128-GCM-encrypt(key, iv, utf8(json(payload)))       // no AAD, 16-byte tag
p       = base64url(iv || tag || ct)                              // note: tag BEFORE ciphertext
token   = p + "." + base64url(HMAC-SHA256(key, ascii(p)))
```

- base64url without padding (`=`).
- `str`: `channel/<channel id>/ts` or `channel/<channel id>/m3u8`;
  `vod/<movie id>/<container_extension>`;
  `saga/<episode id>/<container_extension>`.
- WebCrypto's `encrypt` returns `ct || tag`: split the last 16 bytes off and
  reorder to `iv || tag || ct`.
- Content outside the line's package is refused by the node (`403`).

**Test vectors.** With these inputs your code must produce exactly these
tokens:

| | Vector 1 | Vector 2 |
|---|---|---|
| `playback_key` | `AAECAwQFBgcICQoLDA0ODw` | `AAECAwQFBgcICQoLDA0ODw` |
| key (hex) | `000102030405060708090a0b0c0d0e0f` | same |
| iv (hex) | `a0a1a2a3a4a5a6a7a8a9aaab` | `b0b1b2b3b4b5b6b7b8b9babb` |
| payload | `{"exp":1767225600,"sid":"wSN7wDGh2fH4o6Ai1LxUAg","str":"channel/123/ts"}` | `{"exp":1767225600,"sid":"wSN7wDGh2fH4o6Ai1LxUAg","str":"vod/4321/mp4"}` |
| tag (hex) | `df94b875544f120892a13ab9d051d75f` | `705e61aec7ca9d1ef50ac1c51db91198` |

Vector 1 token:

```
oKGio6Slpqeoqaqr35S4dVRPEgiSoTq50FHXX9GkXcMOqwk7vU6CMnQnhlBjwgF76nMflQdnItV1-svv8pEjeTuF2iwW7668-ppu8pB2Pz1Bo6zP4V7vpIsAaFLAA4D9HwWzvw.EbSRReGnF1PpE7iG426GxgG9jmEncKdcOj8bdAn0Wzk
```

Vector 2 token:

```
sLGys7S1tre4ubq7cF5hrsfKnR71CsHFHbkRmCXr1eW3tREOiRxsNOdPKdbpf3UEoIY13IOBVPeNZGaA880rY5YNtaMbP-JgJZVG5jyalBK4xDrNzXGF6i5D1oESAXdi_9Q.e_vR9ATz3Xp3Vdey0QZcCf19BNcExPx8dZf1j-noOEA
```

The JSON is serialized exactly as shown (no spaces, keys in that order).
Any valid JSON works for the node; the fixed form only makes the vectors
reproducible.

## 7.3 Clock

The node refuses a token whose `exp` has passed or is more than 600 s in the
future. **TV clocks are often wrong**, so never use the device clock directly:

```
offset     = server_time - device_now      // from login / refresh-session (or identity.timestamp)
server_now = device_now + offset
```

Update `offset` on every `login` and `refresh-session`.
