# Security

> **TL;DR** — Your API keys live on your servers only. The viewer's device is untrusted: it only ever holds a ticket for one episode on that device, for 30 minutes. Revoke a key the moment you think it leaked, then tell Wandura. Never put keys, tickets or signed links in apps, front-end code, logs, analytics, crash reports or URLs. `viewer_ref` is an opaque, hashed id, kept by Wandura for 90 days.

## Trust boundary
```mermaid
flowchart LR
  D["Viewer device<br/>untrusted<br/>Player SDK, one ticket"] -->|"your session + device values"| B["Your backend<br/>trusted<br/>API keys, paywall, per-viewer limits"]
  B -->|"Bearer API key, TLS 1.2+"| W["Wandura Delivery API<br/>checks key, licence<br/>signs the ticket"]
  W -->|"ticket for one episode, one device"| B
  B -->|"ticket"| D
  D -->|"ticket"| S["Wandura streaming<br/>encrypted stream + keys"]
```

| Zone | Holds | Must never hold |
|---|---|---|
| Viewer device | One ticket per episode, in memory; the Player SDK licence file | API keys |
| Your backend | API keys, your paywall, your viewers | — |
| Wandura | Your licences, the films, the ticket ledger | Your viewers' identities |

**Your ticket endpoint is the gate.** Wandura checks your licence, not your viewers. Device values come from a client you do not control. So your endpoint must:

- require a signed-in viewer session (or your own device attestation) before it asks Wandura for a ticket;
- apply your paywall before every ticket request;
- limit tickets per viewer and per device (for example 10 per minute) and alert on viewers who open many films in a short time;
- treat `union_info` and `drm_kek` as untrusted input: pass them through, never use them for anything else.

Wandura's rate limit is per **organisation**: one abusive client of your app can use up the allowance of all your viewers. See [Rate limits](https://docs.wandura.ai/developers/concepts/rate-limits).

## Keys
### Scopes and least privilege
One key per system and per environment, with only the scopes that system needs. The scope of each endpoint is on [Authentication](https://docs.wandura.ai/developers/authentication#scopes). A catalog sync job never needs `playback`; a marketing job needs only `creatives:export`.

### Production and testing
There is no separate staging host: every key calls `https://www.wandura.ai`. What keeps test traffic apart is the **organisation**.

Each partner gets a **test organisation** next to its real one. Its keys call the same production URL exactly like real keys, against sample films only, and its plays are never counted as views, usage or revenue. It also accepts the `Wandura-Simulate` header to reproduce errors on demand; production keys ignore that header completely. See [Testing](https://docs.wandura.ai/developers/guides/testing).

Never use a production key in a test environment, or the other way round.

### Storage
- Keep keys in a secrets manager, exposed to the one service that needs them as `WANDURA_API_KEY`.
- Never commit a key. Add the pattern `wnd_live_[A-Za-z0-9_-]{32}` to your secret scanner and to pre-commit hooks.
- Search every app build for `wnd_live_` before release: there must be no match.

### Key lifecycle
| Action | Who | What happens |
|---|---|---|
| Create | Admin or Developer, on **Developers** | The full key is shown once. A new key is pinned to the **latest** dated API version. |
| Rotate | Admin or Developer | Replaces the secret, keeps label, scopes and **version pin**. The old secret is refused within about **1 second** on every Wandura server (always within 30 seconds) — there is no overlap. |
| Revoke | Admin or Developer | Final. New requests with the key are refused within about **1 second** (always within 30 seconds). |
| Already-issued tickets | — | Keep playing until they expire (≤ 30 minutes). They cannot be recalled. |

**Rotate without downtime:** create a second key with the same scopes, deploy it, check traffic has moved (**Last used** on the old key stops changing), then revoke the old one. A new key is pinned to the latest dated version: if your key is pinned to an older one, send `Wandura-Version: <your date>` with the new key, or read `api.pinned_version` from `GET /config` with both keys before you switch. See [Migrating versions](https://docs.wandura.ai/developers/guides/migrating-versions).

Wandura monitors new and rotated playback keys and organisations that reach their playback limit. You still own your keys: watch **Last used** and **API usage**, and tell Wandura at once if you suspect a leak.

Every key creation, rotation and revocation is recorded in Wandura's audit log with your organisation, the member who did it and the key prefix.

## If a key leaks
1. **Revoke it** on **Developers** (any Admin or Developer). New requests stop within about a second.
2. **Deploy a replacement** with the same scopes. Mind the version pin (above).
3. **Tell Wandura** in your team's Wandura Discord channel: the key prefix, when and where it was exposed. Wandura can list the tickets issued with it (kept 90 days) and, if tickets already issued must stop before they expire, suspend the affected films for your organisation.
4. **Check** **Developers → API usage** for calls you did not make.
5. **Close the leak path**: repository, app binary, logs, crash reports, CI output. Add the secret-scanner pattern.

Calling `http://` instead of `https://` sends the key unencrypted before the redirect: treat that key as leaked.

## Tickets and signed links
| Value | Rule |
|---|---|
| `play_auth_token` | A bearer secret for one episode on one device, valid ≤ 30 minutes. Keep it in memory for the current play. Never write it to disk, logs, analytics or crash reports. |
| `manifest_url` | A bearer link: anyone holding it can download the stream until `expires_at` (≤ 15 minutes). Same rules as the token. |
| Open video links | Anyone holding one can play or download the file until it expires (1 hour). Do not publish them, do not store them in the app. |
| Subtitle URLs | Public and long-lived. Fine to cache; do not publish lists of them. |

A leaked token or link expires on its own; stop the path that leaked it.

## Viewer and device data
- **`viewer_ref`** is optional, opaque, at most 256 characters. Wandura stores it with each ticket for **90 days**, for support lookups, then keeps only monthly counts. Never put an email address, phone number or name in it. Use a keyed hash: `HMAC(secret, user_id)` plus a hash of your install id, so it means nothing outside your systems and stays stable per viewer and device. The [examples](https://docs.wandura.ai/developers/guides/playback-flow#code) do exactly this.
- **`union_info`** is a persistent device identifier generated by the Player SDK: personal data on your side. Wandura uses the device values only to bind the ticket and does not store them. Do not log them raw; hash them if you need them for debugging.

## Never put secrets here
| Value | App / front-end JS | Server logs, APM, proxies | Analytics | Crash reports | URLs | Support chats |
|---|---|---|---|---|---|---|
| API key | **never** | **never** (redact `Authorization`) | **never** | **never** | **never** | prefix only |
| `play_auth_token`, `manifest_url` | memory only, current play | **never** | **never** | **never** | **never** | **never** |
| `union_info`, `drm_kek` | generated there | hashed only | **never** | avoid | **never** | if asked |
| `viewer_ref` | — | yes | pseudonymous only | yes | avoid | yes |

The safe-logging helper in the examples drops these fields for you: [Observability](https://docs.wandura.ai/developers/production/observability#never-log).

## Transport
- Always `https://www.wandura.ai`. TLS 1.2 or 1.3 only.
- Not `wandura.ai` without `www`: it redirects, and some clients drop the body of a `POST` on a redirect.
- The API does not use cookies for authentication and sends no CORS headers: browser calls fail by design.
- Never put a key in a query string. The API only reads the `Authorization` header.

## Related
- [Authentication](https://docs.wandura.ai/developers/authentication): sending the key, scopes.
- [Go-live checklist](https://docs.wandura.ai/developers/production/go-live): the security items to tick before release.
- [Canonical API behaviour](https://docs.wandura.ai/developers/reference/behaviour#freshness): every timing on one page.
