> ## Documentation Index
> Fetch the complete documentation index at: https://api.simkl.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Rewatches

> Track separate viewing sessions for items the user already finished — mirror what simkl.com displays on a movie / show / anime detail page.

A **rewatch** is a separate viewing session for an item the user already completed. Each session has its own start date, episode progress (for shows / anime), and status (`active`, `completed`, or `closed`). Up to 50 sessions per item. Simkl returns the full session list so you can replicate the simkl.com "Rewatches" panel.

<Info>
  **Two watches of the same movie or episode must be at least 2 days apart.** Anything closer is absorbed into the existing session rather than starting a new one — it's a rewatch, not a rewind. On [`POST /scrobble/stop`](/api-reference/simkl/scrobble-stop) you'll see `rewatch_status: "too_soon"`; on [`POST /sync/history`](/api-reference/simkl/add-to-history) the write is simply a no-op.

  This is deliberate: it absorbs player retries, scrubbing back and forth across the 80% mark, and importer re-runs, which would otherwise fill the user's history with phantom sessions. Don't treat it as an error or retry it.

  Details and the other ceilings (50 sessions per item) in [The 2-day gap](#the-2-day-gap) and [Limits and rules](#limits-and-rules).
</Info>

<Warning>
  **Read this guide before enabling `?allow_rewatch=yes` in production.** Wired up carelessly — on retries, scrobble events, importer re-runs, or without pinning `rewatch_id` after the first write — the flag pollutes the user's history stats and rewatches panel with phantom sessions. Before flipping it on:

  1. **Pin `rewatch_id`** on every write after the first. Without it, sessions can fork silently.
  2. **Gate `?allow_rewatch=yes` behind explicit user intent** — a dedicated "Rewatch" button. Never on background syncs, importer re-runs, or retries. Player scrobbling is supported on [`POST /scrobble/stop`](/api-reference/simkl/scrobble-stop) and [`POST /scrobble/checkin`](/api-reference/simkl/scrobble-checkin), and only when the user asked for it: sending the flag on `/scrobble/start` can silently open a session on whatever the user *previously* left playing. See [Scrobble](#scrobble).
  3. **Default off in your app settings.** Expose a per-user *"Track rewatches"* toggle, starting disabled — many users prefer plain "mark watched" without session bookkeeping. Check the user's plan when they switch it on and prompt for an upgrade if they're on the free tier: [Adding a rewatch toggle to your app](#adding-a-rewatch-toggle).
  4. **Test the full lifecycle** (start → episodes → close → reactivate → complete) on a real account before shipping.

  If you just need to mark watched, plain `POST /sync/history` is what you want — see [Mark as watched](/guides/mark-as-watched).
</Warning>

<a id="pro-vip-gate" />

<Note>
  **Simkl PRO / VIP only.** Free-tier callers with `?allow_rewatch=yes` get a silent no-op on `POST /sync/history` — the request still consumes a rate-limit slot but returns `added: { movies: 0, shows: 0, episodes: 0 }`. [`POST /scrobble/stop`](/api-reference/simkl/scrobble-stop) is the exception: it answers `rewatch_status: "pro_required"`, so you can read the reason off the response. Either way, **gate the flag behind the user's plan instead of sending it blindly:**

  1. **On sign-in (or first launch)**, call [`GET /users/settings`](/api-reference/simkl/get-user-settings) once and read `account.type` — one of `"free"`, `"pro"`, or `"vip"`. Cache it locally.
  2. **Don't refetch on every poll.** User settings are set-and-forget. Watch `activities.settings.all` from [`GET /sync/activities`](/api-reference/simkl/get-activities) (you're already polling it for the sync delta — see the [Sync guide](/guides/sync)); when that timestamp bumps, refetch `/users/settings` once to pick up plan upgrades, downgrades, timezone changes, etc.
  3. **If `account.type === "free"`**, hide or disable the "Rewatch" button with a short note like *"Rewatch tracking requires Simkl PRO or VIP"* and link to [simkl.com/vip](https://simkl.com/vip/). Don't send `?allow_rewatch=yes` for free-tier users — there's no value in firing a request that the server will silently drop.
</Note>

## Endpoints used

<CardGroup cols={2}>
  <Card title="POST /sync/history" icon="circle-plus" href="/api-reference/simkl/add-to-history">
    Write watch events. `?allow_rewatch=yes` makes the write land in a rewatch session instead of the canonical row.
  </Card>

  <Card title="GET /sync/all-items" icon="list" href="/api-reference/simkl/get-all-items">
    Read sessions back. `?allow_rewatch=yes` adds one extra row per saved session alongside the canonical entry.
  </Card>

  <Card title="POST /scrobble/stop" icon="circle-stop" href="/api-reference/simkl/scrobble-stop">
    For players. `?allow_rewatch=yes` on a stop with `progress` ≥ 80, or on a check-in. Never on `/scrobble/start`.
  </Card>
</CardGroup>

<a id="adding-a-rewatch-toggle" />

## Adding a rewatch toggle to your app

Rewatch tracking is opt-in twice over: once by the user in your settings, and once by Simkl's plan gate. Wire both before you send `?allow_rewatch=yes` anywhere.

### 1. Ship it off by default

**The toggle must start disabled.** Most people just want "mark watched" and would be confused by a title that shows as completed *and* has a session in progress. Turning it on should be a deliberate act.

Something like this in your settings screen:

```
[ ] Track rewatches                        Simkl PRO
    Log each time you rewatch something as a
    separate viewing, with its own dates and
    episode progress.
```

### 2. Check the plan when they flip it on

Rewatches are a **Simkl PRO / VIP** feature. Check before enabling, not after the first failed write — a free-tier `POST /sync/history` with the flag is a silent no-op that still costs a rate-limit slot.

You should already have `account.type` cached from sign-in (see [the plan check](#pro-vip-gate)). If it isn't `"pro"` or `"vip"`, don't enable the toggle — show an upgrade prompt instead:

```
Rewatch tracking requires Simkl PRO

Log each rewatch as its own viewing session, with
separate dates and episode progress.

                [ Not now ]  [ Upgrade ]
```

Point **Upgrade** at [https://simkl.com/vip/](https://simkl.com/vip/).

```js theme={"theme":{"light":"github-light","dark":"vesper"}}
async function onTrackRewatchesToggled(enabled) {
  if (!enabled) {
    prefs.trackRewatches = false;
    return;
  }

  // Cached at sign-in; refreshed only when activities.settings.all bumps.
  if (!['pro', 'vip'].includes(account.type)) {
    showUpgradeDialog({
      title: 'Rewatch tracking requires Simkl PRO',
      url: 'https://simkl.com/vip/',
    });
    return;                     // leave the toggle off
  }

  prefs.trackRewatches = true;
}
```

<Warning>
  **Don't call [`GET /users/settings`](/api-reference/simkl/get-user-settings) every time the user opens your settings screen.** Read the cached value. It refreshes on its own when `activities.settings.all` from [`POST /sync/activities`](/api-reference/simkl/get-activities) changes — the same poll you already run for the sync delta.

  If you want to be certain the plan is current at the exact moment someone taps Upgrade and comes back, refetch once on app foreground, not on every render.
</Warning>

### 3. Handle the downgrade

A user can be PRO today and free next month. When `activities.settings.all` bumps and the refetched `account.type` comes back `"free"`, stop sending the flag and disable the toggle — but **don't delete anything**. Their existing sessions stay on their Simkl account and reappear if they resubscribe.

Both surfaces fail safely if you miss it: `POST /sync/history` no-ops, and [`POST /scrobble/stop`](/api-reference/simkl/scrobble-stop) answers `rewatch_status: "pro_required"`, which is a good trigger to flip your cached flag and re-check the plan.

## Quick start

**A movie rewatch:**

```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
curl -X POST "https://api.simkl.com/sync/history?allow_rewatch=yes&client_id=$CLIENT_ID&app-name=my-app-name&app-version=1.0" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "User-Agent: my-app-name/1.0" \
  -d '{ "movies": [{ "ids": { "simkl": 53536 }, "watched_at": "2026-05-10T20:00:00Z" }] }'
```

**A TV episode rewatch — first call creates the session:**

```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
POST /sync/history?allow_rewatch=yes

{ "shows": [{ "ids": { "simkl": 1234 }, "seasons": [{ "number": 1, "episodes": [{ "number": 1, "watched_at": "2026-05-10T20:00:00Z" }] }] }] }
```

Response echoes `rewatch_id` and `rewatch_status` inside `added.statuses[].response`. **Cache the `rewatch_id` and pin it on every subsequent write** for that session.

## When does a write become a rewatch?

The server always tries the regular write first. If the regular write would actually mark new data, that takes precedence — no rewatch is created. A write lands in a rewatch session when **any** of these holds:

1. **The item is already Completed and the regular add is a no-op** (movies: user already finished it; shows: payload has no episodes and the show is Completed).
2. **You included episodes and every one is already on the user's history** — i.e. the regular write would change nothing.
3. **You passed `is_rewatch: true`** on the item.

`is_rewatch: true` does **not** bypass the regular-first logic. If the write tries to add a date that already exists in canonical history, it's still rejected. The flag's main use is to create an empty rewatch session on a Completed item your UI explicitly marked as *"starting a rewatch"* — without writing any episodes yet.

The episode or movie must already exist on the user's history (`completed` / `watching` / `hold` / `dropped`) — Plan-to-Watch entries can't be rewatched, there's nothing to rewatch.

<a id="scrobble" />

## Scrobble (players and media servers)

If your app is a player that reports playback with [`/scrobble/*`](/guides/scrobble) instead of writing history directly, you can record rewatches too.

**The problem it solves:** your user finishes *Interstellar* for the third time. Your player sends `/scrobble/stop` at 95%. Simkl looks at it, sees the movie is already marked watched, and throws the event away. The user's rewatch count never moves.

**The fix:** add `?allow_rewatch=yes` to that one call. Simkl then logs it as a separate viewing session instead of discarding it.

### The whole feature in one call

```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
curl -X POST "https://api.simkl.com/scrobble/stop?allow_rewatch=yes&client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0" \
  -H "Authorization: Bearer USER_ACCESS_TOKEN" \
  -H "User-Agent: my-app-name/1.0" \
  -H "Content-Type: application/json" \
  -d '{"movie": {"ids": {"simkl": 53494}}, "progress": 95}'
```

<Warning>
  **`progress` must be ≥ 80.** That's the threshold at which a stop marks the item watched, and a rewatch can only exist where a watch does. A stop below 80 resolves to `action: "pause"` — it saves a resume position, marks nothing, and returns **no rewatch fields at all**, whether or not you sent the flag.

  If you're getting no `rewatch_status` back, check `action` in the response first. `"pause"` means the progress was too low, not that the rewatch failed.
</Warning>

You get the normal scrobble response plus two extra keys:

```json theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "action": "scrobble",
  "progress": 95,
  "rewatch_id": 21284,
  "rewatch_status": "active"
}
```

### Reading the result

`rewatch_status` is the only field you need to branch on. Three values mean it worked:

| `rewatch_status` | What happened |
| - | - |
| `active` | Saved to a session that's still running. The usual result for a TV show or anime, where more episodes are still to come. |
| `completed` | Saved to a session that is now finished. **Movies always land here** — one watch is the whole rewatch. |
| `closed` | Saved to a session the user had manually closed or left part-finished. |

The rest tell you why nothing was saved. None of them are errors you need to retry:

| `rewatch_status` | What happened | What to show the user |
| - | - | - |
| `first_watch` | Not a repeat. This was the user's first time, so it went into normal history. | Nothing. This is the happy path for a first viewing. |
| `too_soon` | The user already logged a watch of this same item within the last 2 days. See [The 2-day gap](#the-2-day-gap). | Nothing, or a quiet "already counted". |
| `not_eligible` | The item is on Plan-to-Watch. There's no original watch to repeat yet. | Nothing. Mark it watched normally first. |
| `pro_required` | The account is Free. | Let the user know rewatch tracking needs PRO. |
| `null` | The watch itself failed to save, so no rewatch was attempted. Rare. | Nothing. |

<Warning>
  **`rewatch_id` is not a success signal.** A session that's already running reports its id **even when it rejected your watch** — inside the [2-day gap](#the-2-day-gap), for example. If you check `if (rewatch_id)` you will tell users a rewatch was saved when it wasn't. Always branch on `rewatch_status`.
</Warning>

<Note>
  This list is **longer than the `rewatch_status` on [`GET /sync/all-items`](/api-reference/simkl/get-all-items)**, which only ever returns the three session states. If you share an enum type between the two, widen it here.
</Note>

### Send it on stop or checkin, never on start or pause

<Danger>
  **Never put `allow_rewatch=yes` on `/scrobble/start`.** It will eventually log a rewatch on a title the user isn't even watching.

  Here's how: your user watched 90% of *Dune* last night, then closed the player without a clean stop. Tonight they press play on *Arrival*. Your `/scrobble/start` for *Arrival* makes Simkl tidy up that stale *Dune* session first — and if the flag is on the URL, ***Dune* gets logged as a rewatch**. Nothing in the response mentions it, so you'd never notice in testing.
</Danger>

| Endpoint | What `allow_rewatch=yes` does here | Send it? |
| - | - | - |
| [`/scrobble/stop`](/api-reference/simkl/scrobble-stop) | With `progress` ≥ 80: logs the rewatch immediately and returns `rewatch_id` + `rewatch_status`. Below 80 it's a pause and nothing happens. | **Yes** — when the user asked for it |
| [`/scrobble/checkin`](/api-reference/simkl/scrobble-checkin) | Remembers the opt-in and logs the rewatch later, when the check-in matures. No rewatch fields in the response. | **Yes** — when the user asked for it |
| [`/scrobble/start`](/api-reference/simkl/scrobble-start) | Nothing for this title. Silently applies to whatever the user *previously* left playing. | **No** |
| [`/scrobble/pause`](/api-reference/simkl/scrobble-pause) | Nothing at all. Pausing never marks anything watched. | **No** |

<a id="checkin-rewatches" />

#### Check-ins rewatch too, just not straight away

[`/scrobble/checkin`](/api-reference/simkl/scrobble-checkin) is fire-and-forget: it doesn't mark anything watched when you call it, so it can't tell you a rewatch result either. Simkl **stores the opt-in on the check-in** and applies it when the check-in matures into a watch at the end of the item's runtime.

Three things follow from that, and they're easy to get wrong:

* **No `rewatch_id` / `rewatch_status` in the response.** Nothing has happened yet. If you need to confirm the session exists, read it back later from [`GET /sync/all-items?allow_rewatch=yes`](/api-reference/simkl/get-all-items).
* **The PRO / VIP check runs at check-in time.** A user who is PRO when you check in still gets the rewatch when it matures, even if their plan lapses in between.
* **The decision is frozen once the check-in is recorded.** You can't add or remove the flag afterwards. Deleting the playback before it matures cancels the whole check-in, rewatch included.

If your app can reliably catch a stop event, prefer [`/scrobble/stop`](/api-reference/simkl/scrobble-stop) for rewatches — you get an immediate, readable answer instead of having to poll for one.

### Wiring it into your player

Four gates, all of which must be true before the flag goes on the URL:

<Steps>
  <Step title="Is the account PRO or VIP?" icon="crown">
    Check `account.type` once at sign-in and cache it. See [the plan check](#pro-vip-gate) above.
  </Step>

  <Step title="Has the user enabled rewatch tracking in your settings?" icon="toggle-off">
    Default this **off**. Plenty of people just want "watched" without session bookkeeping.
  </Step>

  <Step title="Did the user actually ask for this watch to be a rewatch?" icon="hand-pointer">
    A "Watching again" button, or a prompt when they replay something already completed. Never infer it.
  </Step>

  <Step title="Is this the stop call?" icon="stop">
    Only [`/scrobble/stop`](/api-reference/simkl/scrobble-stop), with `progress` ≥ 80 so it actually marks watched.
  </Step>
</Steps>

```js theme={"theme":{"light":"github-light","dark":"vesper"}}
// 1 + 2: cached from sign-in and your own settings screen.
const canRewatch = ['pro', 'vip'].includes(account.type) && prefs.trackRewatches;

// 3 + 4: only on stop, only when the user asked.
const query = canRewatch && userChoseRewatch ? '?allow_rewatch=yes' : '';
const res = await api.post('/scrobble/stop' + query, { movie, progress: 95 });

// Branch on the status, never on the id.
if (['active', 'completed', 'closed'].includes(res.rewatch_status)) {
  toast('Rewatch saved');
} else if (res.rewatch_status === 'pro_required') {
  promptUpgrade();
}
// first_watch / too_soon / not_eligible / null: say nothing, the watch is still recorded.
```

<Tip>
  Free accounts get `rewatch_status: "pro_required"` back, which is more helpful than [`POST /sync/history`](/api-reference/simkl/add-to-history), where a Free-tier rewatch is a silent no-op. Still check the plan client-side so you're not spending a rate-limit slot to be told no.
</Tip>

## Per-item rewatch fields

**All fields below are optional.** They only take effect when `?allow_rewatch=yes` is on the URL.

| Field | Type | Purpose |
| - | - | - |
| `is_rewatch` | bool | Trigger rewatch logic explicitly. See [When does a write become a rewatch?](#when-does-a-write-become-a-rewatch) for what overrides what. |
| `rewatch_id` | int | Resume a specific session. Use the value from a previous response. Without it, the server picks the item's active session if one exists, else creates a new session. |
| `rewatch_status` | string | Explicitly set session state — `active`, `closed`, or (movies only) `completed`. See [Session states](#session-states) for the full state machine. |
| `watched_at` | string (ISO-8601) | Standard per-item timestamp. Also serves as the session's start date. |
| `last_watched_at` | string (ISO-8601) | Primarily a **response** field from `GET /sync/all-items` (the session's most recent watch event). Rarely set on writes. |

## Session states

A session is in exactly one of three states. For TV / anime, `completed` is **server-earned only**: the server promotes a session when the rewatch covers every aired regular episode (specials in season 0 don't count). For movies, `completed` is the default and only meaningful state.

| State | Meaning |
| - | - |
| `active` | In progress. Exactly one per item — starting another auto-closes this one. |
| `closed` | Explicitly ended by the user. Resumable. Keeps `watched_episodes_count`. |
| `completed` | All aired regular episodes covered (TV / anime); or the movie's default. |

**Transitions you can drive from a write** (`POST /sync/history?allow_rewatch=yes` + `rewatch_status: "..."`):

* `"active"` — start or reactivate a session. The previously-active session for the item auto-closes.
* `"closed"` — end the active session. No-op against a `completed` session; reactivate it first if you really need to close it.
* `"completed"` — honored for movies. **For TV / anime, silently clamped to `"closed"`** because only coverage earns DONE. Subsequent episode writes that complete coverage will promote the session back to `"completed"`.

**Transitions the server runs automatically:** any non-`completed` session (whether `active` or `closed`) auto-promotes to `completed` the moment an episode write pushes coverage to every aired regular episode.

## The 2-day gap

Any two watch events on the **same item** (movie or individual episode) must be at least 2 days apart, or the second write collapses into the first session. *It's a rewatch, not a rewind* 😄.

The gap is **per-item**, not per-show. Watching S1E1 on Monday and S1E2 on Tuesday is fine — those are different items. The rule only fires when the *same* episode is written twice within 48h.

<Info>
  **Why a 2-day gap?** It absorbs common timestamping problems that would otherwise inflate the user's history with phantom rewatches:

  * **Sleep-and-resume** — user starts an episode at night, finishes the next morning. One viewing, not two.
  * **Wrong timezones** — clients labeling local time as UTC (or vice versa) drift 1–12 hours per write.
  * **DST transitions** — naive date libraries miscalculate by an hour twice a year.
  * **Multi-device duplicates** — phone + TV both report the same play.
  * **Retry storms** — flaky connections retry a write that already landed.
  * **Importer re-runs** — re-running an import re-sends the same `watched_at`.
  * **Scrobble pause/resume noise** — bathroom break → re-fire of `/start` + `/stop`.
  * **Buggy progress reporting** — players hitting 80%+ multiple times on seeks.

  Real rewatches happen days, weeks, or months apart — not within hours. The 48-hour rule encodes that.
</Info>

## Reading sessions back

`?allow_rewatch=yes` on `GET /sync/all-items` adds one extra entry per saved rewatch session alongside the canonical entry for each item. **Always pair with `date_from`** — never call without it outside the initial Phase-1 sync.

<Warning>
  **`allow_rewatch=yes` alone returns summary-only rewatch rows.** `watched_episodes_count` returns `0` as a sentinel, `last_watched` is `null`, and `seasons[].episodes[]` is absent — even when the session actually has episodes recorded. To get per-episode data on rewatch rows you **must** add `extended=full` (and usually `episode_watched_at=yes` for timestamps):

  ```
  GET /sync/all-items/all/completed?allow_rewatch=yes&extended=full&episode_watched_at=yes
  ```

  Apps that mirror simkl.com's "Rewatches" panel — or that need to apply per-episode diffs from rewatch sessions to a local cache — should always use this flag combo. The [Flag combinations table](#flag-combinations-for-episode-lists) below has the full set.
</Warning>

### Canonical vs rewatch rows

| Field | Canonical (`is_rewatch: false`) | Rewatch (`is_rewatch: true`) |
| - | - | - |
| `is_rewatch` | `false` *(emitted when `?allow_rewatch=yes` is set; otherwise absent)* | `true` |
| `rewatch_id` | absent | session id |
| `rewatch_status` | absent | `active` / `completed` / `closed` |
| `last_watched_at` | The canonical (original) last watch date for the item — **does not** reflect rewatch sessions. | This session's most-recent watch event. |
| `watched_episodes_count` | Episodes marked watched on the canonical row (e.g. `62/62` for a finished show). **Not** a lifetime total across rewatches. | Per-session count — **`0` as a sentinel without `extended=full`**. |

The canonical row and the rewatch rows are independent: `last_watched_at` and `watched_episodes_count` on the canonical entry never incorporate rewatch progress, no matter how many sessions exist for the item.

### Worked example — Game of Thrones with one rewatch session

A representative shape from `GET /sync/all-items?allow_rewatch=yes&extended=full&episode_watched_at=yes` — Game of Thrones (Simkl ID `17465`) finished long ago, currently being rewatched in progress (4 of 73 episodes into S1). The item appears **twice** in `shows[]`: once as the canonical row, once as the in-progress rewatch row.

```json theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "shows": [
    {
      "added_to_watchlist_at":   "2026-05-15T00:13:18Z",
      "last_watched_at":         "2026-05-15T00:13:18Z",
      "user_rated_at":           "2026-05-16T14:20:16Z",
      "user_rating":             10,
      "status":                  "completed",
      "last_watched":            null,
      "next_to_watch":           "S01E01",
      "watched_episodes_count":  73,
      "total_episodes_count":    73,
      "not_aired_episodes_count": 0,
      "show": {
        "title":  "Game of Thrones",
        "poster": "57/5742576cd8f59fcb0",
        "year":   2011,
        "runtime": 52,
        "ids": {
          "simkl":        17465,
          "slug":         "game-of-thrones",
          "imdb":         "tt0944947",
          "tvdbslug":     "game-of-thrones",
          "traktslug":    "game-of-thrones",
          "tvdb":         "121361",
          "tmdb":         "1399"
        }
      },
      "is_rewatch": false
    },
    {
      "added_to_watchlist_at":   "2026-05-16T14:02:10Z",
      "last_watched_at":         "2026-05-16T20:00:00Z",
      "user_rated_at":           "2026-05-16T14:20:16Z",
      "user_rating":             10,
      "status":                  "completed",
      "last_watched":            "S01E04",
      "next_to_watch":           "S01E05",
      "watched_episodes_count":  4,
      "total_episodes_count":    73,
      "not_aired_episodes_count": 0,
      "show": { "title": "Game of Thrones", "ids": { "simkl": 17465, "slug": "game-of-thrones" /* ... */ } },
      "is_rewatch":      true,
      "rewatch_id":      7482,
      "rewatch_status":  "active",
      "seasons": [
        {
          "number": 1,
          "episodes": [
            { "number": 1, "watched_at": "2026-05-16T14:02:10Z" },
            { "number": 2, "watched_at": "2026-05-16T14:02:10Z" },
            { "number": 3, "watched_at": "2026-05-16T19:00:00Z" },
            { "number": 4, "watched_at": "2026-05-16T20:00:00Z" }
          ]
        }
      ]
    }
  ]
}
```

A few things to notice in the example above:

* **Same `simkl` ID on both rows** — `17465` appears twice. Group by `ids.simkl` when iterating; treat `is_rewatch: true` rows as side-cars on the canonical entry.
* **Canonical `watched_episodes_count`** is `73/73` (the user finished the show originally); the rewatch row's `4/73` is per-session and does NOT advance the canonical count.
* **`last_watched_at` is independent** on each row — canonical reflects the original final watch (2026-05-15); the rewatch row reflects its own most-recent episode (2026-05-16T20:00:00Z).
* **`rewatch_status: "active"`** because the session is mid-coverage (4/73). It auto-flips to `"completed"` when `watched_episodes_count >= total_episodes_count - not_aired_episodes_count`. See [Session states](#session-states) for the full rules.
* **`seasons[].episodes[]` only appears on the rewatch row** under this flag combo (`extended=full` + `episode_watched_at=yes`). Without `extended=full`, the rewatch row would arrive summary-only: `watched_episodes_count: 0` sentinel, no `seasons[]` block.
* **Movies are simpler** — no `seasons[]`, just `movie: { ... }`, and the rewatch row's `rewatch_status` always starts at `completed` (movies have nothing to track episode-wise).
* **Anime rewatch rows** have the same shape as show rows plus an `anime_type` field (`"tv"`, `"movie"`, etc.) on the rewatch row itself.

### Flag combinations for episode lists

| Flags (on top of `allow_rewatch=yes`) | What rewatch entries carry |
| - | - |
| (none) | Summary only. `watched_episodes_count` returns `0` as a **sentinel** even when the session actually has episodes. |
| `extended=full` | Summary fields **populated** with real per-session counts, plus `seasons[].episodes[]` listing episode `number`. |
| `extended=full_anime_seasons` | Same plus anime `tvdb` mapping per episode. |
| `extended=full` + `episode_watched_at=yes` | Same plus per-episode `watched_at` timestamps. |

`episode_watched_at=yes` is a **modifier** — it adds timestamps to episodes already loaded. Without `extended=full`, episodes aren't loaded and the flag has no effect on rewatch entries.

## UI patterns (mirror simkl.com)

<AccordionGroup>
  <Accordion title="Stats badge — '×3 times (1 partial), last 12 days ago'">
    Filter `?allow_rewatch=yes` response for the current item and count by `rewatch_status`. `closed` is the "partial" count; `completed` is the clean-finish count.

    ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
    const sessions = response.filter(r => r.is_rewatch && r.show.ids.simkl === currentId);
    const completed = sessions.filter(s => s.rewatch_status === 'completed').length;
    const closed    = sessions.filter(s => s.rewatch_status === 'closed').length;
    const lastDate  = sessions.map(s => s.last_watched_at).sort().pop();
    ```
  </Accordion>

  <Accordion title="'Currently rewatching #N' progress bar (TV / anime)">
    Find the `active` session and show `watched_episodes_count / total_episodes_count`. Number the badge by chronological index among all sessions for the item — the number isn't stored on the server, you compute it client-side.

    Movies don't really have an active phase — every movie rewatch session flips to `completed` immediately.
  </Accordion>

  <Accordion title="'Mark next episode rewatched' button">
    Fetch the active session with `?allow_rewatch=yes&extended=full&date_from=<your-last-sync>`, walk `total_episodes_count`, find the first episode not in the session's `seasons[].episodes[]` list, and POST it pinned to the session's `rewatch_id`.
  </Accordion>

  <Accordion title="'Reactivate' / 'Start new' / 'Close' buttons">
    All three are `POST /sync/history?allow_rewatch=yes`:

    * **Reactivate** an older session — `rewatch_id: N, rewatch_status: "active"`. The previously-active session for the item auto-closes.
    * **Start new** — write the first episode, or `is_rewatch: true` for an empty session on a Completed item.
    * **Close** — `rewatch_id: N, rewatch_status: "closed"`. Keeps `watched_episodes_count`; resumable by another write to the same `rewatch_id`. No-op on a `completed` session — reactivate first.

    **Pin `rewatch_id` on every write after the first.** Without it, finished sessions need `last_watched_at` to pinpoint which row to resume — otherwise the server creates a new row. (The singleton `active` session is always resumable by bare `is_rewatch: true`.)
  </Accordion>

  <Accordion title="Rewatch history list — '×3 sessions' panel">
    Filter rewatch entries, sort by `last_watched_at` ascending. Label sessions "Rewatch #1", "Rewatch #2", etc. on the client.
  </Accordion>
</AccordionGroup>

## Limits and rules

* **Maximum 50 rewatches per item** (movie, show, or anime — combined cap).
* **One `active` session per item.** Making another session active auto-closes the previous active one.
* **2-day minimum gap** between watch events on the same movie or episode. See [The 2-day gap](#the-2-day-gap).
* **Simkl PRO / VIP only.** Free-tier writes are silent no-ops.

## Edge cases

<AccordionGroup>
  <Accordion title="Free-tier user calls `?allow_rewatch=yes`">
    Returns `200 OK` with `added: { movies: 0, shows: 0, episodes: 0 }`. No error code is surfaced. To prevent this in your app, check `account.type` from [`GET /users/settings`](/api-reference/simkl/get-user-settings) at sign-in (cache it locally; refetch only when `activities.settings.all` bumps — see the [PRO / VIP gate](#) at the top of this page). Reading the `added` counts in the response is a fallback if you can't gate up-front, but it's wasteful — the request still consumed a rate-limit slot.
  </Accordion>

  <Accordion title="50-cap reached">
    Subsequent attempts past 50 may start being rejected once server-side enforcement is added. Count sessions client-side before exposing a "Start new rewatch" button.
  </Accordion>

  <Accordion title="Active session collision">
    Starting a new active session auto-closes the previous one (its `watched_episodes_count` is preserved). The old session is still readable and still counts toward the 50-cap.
  </Accordion>

  <Accordion title="`watched_at` collides with canonical history">
    A rewatch write — including one with `is_rewatch: true` — is rejected if the `watched_at` date matches an existing canonical episode/movie row. Use a different `watched_at` value, or omit the conflicting episode.
  </Accordion>

  <Accordion title="Same episode written twice within 48h">
    Second write collapses into the same session. `watched_episodes_count` doesn't increment; `last_watched_at` updates to the latest value. Intentional — see [The 2-day gap](#the-2-day-gap).
  </Accordion>

  <Accordion title="Conflict-split on same episode, different dates">
    If an episode is already in the active session but a new write for that same episode arrives with a `watched_at` more than 48h away, the server closes the current active session (preserving its `watched_episodes_count`) and opens a fresh active session with the conflicting episode at the new timestamp. Common with offline-capable apps catching up after a long sync gap.
  </Accordion>

  <Accordion title="Sessions started via simkl.com are pre-populated">
    When a rewatch session is created through the simkl.com web UI's "Start new rewatch" button, the server bulk-inserts episode rows for every episode the user had previously watched (a snapshot of their prior progress). Sessions created via plain API writes don't do this. Re-fetch `/sync/all-items` after a session was created on simkl.com to see what's actually there.
  </Accordion>

  <Accordion title="`1970-01-01T00:00:01Z` on a rewatch episode">
    Intentional *"watched, date unknown / long ago"* placeholder — not a NULL or bug. See [Dates and timezones → "Very long time ago" placeholder](/conventions/dates#very-long-time-ago).
  </Accordion>
</AccordionGroup>

## Reference

<CardGroup cols={2}>
  <Card title="Sync guide" icon="arrows-rotate" href="/guides/sync">
    Two-phase model, `date_from` semantics, deletion reconciliation, edge cases.
  </Card>

  <Card title="Mark as watched" icon="circle-check" href="/guides/mark-as-watched">
    Simple "mark watched" flow without rewatch sessions.
  </Card>

  <Card title="POST /sync/history" icon="circle-plus" href="/api-reference/simkl/add-to-history">
    Endpoint reference + interactive playground.
  </Card>

  <Card title="GET /sync/all-items" icon="list" href="/api-reference/simkl/get-all-items">
    Endpoint reference + interactive playground.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.