---
title: Scrobbling
description: Report playback in near real time and let FlickList mark things watched automatically.
section: Scrobbling
order: 1
---

# Scrobbling

Tell FlickList what is playing and how far along it is; FlickList keeps resume points current and marks items watched when playback genuinely finishes. Three endpoints cover the whole lifecycle: `POST /v3/scrobble/start`, `POST /v3/scrobble/pause`, `POST /v3/scrobble/stop`.

> [!REQUIRED]
> All scrobble endpoints need the `write` scope (see [Authentication](/dev/authentication)), and the item must resolve to a TMDB-linked catalog entry. An item without a TMDB link returns `400`; send whatever ids you have and let the [ids object](/dev/ids) precedence do the resolution.

## The lifecycle

**Start** when playback begins. Start is an upsert: it creates or updates the resume point for this item, and it doubles as the progress heartbeat. There is no separate heartbeat verb. While playback runs, re-POST `start` with updated `progress` every 30 to 60 seconds. That is roughly 60 to 120 requests per hour, comfortably inside the [rate limit](/dev/rate-limits).

**Pause** only when the person actually pauses. There is no cadence on pause. Pause never creates a resume point: if no active session matches the item, the response comes back with `"id": null` and nothing is written, which makes it safe to send on ambiguous player events.

**Stop** when playback ends for any reason: natural finish, user stop, or your app shutting down. Stop is where the outcome is decided.

:::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": 27205 },
    "progress": 12.4
  }'
```
```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: 27205 },
    progress: 12.4
  })
});
```
```python
requests.post(
    "https://flicklist.tv/api/v3/scrobble/start",
    headers={"Authorization": f"Bearer {token}"},
    json={"media_type": "movie", "ids": {"tmdb": 27205}, "progress": 12.4},
)
```
:::

For an episode, send `"media_type": "show"` (or `"tv"`, both are accepted) with the show's ids plus `season` and `episode` numbers.

## What stop decides

| Progress at stop | Session length | Outcome |
| --- | --- | --- |
| 90% or more | At least 300 seconds, or 15% of the runtime, whichever is greater | Marked **watched**, resume point cleared |
| 90% or more | Under that floor | Recorded as a **preview**, resume point cleared |
| Under 90% | Any | Resume point kept as **partial**, playback resumes there next time |
| Under 0.5%, no progress ever recorded | Any | Discarded as a phantom event, nothing written |

The response's `watch_status` tells you which branch you landed in, so your app never has to guess.

## Two behaviors worth knowing

- **Stale-session cleanup rides on start.** Starting playback also resolves your account's other lingering in-progress sessions: siblings already at 90% or more get marked watched, the rest get their resume points persisted. A crashed player session heals itself the next time anything starts.
- **Send stop even on shutdown.** A stop at current progress is what converts "the app died at 96%" into a watched mark instead of a stuck resume point.

## Reading progress back

Resume points written by scrobbling are the same ones served by [`GET /v3/sync/playback`](/dev/sync/playback), and watched marks land in [history](/dev/sync/history) and the [watched collections](/dev/sync/watched). Up Next reacts immediately; see [Up Next & change detection](/dev/sync/up-next).
