---
title: Building a Kodi integration
description: Wire device-code login, scrobbling, and watched-state sync into a Kodi addon.
section: Guides
order: 1
---

# Building a Kodi integration

Connect a Kodi addon running on a TV to FlickList, so playback there updates watch history and feeds a next-episode prompt back into the addon's UI.

## 1. Get a device code and let the user approve it

Kodi runs on a screen with no keyboard sign-in, so use the device-code flow instead of a password form. Request a code, show the `user_code` on screen, and poll until the person approves it from their phone or laptop. The full request-and-poll sequence is in [Quickstart](/dev/quickstart). This guide picks up once your addon is holding a bearer token in its settings.

```python
# see /dev/quickstart for the request/poll loop itself
token = run_device_code_login(client_id="fl_kodi_scrobbler")
addon.setSetting("access_token", token)
```

> [!REQUIRED]
> Scrobble calls need the `write` scope on the token you end up with. See [Authentication](/dev/authentication).

## 2. Start tracking playback

Call this from your player class's `onPlayBackStarted` callback (and again from `onPlayBackResumed`, after a seek). Resolve the title to a TMDB ID first, since scrobble targets must carry one.

:::tabs
```bash
curl -X POST "https://flicklist.tv/api/v3/scrobble/start" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "media_type": "movie",
    "ids": { "tmdb": 550 },
    "progress": 0.0
  }'
```
```javascript
await fetch('https://flicklist.tv/api/v3/scrobble/start', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    media_type: 'movie',
    ids: { tmdb: 550 },
    progress: 0.0,
  }),
});
```
```python
import requests
requests.post(
    "https://flicklist.tv/api/v3/scrobble/start",
    headers={"Authorization": f"Bearer {token}"},
    json={"media_type": "movie", "ids": {"tmdb": 550}, "progress": 0.0},
)
```
:::

For an episode, send `"media_type": "show"` (or `"tv"`), the show's `ids` block, and `season` and `episode` numbers. The full shape is on the [Scrobbling](/dev/scrobbling) page.

## 3. Keep sending start as your heartbeat

Kodi doesn't fire a periodic "still playing" event, so start a 30 to 60 second interval timer while the title plays and re-POST `start` with the current `progress` each time. `start` is an upsert, so every call updates the same resume point rather than creating a new one.

> [!TIP]
> That cadence works out to roughly 60 to 120 requests per hour per active session, well under the rate ceiling.

## 4. Pause when the person actually pauses

Wire this to `onPlayBackPaused`, not to buffering or seek events. Send it only when playback genuinely stops for the user.

```json
{ "id": null }
```

If nothing was actively playing that item, `pause` is a no-op and `id` comes back `null`. It never creates a new resume point on its own.

## 5. Stop at the end of playback

Call this from `onPlayBackStopped` and `onPlayBackEnded` alike, with the final `progress` value.

```json
{ "watch_status": "watched" }
```

`watch_status` tells you the outcome. At 90% progress or more, with enough of the runtime actually played, the item is marked `watched`. At 90% or more but under that time floor, it's recorded as a `preview`. Below 90%, the resume point is kept as `partial` so the person can pick up where they left off.

> [!WARNING]
> A scrobble target without a TMDB-linked catalog row returns 400. Resolve the title against the catalog before you start tracking it.

## 6. Sync watched state without hammering the API

Don't refetch full watched history every time the addon's menu opens. Poll `GET /v3/sync/last_activities` first and compare its per-domain timestamps against what you last cached.

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

Only when a domain's timestamp moved forward do you call `GET /v3/sync/watched/movies` or `GET /v3/sync/watched/shows` to refresh that part of your local cache. Everything else stays as it was.

## 7. Surface what's next

`GET /v3/sync/up_next` returns the next unwatched episode for each show the person is in the middle of, which is exactly what a "play next" prompt needs once credits roll. Scrobbling a finished episode updates it immediately, so calling it right after `stop` reflects the episode that just ended. Details, including dropping a show from the list, are on [Up Next & change detection](/dev/sync/up-next).
