---
title: "Streaming API v2: Errors"
description: "Every error code, its HTTP status and what your app should do with it."
---

Every error from the API is JSON with a stable `code` and a human-readable
English `error`:

```json
{ "error": "Unauthorized: Session has been revoked.", "code": "session_revoked" }
```

**Branch on `code`, never on the text.** The text may change; codes will not.
New codes may be added, so treat an unknown code by its HTTP status.

| `code` | HTTP | Meaning | What the app does |
|---|---|---|---|
| `invalid_api_key` | 403 | The gateway's key was revoked or disabled (an unknown key from an unregistered gateway gets the HTML `404` of section 2) | "Service unavailable". It is a gateway problem, not the user's |
| `method_not_allowed` | 405 | Wrong HTTP method | Fix the client |
| `missing_credentials` | 400 | `login` without username or password | Ask again |
| `invalid_credentials` | 401 | Wrong username or password | Ask again |
| `account_disabled` | 403 | Line disabled or banned | Show it. Keep the session token |
| `account_expired` | 403 | Line expired | Show it. Keep the session token: after the reseller renews the line, `refresh-session` works again |
| `restreamer_not_allowed` | 403 | Restreamer lines cannot use this API | Show it |
| `wrong_reseller` | 403 | The reseller's key no longer serves this session's line | Show it. Keep the session token |
| `access_denied` | 403 | Access refused from where the device is (temporary IP block, reseller's domain lock) | Show it. Keep the session token and try again later |
| `token_missing` | 401 | No `Authorization: Bearer` header | Fix the client |
| `session_expired` | 401 | The session token expired but can still be renewed | Call `refresh-session` once, then retry |
| `session_invalid` | 401 | The session token is not valid here (malformed, from another key, or too old to renew) | Sign in / pair again |
| `session_revoked` | 401 | This device was signed out or unlinked, or `logout-all` was used | Sign in / pair again |
| `credentials_changed` | 401 | The line's username or password changed (also seen some time after a `logout-all`) | Sign in / pair again |
| `account_not_found` | 401 | The line no longer exists | Sign in / pair again |
| `invalid_parameter` | 400 | A parameter is missing or out of range | Fix the request |
| `not_in_package` | 403 | The content exists but this line's package does not include it | "Not included in your plan" |
| `not_found` | 404 | No movie or series with that id | Treat as gone |
| `unknown_endpoint` | 404 | No such action | Fix the client |
| `rate_limited` | 429 | Too many `history-update` reports for the line, or more than one `diagnostics` batch a minute from the device (`Retry-After`) | `history-update`: drop that report, do not retry it. `diagnostics`: keep the records and send them after `Retry-After` |
| `temporarily_unavailable` | 503 | Try again shortly | Retry with backoff |

**Not every failure is JSON.** The `404` HTML page of section 2 (gateway not
recognized), your gateway's own rate limit (`429`/`503`) and server errors
(`5xx`) come without `code`. When there is no `code`, go by the status, and
treat `429` and `5xx` as temporary: retry with backoff, and **never sign the
user out** because of them. A `429` with `code` `rate_limited` is the panel's
own limit on history and diagnostics reports (above).

The streaming nodes answer with bare HTTP status codes, not JSON (section 7.4).

API responses carry `Cache-Control: no-store` (except the channel map of
`playback-routes`, which has its own `max-age` and `ETag`): do not cache them
in shared caches.
