> ## 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.

<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, scrobble auto-events, importer re-runs, or retries.
  3. **Default off in your app settings.** Expose a per-user *"Track rewatches"* toggle — many users prefer plain "mark watched" without session bookkeeping.
  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>

<Note>
  **Simkl Pro / VIP only.** Free-tier callers with `?allow_rewatch=yes` get a silent no-op — the request still consumes a rate-limit slot but returns `added: { movies: 0, shows: 0, episodes: 0 }`. **Gate the flag behind the user's plan instead of sending it blindly:**

  1. **On sign-in (or first launch)**, call [`POST /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>
</CardGroup>

## 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.

## 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",
          "trakttvslug":  "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` (from each episode's `completed_on`).                                          |

`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 [`POST /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-placeholder).
  </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>
