---
title: Authentication
description: Device-code flow, API keys, scopes, and token rotation.
section: Getting started
order: 2
---

# Authentication

Two credential types, one rule: every request carries one of them. Catalog and sync endpoints both require a credential; the only exceptions are the [public list and discovery endpoints](/dev/lists) and the global [calendar](/dev/calendar).

| Credential | Best for | How it authenticates |
| --- | --- | --- |
| Session token | Apps acting as a signed-in person (Kodi addons, TV apps, mobile) | `Authorization: Bearer <token>`, obtained through the device-code flow |
| API key | Scripts and server-side tools tied to your own account | `Authorization: Bearer fs_live_…` or `?api_key=fs_live_…` (TMDB style) |

## The device-code flow

Four steps, following the standard device-authorization pattern (RFC 8628, the same flow TV apps use everywhere). The [quickstart](/dev/quickstart) walks it end to end with the full poll loop in three languages.

> [!TIP]
> The auth endpoints are the one group that lives at `https://flicklist.tv/api/auth/…`, one level above `/v3`. Every other path in these docs hangs off `https://flicklist.tv/api/v3`.

1. `POST /auth/device/code` with your registered `client_id`. Returns `user_code`, `device_code`, `verification_uri`, `expires_in` (900 seconds), and `interval` (minimum seconds between polls).
2. Show `user_code` and point the person at `https://flicklist.tv/link`, where they approve the device from any signed-in browser. They can label the session ("Living Room TV") and revoke it later from their Connected Apps settings.
3. `POST /auth/device/token` with the `device_code`, no faster than `interval`. The endpoint returns `200` for every state. The body's `error` field tells you where things stand: `authorization_pending` (keep polling), `access_denied` (the person declined, stop polling), or `expired_token` (the 15-minute window lapsed, request a fresh code). After approval the body carries `access_token` plus the account's profile. That profile object currently includes the account email and internal role fields: treat it as sensitive, and don't display, store, or transmit more of it than your app needs. Only an unrecognized `device_code` gets a non-200.
4. Store the token. It is a 30-day session bound to your `client_id`, and the response's `expires_at` tells you exactly when it lapses.

> [!REQUIRED]
> `client_id` must be a registered app; unregistered ids are rejected at step 1. There is no self-serve registration yet: [contact FlickList](/dev/faq#how-do-i-get-a-client_id) to register yours.

## Token rotation

`POST /auth/refresh` with a valid session token returns a new token with a fresh 30-day expiration and invalidates the old one (rotation, not extension). Refresh whenever you like; a common pattern is on app start when the token is older than a week.

API keys are not refreshed through this endpoint (sending one returns 400). They carry their own independent expiration.

## API keys and scopes

Create keys from your FlickList dashboard (Settings, then Apps). Treat a key like a password: it belongs in server-side code and scripts, never in browser code you ship. Keys are prefixed `fs_live_` and carry scopes:

| Scope | Grants |
| --- | --- |
| `read` | Every `GET` under `/v3/sync/*`, plus your personal calendar |
| `write` | Mutations under `/v3/sync/*` and all of `/v3/scrobble/*` |

Requests that lack the needed scope fail with `403` and the standard [error envelope](/dev/errors). Catalog reads work with any valid credential, no scope needed.

## Checking who you are

`GET /v3/me` returns the account behind the calling credential: id, username, display name, avatar. It deliberately omits email and internal flags. Use it on startup to confirm a stored token still works and which account it belongs to.

:::tabs
```bash
curl -H "Authorization: Bearer $TOKEN" "https://flicklist.tv/api/v3/me"
```
```javascript
const me = await fetch('https://flicklist.tv/api/v3/me', {
  headers: { Authorization: `Bearer ${token}` }
}).then((r) => r.json());
```
```python
me = requests.get(
    "https://flicklist.tv/api/v3/me",
    headers={"Authorization": f"Bearer {token}"},
).json()
```
:::

## Respect the revoke

Any FlickList user can disconnect your app at any moment from their own settings, without telling you first. A revoked session starts returning `401`: treat that as "re-run the device flow", never as an error worth retry-hammering.
