---
title: Quickstart
description: Make your first authenticated API call in minutes, with an API key or the device-code flow.
section: Getting started
order: 1
---

# Quickstart

Two ways in. If you are writing something for yourself, start with an API key and you'll make your first call in about two minutes. If you are building an app that other people will sign in to, use the device-code flow in track two.

All the JavaScript on this page uses top-level `await`: save it as `script.mjs` and run `node script.mjs`. Python examples run as plain `python3 script.py` and need the `requests` package.

## Track one: an API key (for your own scripts)

**1.** On flicklist.tv, open Settings, then Apps, and create an API key with the `read` scope. Keys look like `fs_live_…`. Treat them like passwords, and keep them out of browser code you ship.

**2.** Call the API. A bearer token here just means the credential rides in the `Authorization` header, in the form `Bearer <value>`:

:::tabs
```bash
API_KEY="fs_live_your_key_here"

curl -H "Authorization: Bearer $API_KEY" \
  "https://flicklist.tv/api/v3/sync/watched/shows" | head -c 400
echo
echo "It works."
```
```javascript
const apiKey = 'fs_live_your_key_here';

const res = await fetch('https://flicklist.tv/api/v3/sync/watched/shows', {
  headers: { Authorization: `Bearer ${apiKey}` }
});
const shows = await res.json();
console.log(`It works: ${shows.length} watched shows on this account.`);
```
```python
import requests

api_key = "fs_live_your_key_here"

shows = requests.get(
    "https://flicklist.tv/api/v3/sync/watched/shows",
    headers={"Authorization": f"Bearer {api_key}"},
).json()
print(f"It works: {len(shows)} watched shows on this account.")
```
:::

That's the whole track. Every watched show on your account, each with a full [ids object](/dev/ids). [Quick scripts](/dev/guides/scripts) takes this straight into a worked example, and the [API reference](/dev/api) has every endpoint you can point this key at.

## Track two: the device-code flow (for apps)

This is how an app on a TV, a Kodi box, or a CLI signs a user in without a password form: show the person a short code, they approve it on their phone, your app polls until a token appears.

> [!REQUIRED]
> The device flow needs a registered `client_id`, and registration is manual today: [contact FlickList](/dev/faq#how-do-i-get-a-client_id) and we'll set one up. No `client_id` yet? Track one works right now, and everything below still copy-pastes once yours arrives.

One path quirk to know before you start: the auth endpoints live at `https://flicklist.tv/api/auth/…`, one level above `/v3`. Everything else in these docs hangs off `https://flicklist.tv/api/v3`.

And one mechanical note: the four steps below build a single script. Each snippet reuses variables from the one before (`code`, then `token`), so append them to the same `script.mjs` or `script.py` as you go.

### Step 1: request a device code

Send your `client_id`. You get back a `user_code` to show the person and a `device_code` your app keeps private.

:::tabs
```bash
curl -X POST "https://flicklist.tv/api/auth/device/code" \
  -H "Content-Type: application/json" \
  -d '{"client_id": "your_client_id"}'
```
```javascript
const clientId = 'your_client_id';

const res = await fetch('https://flicklist.tv/api/auth/device/code', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ client_id: clientId })
});
const code = await res.json();
console.log(`Tell the user: enter ${code.user_code} at ${code.verification_uri}`);
```
```python
import requests

client_id = "your_client_id"

code = requests.post(
    "https://flicklist.tv/api/auth/device/code",
    json={"client_id": client_id},
).json()
print(f"Tell the user: enter {code['user_code']} at {code['verification_uri']}")
```
:::

```json
{
  "user_code": "A1B2C3D4",
  "device_code": "b2f1c9e4-…",
  "verification_uri": "https://flicklist.tv/link",
  "expires_in": 900,
  "interval": 5
}
```

### Step 2: show the code

Display `user_code` and tell the person to enter it at `flicklist.tv/link` on any signed-in device. The code pair lives for 15 minutes (`expires_in`).

### Step 3: poll for the token

Poll `POST /auth/device/token` with the `device_code`, waiting at least `interval` seconds between polls. The endpoint answers `200` for every state, including while the person has not approved yet, so check the body's `error` field, not the status code. While pending you get `"error": "authorization_pending"`; once approved you get an `access_token`. Two other codes can appear: `access_denied` (the person declined, stop polling) and `expired_token` (the 15-minute window lapsed, go back to step 1 for a fresh code).

:::tabs
```bash
DEVICE_CODE="the device_code from step 1"
INTERVAL=5 # the "interval" from step 1

while true; do
  RES=$(curl -s -X POST "https://flicklist.tv/api/auth/device/token" \
    -H "Content-Type: application/json" \
    -d "{\"device_code\": \"$DEVICE_CODE\"}")
  TOKEN=$(echo "$RES" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4)
  [ -n "$TOKEN" ] && break
  sleep "$INTERVAL"
done
echo "Authenticated. Token starts with: ${TOKEN:0:8}"
```
```javascript
let token = null;
while (!token) {
  await new Promise((r) => setTimeout(r, code.interval * 1000));
  const res = await fetch('https://flicklist.tv/api/auth/device/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ device_code: code.device_code })
  });
  const body = await res.json();
  if (body.access_token) token = body.access_token;
  else if (body.error !== 'authorization_pending') throw new Error(body.error);
}
console.log(`Authenticated. Token starts with: ${token.slice(0, 8)}`);
```
```python
import time

token = None
while token is None:
    time.sleep(code["interval"])
    body = requests.post(
        "https://flicklist.tv/api/auth/device/token",
        json={"device_code": code["device_code"]},
    ).json()
    if "access_token" in body:
        token = body["access_token"]
    elif body.get("error") != "authorization_pending":
        raise RuntimeError(body.get("error"))
print(f"Authenticated. Token starts with: {token[:8]}")
```
:::

### Step 4: make your first call

The token is good for 30 days (the response's `expires_at` says exactly when; rotate it with [`POST /auth/refresh`](/dev/authentication)). From here on, everything is back under `/v3`:

:::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());
console.log(`Signed in as ${me.username}`);
```
```python
me = requests.get(
    "https://flicklist.tv/api/v3/me",
    headers={"Authorization": f"Bearer {token}"},
).json()
print(f"Signed in as {me['username']}")
```
:::

## Where to next

- [Scrobbling](/dev/scrobbling), the flagship: report playback in near real time.
- [Authentication](/dev/authentication): scopes, token rotation, and both flows in full.
- [The ids object](/dev/ids): how items are identified everywhere.
- [Quick scripts](/dev/guides/scripts): the API-key track taken to a finished script.
- [API reference](/dev/api): every endpoint, generated from the OpenAPI spec.
