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

# Add to History

> Record watch events. The unit is the **watch event** — adding "I watched The Walking Dead S01E01 at 8pm" — not list membership (use [`POST /sync/add-to-list`](/api-reference/simkl/add-to-list) for that, or set `status` per-item here to do both at once).

<Tip>
**You don't need a Simkl ID.** Same as `/sync/add-to-list` — the server resolves any combination of identifiers (`imdb`, `tmdb`, `tvdb`, `mal`, `anidb`, `anilist`, `kitsu`) plus `title` + `year`. See [Standard media objects → Supported ID keys](/conventions/standard-media-objects#supported-id-keys) for the full list, and the [/sync/add-to-list ID-resolution table](/api-reference/simkl/add-to-list) for per-slot semantics.
</Tip>

#### Granularity — when does the server expand "implicit all"?

The body shape determines whether you mark a single episode, a season, or a whole show.

**One movie** — single movie completion.

```json
{ "movies": [{ "ids": {...} }] }
```

**Whole show** — every episode marked. *"I finished this whole series."* Send `status: "completed"` with no `seasons` / `episodes`.

```json
{ "shows": [{ "ids": {...}, "status": "completed" }] }
```

**Whole season** — every episode of one season. *"I finished season 2."* Send `seasons[]` without an inner `episodes`.

```json
{ "shows": [{ "ids": {...}, "seasons": [{ "number": 2 }] }] }
```

**Specific episodes only** — per-event scrobbling, manual tick-off.

```json
{
  "shows": [{
    "ids": {...},
    "seasons": [{
      "number": 1,
      "episodes": [{ "number": 1 }, { "number": 2 }]
    }]
  }]
}
```

**Top-level `episodes[]` shorthand** — auto-wraps to `seasons: [{ number: 1, ... }]`. Useful for anime sequential numbering and single-season shows.

```json
{ "shows": [{ "ids": {...}, "episodes": [{ "number": 1 }] }] }
```

The response always reports the actual count of episodes affected in `added.episodes`, so apps can verify the server expanded correctly.

#### Memo-only updates (and "add to watchlist + memo" in one call)

This endpoint is the **only way to set a memo on an item.** [`POST /sync/add-to-list`](/api-reference/simkl/add-to-list) accepts the `memo` field in the request body and echoes it back in the response, but **does not persist it** — silently discarded.

To set or update a memo without recording a watch event, send `ids` + `status` + `memo`:

```json
{
  "movies": [{
    "ids": { "simkl": 53536 },
    "status": "plantowatch",
    "memo": { "text": "Remind me why I added this", "is_private": true }
  }]
}
```

Two behaviors worth knowing:

- **Memos attach to watchlist items only.** The item has to be in one of the five statuses (`watching` / `plantowatch` / `hold` / `dropped` / `completed`) for the memo to stick. Sending `status` makes this explicit.
- **The endpoint auto-adds items.** If the item isn't on the user's watchlist yet, this same call creates the watchlist row at the specified `status` AND saves the memo in one shot. `added.movies` / `added.shows` reports the count of newly-added items (`0` when the item was already there and you only changed memo/status).

To read memos back, call `GET /sync/all-items?memos=yes` — see the [Sync guide](/guides/sync).

#### Per-item options

| Field | Type | Notes |
|---|---|---|
| `watched_at` | ISO-8601 string | Pin the watch event to a specific time. Defaults to request time. |
| `added_at` | ISO-8601 string | Override when the item was added to the watchlist (rarely used outside backups). |
| `status` | string | Set the watchlist status (`watching`/`plantowatch`/`hold`/`completed`/`dropped`) in the same call. Combine with `rating` to do "watched + rate + status" in one request. |
| `rating` | int 1-10 | Rate the item alongside the watch event. Same effect as a separate `POST /sync/ratings` call. |
| `memo` | `{ "text": string, "is_private": bool }` | User memo, max 140 chars. `is_private: false` shows the memo on the user's public profile + activity feed; `true` keeps it self-only. Read-back requires `/sync/all-items?memos=yes`. |
| `is_rewatch` | bool | Force the rewatch path on this item even if the server can't auto-detect (used by backup/restore tools). Requires `?allow_rewatch=yes` query param to take effect. |
| `use_tvdb_anime_seasons` | bool *(anime-only, optional)* | Default `false` (AniDB sequential — flat single-season). Set `true` to interpret `season`/`number` as TVDB per-season numbering. **Only needed when your source uses TVDB-style numbering AND the title is multi-season in TVDB** (e.g. Demon Slayer S2 Entertainment District). Single-season anime and AniDB-sequential inputs work without this flag. Use when syncing from Plex/Sonarr/Kodi/Jellyfin. |

#### Rewatches

Re-posting an already-watched episode is a **no-op by default** — the server detects the duplicate and skips. To insert a rewatch session, send `?allow_rewatch=yes` as a query parameter. The server then creates a separate rewatch row that doesn't double-count the original watch.

**Simkl Pro / VIP only.** Free-tier callers with `?allow_rewatch=yes` get a silent no-op (`added: { movies: 0, shows: 0, episodes: 0 }`) that still consumes a rate-limit slot. Check `account.type` from [`POST /users/settings`](/api-reference/simkl/get-user-settings) at sign-in and gate the flag on `"pro"` / `"vip"`. Cache the value; refetch only when `activities.settings.all` from [`GET /sync/activities`](/api-reference/simkl/get-activities) bumps. Full pattern in [Rewatches guide → Pro / VIP gate](/guides/rewatches).

For backup/restore tools that always want to insert (even when the auto-detect heuristic can't fire), set `is_rewatch: true` per-item AND pass the query param.

#### Response: `added` and `not_found`

```json
{
  "added": {
    "movies": <int count>,
    "shows": <int count>,
    "episodes": <int count>,
    "statuses": [
      {
        "request": { /* echo of input item, with type added */ },
        "response": {
          "status": "completed",
          "simkl_type": "tv" | "anime" | "movie",
          "anime_type": "tv" | "movie" | "ova" | null
        }
      }
    ]
  },
  "not_found": {
    "movies": [...],
    "shows": [...],
    "episodes": [...]
  }
}
```

**`added.statuses[*].response.status`** is the **resolved Watchlist status** the server placed the item on — e.g. a `"completed"` write on a still-airing show is silently downgraded to `"watching"` and reflected here. **You don't need a follow-up [`POST /sync/add-to-list`](/api-reference/simkl/add-to-list)** — this call already moves the item; chaining would just overwrite the server's smarter decision.

**`added.statuses[*].response.simkl_type`** tells you which catalog the item resolved to (useful when you sent ambiguous IDs — TMDB IDs can be either movie or tv on Simkl). Always inspect to know what got created.

**`not_found`** carries the verbatim input for items the resolver couldn't match (typo, fuzzy-title miss, ID not in Simkl's catalog yet). Apps should:

- Show "we couldn't track: …" UI for these
- Offer manual ID-entry fallback
- Don't infer success from the 201 status alone — branch on `not_found.movies.length === 0 && not_found.shows.length === 0 && not_found.episodes.length === 0`

#### Errors

`400 empty_field` if a per-item required field is missing. `400 wrong_parameter` for invalid enum values. Empty body `{}` returns 201 with zero counts (NOT 400) — that's a known asymmetry vs `/scrobble/start` which 400s on empty body.

<Card title="Sync guide — full walkthrough" icon="arrows-rotate" href="/guides/sync" horizontal>
 Initial-pull-then-delta-loop pattern, `date_from` semantics, deletion reconciliation, Trakt/Letterboxd migration recipes.
</Card>

#### When to use `/sync/history` vs `/sync/add-to-list`

| Goal | Endpoint |
|---|---|
| User finished watching → mark watched + rate + memo | **`/sync/history`** (carries all three in one shape) |
| User clicked "Add to Plan to Watch" button | **`/sync/add-to-list`** (status-only, no watch event) |
| Backfill from Trakt/Letterboxd/IMDb (events with timestamps) | **`/sync/history`** with `watched_at` per item |
| Bulk import a watchlist (no watch events) | **`/sync/add-to-list`** |
| Remove an item from the user's library | **[`/sync/history/remove`](/api-reference/simkl/remove-from-history)** |

<Tip>
**Which IDs can I send/expect?** All accepted input identifiers and the keys you'll see echoed back in responses are listed at [**Standard media objects → Supported ID keys**](/conventions/standard-media-objects#supported-id-keys). Send every ID you have on writes — Simkl picks the first that resolves and ignores the rest. Reminder: `slug` is **response-only** (never send it on a request).
</Tip>



## OpenAPI

````yaml /openapi.json post /sync/history
openapi: 3.1.0
info:
  title: Simkl API
  version: 1.0.0
  description: >-
    The **Simkl API** lets you build apps that track Movies, TV Shows, and Anime
    — scrobble playback, sync watch history, and pull rich metadata.


    All requests use HTTPS and return JSON. The base URL is
    `https://api.simkl.com`.


    **Every request needs three URL parameters and a `User-Agent` header:**


    ```

    /endpoint?client_id=YOUR_CLIENT_ID&app-name=my-app&app-version=1.0

    ```


    See [Headers and required parameters](/conventions/headers) for the full
    reference. Endpoints that read or modify a user's data also need an
    `Authorization: Bearer <token>` header — see [OAuth
    2.0](/api-reference/oauth) or the [PIN flow](/api-reference/pin).


    > ⚠️ **About the auto-rendered cURL examples on each endpoint page:** they
    omit `?client_id=…&app-name=…&app-version=…` from the URL for brevity. **You
    must add them yourself when copy-pasting**, or use the interactive **"Try
    it"** playground (it fills them in for you). This is a Mintlify rendering
    convention — every Mintlify-built API doc behaves the same way.


    **Get started**


    - [Quickstart](/quickstart) — first request in 60 seconds

    - [API rules](/api-rules) — what's allowed, what isn't

    - [Standard media objects](/conventions/standard-media-objects) — the shapes
    every endpoint speaks

    - [Errors](/conventions/errors) — every status code Simkl returns
  contact:
    name: Simkl Developer Support
    url: https://support.simkl.com
  termsOfService: https://simkl.com/about/policies/terms/
  license:
    name: Simkl API Terms of Use
    url: https://simkl.com/about/policies/terms/
  x-logo:
    url: https://i.simkl.com/img_tv/apiary_logo_api.png
    backgroundColor: '#0E5DAB'
    altText: Simkl
servers:
  - url: https://api.simkl.com
    description: Production API
  - url: https://data.simkl.in
    description: CDN for static, public data files (Calendar + Trending). No auth required.
security:
  - clientId: []
  - simklApiKey: []
tags:
  - name: OAuth 2.0
    description: >-
      OAuth 2.0 authorization-code flow for web and mobile apps. The user is
      redirected to Simkl, signs in, approves your app, and is redirected back
      with a `code` you exchange for an `access_token`. Long-lived tokens — no
      refresh-token flow needed.
    externalDocs:
      description: OAuth 2.0 walkthrough
      url: https://api.simkl.org/api-reference/oauth
  - name: PIN
    description: >-
      Device flow for TVs, consoles, smart watches, CLI tools — anywhere typing
      a URL is hard. Show a 5-character code; user enters it on simkl.com/pin;
      you poll for the access token. **No `client_secret` required.**
    externalDocs:
      description: PIN flow walkthrough
      url: https://api.simkl.org/api-reference/pin
  - name: Redirect
    description: >-
      Helper redirects for one-click "mark as watched", trailers, sharing, and
      more.
  - name: Search
    description: Find shows, movies, and anime by ID, text query, file name, or randomly.
    externalDocs:
      description: Search guide
      url: https://api.simkl.org/guides/search
  - name: Movies
    description: >-
      Browse the Simkl movies catalog: details, premieres, top-of-best, and
      genre filters.
  - name: TV
    description: >-
      Browse the Simkl TV catalog: details, episodes, what's airing, premieres,
      top-of-best, and genre filters.
  - name: Anime
    description: >-
      Browse the Simkl anime catalog: details, episodes, what's airing,
      premieres, top-of-best, and genre filters.
  - name: Ratings
    description: >-
      Read Simkl's average community rating, rank, drop rate, and external
      ratings (IMDB, MAL) for any item, or for every item in a user's lists.
  - name: Scrobble
    description: >-
      Report real-time playback to Simkl with `start`, `pause`, and `stop`. At ≥
      80 % progress Simkl marks the item as watched automatically; below 80 %
      the session is saved as a paused playback the user can resume from any
      device.


      See [Standard media objects](/conventions/standard-media-objects) for the
      supported ID keys.


      <!-- IDS_TABLE_START -->


      ### Supported ID keys


      Inside any `ids` object, pass as many of these as you have. Simkl resolves
      to the canonical record.


      | ID key | Type | Example |

      |---|---|---|

      | `simkl` | integer | `49108`. Simkl's canonical ID. Most reliable. |

      | `imdb` | string | `tt1520211` |

      | `tmdb` | integer | `76757` (for TV, specify `type`) |

      | `tvdb` | int / string | `153021` or `the-walking-dead` |

      | `mal` | integer | `4246` (MyAnimeList) |

      | `anidb` | integer | `10846`. Specifying just this is enough for anime
      lookups. |

      | `anilist` | integer | `21` |

      | `kitsu` | integer | `12` |

      | `anisearch` | integer | `2227` |

      | `animeplanet` | string | `one-piece` |

      | `livechart` | integer | `321` |

      | `letterboxd` | string | `the-truman-show` |

      | `netflix` | integer | `70210890` (movie ID) |

      | `traktslug` | string | `john-wick-chapter-4-2023` |


      > 📖 Full reference: [Standard media
      objects](/conventions/standard-media-objects)


      <!-- IDS_TABLE_END -->
    externalDocs:
      description: Scrobble guide
      url: https://api.simkl.org/guides/scrobble
  - name: Sync
    description: >-
      Use Simkl as a cloud backup for the user's watch history and lists
      (Watching, Plan to Watch, On Hold, Dropped, Completed). **Always check
      [`/sync/activities`](/api-reference/simkl/get-activities) first** and sync
      only the lists that have moved.


      All write endpoints accept arrays — batch aggressively. See [Standard
      media objects](/conventions/standard-media-objects) for the supported ID
      keys.


      <!-- IDS_TABLE_START -->


      ### Supported ID keys


      Inside any `ids` object, pass as many of these as you have. Simkl resolves
      to the canonical record.


      | ID key | Type | Example |

      |---|---|---|

      | `simkl` | integer | `49108`. Simkl's canonical ID. Most reliable. |

      | `imdb` | string | `tt1520211` |

      | `tmdb` | integer | `76757` (for TV, specify `type`) |

      | `tvdb` | int / string | `153021` or `the-walking-dead` |

      | `mal` | integer | `4246` (MyAnimeList) |

      | `anidb` | integer | `10846`. Specifying just this is enough for anime
      lookups. |

      | `anilist` | integer | `21` |

      | `kitsu` | integer | `12` |

      | `anisearch` | integer | `2227` |

      | `animeplanet` | string | `one-piece` |

      | `livechart` | integer | `321` |

      | `letterboxd` | string | `the-truman-show` |

      | `netflix` | integer | `70210890` (movie ID) |

      | `traktslug` | string | `john-wick-chapter-4-2023` |


      > 📖 Full reference: [Standard media
      objects](/conventions/standard-media-objects)


      <!-- IDS_TABLE_END -->
    externalDocs:
      description: Sync guide
      url: https://api.simkl.org/guides/sync
  - name: Users
    description: User profile, settings, and watch statistics.
externalDocs:
  description: Simkl API documentation
  url: https://api.simkl.org
paths:
  /sync/history:
    post:
      tags:
        - Sync
      summary: Add to History
      description: >-
        Record watch events. The unit is the **watch event** — adding "I watched
        The Walking Dead S01E01 at 8pm" — not list membership (use [`POST
        /sync/add-to-list`](/api-reference/simkl/add-to-list) for that, or set
        `status` per-item here to do both at once).


        <Tip>

        **You don't need a Simkl ID.** Same as `/sync/add-to-list` — the server
        resolves any combination of identifiers (`imdb`, `tmdb`, `tvdb`, `mal`,
        `anidb`, `anilist`, `kitsu`) plus `title` + `year`. See [Standard media
        objects → Supported ID
        keys](/conventions/standard-media-objects#supported-id-keys) for the
        full list, and the [/sync/add-to-list ID-resolution
        table](/api-reference/simkl/add-to-list) for per-slot semantics.

        </Tip>


        #### Granularity — when does the server expand "implicit all"?


        The body shape determines whether you mark a single episode, a season,
        or a whole show.


        **One movie** — single movie completion.


        ```json

        { "movies": [{ "ids": {...} }] }

        ```


        **Whole show** — every episode marked. *"I finished this whole series."*
        Send `status: "completed"` with no `seasons` / `episodes`.


        ```json

        { "shows": [{ "ids": {...}, "status": "completed" }] }

        ```


        **Whole season** — every episode of one season. *"I finished season 2."*
        Send `seasons[]` without an inner `episodes`.


        ```json

        { "shows": [{ "ids": {...}, "seasons": [{ "number": 2 }] }] }

        ```


        **Specific episodes only** — per-event scrobbling, manual tick-off.


        ```json

        {
          "shows": [{
            "ids": {...},
            "seasons": [{
              "number": 1,
              "episodes": [{ "number": 1 }, { "number": 2 }]
            }]
          }]
        }

        ```


        **Top-level `episodes[]` shorthand** — auto-wraps to `seasons: [{
        number: 1, ... }]`. Useful for anime sequential numbering and
        single-season shows.


        ```json

        { "shows": [{ "ids": {...}, "episodes": [{ "number": 1 }] }] }

        ```


        The response always reports the actual count of episodes affected in
        `added.episodes`, so apps can verify the server expanded correctly.


        #### Memo-only updates (and "add to watchlist + memo" in one call)


        This endpoint is the **only way to set a memo on an item.** [`POST
        /sync/add-to-list`](/api-reference/simkl/add-to-list) accepts the `memo`
        field in the request body and echoes it back in the response, but **does
        not persist it** — silently discarded.


        To set or update a memo without recording a watch event, send `ids` +
        `status` + `memo`:


        ```json

        {
          "movies": [{
            "ids": { "simkl": 53536 },
            "status": "plantowatch",
            "memo": { "text": "Remind me why I added this", "is_private": true }
          }]
        }

        ```


        Two behaviors worth knowing:


        - **Memos attach to watchlist items only.** The item has to be in one of
        the five statuses (`watching` / `plantowatch` / `hold` / `dropped` /
        `completed`) for the memo to stick. Sending `status` makes this
        explicit.

        - **The endpoint auto-adds items.** If the item isn't on the user's
        watchlist yet, this same call creates the watchlist row at the specified
        `status` AND saves the memo in one shot. `added.movies` / `added.shows`
        reports the count of newly-added items (`0` when the item was already
        there and you only changed memo/status).


        To read memos back, call `GET /sync/all-items?memos=yes` — see the [Sync
        guide](/guides/sync).


        #### Per-item options


        | Field | Type | Notes |

        |---|---|---|

        | `watched_at` | ISO-8601 string | Pin the watch event to a specific
        time. Defaults to request time. |

        | `added_at` | ISO-8601 string | Override when the item was added to the
        watchlist (rarely used outside backups). |

        | `status` | string | Set the watchlist status
        (`watching`/`plantowatch`/`hold`/`completed`/`dropped`) in the same
        call. Combine with `rating` to do "watched + rate + status" in one
        request. |

        | `rating` | int 1-10 | Rate the item alongside the watch event. Same
        effect as a separate `POST /sync/ratings` call. |

        | `memo` | `{ "text": string, "is_private": bool }` | User memo, max 140
        chars. `is_private: false` shows the memo on the user's public profile +
        activity feed; `true` keeps it self-only. Read-back requires
        `/sync/all-items?memos=yes`. |

        | `is_rewatch` | bool | Force the rewatch path on this item even if the
        server can't auto-detect (used by backup/restore tools). Requires
        `?allow_rewatch=yes` query param to take effect. |

        | `use_tvdb_anime_seasons` | bool *(anime-only, optional)* | Default
        `false` (AniDB sequential — flat single-season). Set `true` to interpret
        `season`/`number` as TVDB per-season numbering. **Only needed when your
        source uses TVDB-style numbering AND the title is multi-season in TVDB**
        (e.g. Demon Slayer S2 Entertainment District). Single-season anime and
        AniDB-sequential inputs work without this flag. Use when syncing from
        Plex/Sonarr/Kodi/Jellyfin. |


        #### Rewatches


        Re-posting an already-watched episode is a **no-op by default** — the
        server detects the duplicate and skips. To insert a rewatch session,
        send `?allow_rewatch=yes` as a query parameter. The server then creates
        a separate rewatch row that doesn't double-count the original watch.


        **Simkl Pro / VIP only.** Free-tier callers with `?allow_rewatch=yes`
        get a silent no-op (`added: { movies: 0, shows: 0, episodes: 0 }`) that
        still consumes a rate-limit slot. Check `account.type` from [`POST
        /users/settings`](/api-reference/simkl/get-user-settings) at sign-in and
        gate the flag on `"pro"` / `"vip"`. Cache the value; refetch only when
        `activities.settings.all` from [`GET
        /sync/activities`](/api-reference/simkl/get-activities) bumps. Full
        pattern in [Rewatches guide → Pro / VIP gate](/guides/rewatches).


        For backup/restore tools that always want to insert (even when the
        auto-detect heuristic can't fire), set `is_rewatch: true` per-item AND
        pass the query param.


        #### Response: `added` and `not_found`


        ```json

        {
          "added": {
            "movies": <int count>,
            "shows": <int count>,
            "episodes": <int count>,
            "statuses": [
              {
                "request": { /* echo of input item, with type added */ },
                "response": {
                  "status": "completed",
                  "simkl_type": "tv" | "anime" | "movie",
                  "anime_type": "tv" | "movie" | "ova" | null
                }
              }
            ]
          },
          "not_found": {
            "movies": [...],
            "shows": [...],
            "episodes": [...]
          }
        }

        ```


        **`added.statuses[*].response.status`** is the **resolved Watchlist
        status** the server placed the item on — e.g. a `"completed"` write on a
        still-airing show is silently downgraded to `"watching"` and reflected
        here. **You don't need a follow-up [`POST
        /sync/add-to-list`](/api-reference/simkl/add-to-list)** — this call
        already moves the item; chaining would just overwrite the server's
        smarter decision.


        **`added.statuses[*].response.simkl_type`** tells you which catalog the
        item resolved to (useful when you sent ambiguous IDs — TMDB IDs can be
        either movie or tv on Simkl). Always inspect to know what got created.


        **`not_found`** carries the verbatim input for items the resolver
        couldn't match (typo, fuzzy-title miss, ID not in Simkl's catalog yet).
        Apps should:


        - Show "we couldn't track: …" UI for these

        - Offer manual ID-entry fallback

        - Don't infer success from the 201 status alone — branch on
        `not_found.movies.length === 0 && not_found.shows.length === 0 &&
        not_found.episodes.length === 0`


        #### Errors


        `400 empty_field` if a per-item required field is missing. `400
        wrong_parameter` for invalid enum values. Empty body `{}` returns 201
        with zero counts (NOT 400) — that's a known asymmetry vs
        `/scrobble/start` which 400s on empty body.


        <Card title="Sync guide — full walkthrough" icon="arrows-rotate"
        href="/guides/sync" horizontal>
         Initial-pull-then-delta-loop pattern, `date_from` semantics, deletion reconciliation, Trakt/Letterboxd migration recipes.
        </Card>


        #### When to use `/sync/history` vs `/sync/add-to-list`


        | Goal | Endpoint |

        |---|---|

        | User finished watching → mark watched + rate + memo |
        **`/sync/history`** (carries all three in one shape) |

        | User clicked "Add to Plan to Watch" button | **`/sync/add-to-list`**
        (status-only, no watch event) |

        | Backfill from Trakt/Letterboxd/IMDb (events with timestamps) |
        **`/sync/history`** with `watched_at` per item |

        | Bulk import a watchlist (no watch events) | **`/sync/add-to-list`** |

        | Remove an item from the user's library |
        **[`/sync/history/remove`](/api-reference/simkl/remove-from-history)** |


        <Tip>

        **Which IDs can I send/expect?** All accepted input identifiers and the
        keys you'll see echoed back in responses are listed at [**Standard media
        objects → Supported ID
        keys**](/conventions/standard-media-objects#supported-id-keys). Send
        every ID you have on writes — Simkl picks the first that resolves and
        ignores the rest. Reminder: `slug` is **response-only** (never send it
        on a request).

        </Tip>
      operationId: post-sync-history
      parameters:
        - $ref: '#/components/parameters/AllowRewatchQuery'
        - name: skip_auto_watching
          in: query
          required: false
          schema:
            type: string
            enum:
              - 'yes'
          description: >-
            When `yes`, suppresses the implicit episode auto-fill that happens
            when you POST a show without explicit `seasons`/`episodes`. Use when
            the client manages episode-level state itself.
        - $ref: '#/components/parameters/ClientIdQuery'
        - $ref: '#/components/parameters/AppNameQuery'
        - $ref: '#/components/parameters/AppVersionQuery'
        - $ref: '#/components/parameters/UserAgentHeader'
      requestBody:
        content:
          application/json:
            examples:
              realistic_batch:
                summary: >-
                  Realistic batch — multiple movies + shows with mixed IDs,
                  titles, years
                description: >-
                  What a tracker app typically sends after a catch-up sync — a
                  mix of media types with whatever identifiers it has on hand.
                  Some entries carry a Simkl ID directly; others use third-party
                  IDs (IMDB / TMDB / TVDB) plus `title` + `year` as fallback.
                  The server resolves every combination to a canonical Simkl
                  record and records the watch events. Use this pattern for
                  backfill from Trakt/Letterboxd/Plex/Jellyfin, where you'll
                  have a varied bag of metadata per item.
                value:
                  movies:
                    - watched_at: '2026-05-15T22:30:00Z'
                      title: 'Dune: Part Two'
                      year: 2024
                      ids:
                        simkl: 1015859
                    - watched_at: '2026-05-14T20:00:00Z'
                      title: Oppenheimer
                      year: 2023
                      ids:
                        imdb: tt15398776
                        tmdb: '872585'
                  shows:
                    - title: The Last of Us
                      year: 2023
                      ids:
                        simkl: 1411674
                      seasons:
                        - number: 1
                          episodes:
                            - number: 1
                              watched_at: '2026-05-13T19:00:00Z'
                            - number: 2
                              watched_at: '2026-05-13T20:00:00Z'
                            - number: 3
                              watched_at: '2026-05-14T18:30:00Z'
                    - title: Breaking Bad
                      year: 2008
                      ids:
                        imdb: tt0903747
                      status: completed
                      rating: 10
              add_movie_watched_no_timestamp:
                summary: Mark a movie watched right now (no `watched_at`)
                description: >-
                  Minimal shape. Server records the watch event at request time.
                  Use this for live UIs where the user just clicked Watched.
                value:
                  movies:
                    - ids:
                        simkl: 752138
              add_movie_watched_with_timestamp:
                summary: Mark a movie watched at a specific UTC time
                description: >-
                  Pin the watch event to an exact moment. Use this for
                  backfilling history from another tracker (Trakt, IMDb
                  watchlist, etc.). `watched_at` is ISO-8601 with `Z` suffix.
                  Omit it to default to the time of the request.
                value:
                  movies:
                    - watched_at: '2026-05-01T20:00:00Z'
                      ids:
                        simkl: 752138
              mark_top_level_episodes_shorthand:
                summary: 'GRANULARITY: top-level episodes[] (auto-wrapped to season 1)'
                description: >-
                  Shorthand for the common case of single-season targeting (or
                  anime AniDB sequential numbering). The server auto-wraps to
                  `seasons: [{number: 1, episodes: [...]}]`.
                value:
                  shows:
                    - ids:
                        simkl: 2090
                      episodes:
                        - number: 1
                        - number: 2
              add_show_episodes_via_seasons:
                summary: Mark specific TV episodes watched (S01E01-E02)
                description: >-
                  Targeted episode-level history. The `seasons` array carries
                  one entry per season with an `episodes` array inside. Each
                  episode can carry its own `watched_at`. Use this for
                  fine-grained backfill or for marking re-watched episodes.
                value:
                  shows:
                    - ids:
                        simkl: 2090
                      seasons:
                        - number: 1
                          episodes:
                            - number: 1
                              watched_at: '2026-05-01T20:00:00Z'
                            - number: 2
                              watched_at: '2026-05-01T21:00:00Z'
              mark_specific_episodes_explicit:
                summary: 'GRANULARITY: mark specific episodes — seasons[].episodes[]'
                description: >-
                  Episode-level targeting. Each episode can carry its own
                  `watched_at` for accurate per-event timestamps. Use this when
                  syncing from a player that emits one event per episode.
                value:
                  shows:
                    - ids:
                        simkl: 2090
                      seasons:
                        - number: 1
                          episodes:
                            - number: 1
                            - number: 2
                            - number: 3
              mark_whole_season_no_episodes:
                summary: >-
                  GRANULARITY: mark whole season — seasons[{number:N}] with no
                  episodes[]
                description: >-
                  Mark every episode of season N watched without enumerating
                  them. Each season entry without an `episodes` array is
                  expanded by the server. Useful when a user marks 'I finished
                  season 3' in one click. Combine multiple seasons in the array
                  to mark several at once.
                value:
                  shows:
                    - ids:
                        simkl: 2090
                      seasons:
                        - number: 1
              add_show_full_season:
                summary: Mark an entire TV season watched (season 1, all episodes)
                description: >-
                  Shorthand for marking every episode of a season. Omit the
                  `episodes` array; the server enumerates the season's episodes
                  automatically. Watch-time defaults to request time for each
                  episode unless you pass a `watched_at` on the season entry.
                value:
                  shows:
                    - ids:
                        simkl: 2090
                      seasons:
                        - number: 1
              mark_whole_show_completed:
                summary: >-
                  GRANULARITY: mark whole show — no seasons/episodes +
                  status=completed
                description: >-
                  Most efficient way to mark an entire show watched: omit
                  `seasons[]` AND `episodes[]`, set `status: "completed"`. The
                  server iterates every episode of the series and marks them all
                  watched. Response's `added.episodes` is the total count.
                value:
                  shows:
                    - title: The Walking Dead
                      year: 2010
                      ids:
                        simkl: 2090
                      status: completed
              mixed_types_one_call:
                summary: >-
                  Mixed types — a movie + a TV episode + an anime episode (anime
                  under `shows[]`)
                description: >-
                  Tracker apps batch catch-up syncs by sending every
                  newly-watched item in one call. **Anime entries sit under
                  `shows[]`, not `anime[]`** — Simkl resolves to the anime
                  catalog automatically via the IDs (or via fuzzy title+year).
                  The `anime[]` key still works for backwards compatibility but
                  the cleanest pattern is one `shows[]` array for both TV and
                  anime. The response's `added.statuses[]` array echoes each
                  input with the resolved `simkl_type` (`movie` / `tv` /
                  `anime`) and the watchlist status the item ended up in —
                  `anime_type` (`tv` / `movie` / `ova` / `null`) further
                  disambiguates anime catalog entries.
                value:
                  movies:
                    - ids:
                        simkl: 472214
                  shows:
                    - ids:
                        simkl: 2090
                      seasons:
                        - number: 1
                          episodes:
                            - number: 1
                    - ids:
                        simkl: 831411
                        mal: 38000
                        anidb: 14116
                        anilist: 101922
                      seasons:
                        - number: 1
                          episodes:
                            - number: 1
              history_rating_plus_status_combined:
                summary: 'Combined: mark watched + set status + rate in one request'
                description: >-
                  Per-item `rating` (1-10) is processed by the same handler as
                  `/sync/ratings`. Combine with `status` to do all three actions
                  in a single round-trip — common for 'I just finished this and
                  want to rate it' UI flows.passes `rating` to.
                value:
                  shows:
                    - ids:
                        simkl: 2090
                      status: completed
                      rating: 9
              history_with_private_memo:
                summary: Mark watched + attach a PRIVATE memo (self-only notes)
                description: >-
                  Same shape as a public memo, but `is_private: true` keeps the
                  text visible only to the owning user. Use for personal notes,
                  rewatch reminders, spoilers, etc. Read-back requires
                  `/sync/all-items?memos=yes` (read-side gating).
                value:
                  shows:
                    - ids:
                        simkl: 2090
                      status: completed
                      memo:
                        text: Skip S7 next time, too slow
                        is_private: true
              history_with_public_memo:
                summary: Mark watched + attach a PUBLIC memo (visible to other users)
                description: >-
                  `memo` is a per-item field with two sub-fields: `text` (max
                  140 chars) and `is_private` (boolean). `is_private: false`
                  makes the memo visible on the user's public Simkl profile and
                  in the activity feed. Use for spoiler-free reactions and
                  recommendations.
                value:
                  shows:
                    - ids:
                        simkl: 2090
                      status: completed
                      memo:
                        text: Loved every season — best zombie show
                        is_private: false
              add_anime_episode:
                summary: Mark an anime episode watched
                description: >-
                  Anime use the same `shows` body shape as TV — AniDB numbering
                  is single-season, so `seasons[0].number = 1` always. The
                  catalog ID tells the server which catalog (anime vs TV) to
                  dispatch to.
                value:
                  shows:
                    - ids:
                        simkl: 831411
                        mal: 38000
                        anidb: 14116
                        anilist: 101922
                      seasons:
                        - number: 1
                          episodes:
                            - number: 1
              anime_use_tvdb_seasons_flag:
                summary: >-
                  ANIME (Plex/Sonarr): `use_tvdb_anime_seasons: true` with
                  TVDB-only IDs
                description: >-
                  Anime have two parallel numbering schemes: **AniDB
                  sequential** (flat single-season — episode 27 of Demon Slayer
                  S1 in AniDB terms is actually S2E1 in TVDB terms) and **TVDB
                  per-season**. Players that source metadata from TheTVDB —
                  Plex, Sonarr, Kodi, Jellyfin — only have the TVDB shape and
                  typically lack `mal`/`anidb`/`anilist` IDs. Pass
                  `use_tvdb_anime_seasons: true` and Simkl maps your TVDB-style
                  `season`+`number` to the canonical AniDB record. **Note:**
                  this example deliberately uses only TVDB+title+year (no
                  anime-only IDs) to model the real Plex/Sonarr scenario.
                value:
                  shows:
                    - title: 'Demon Slayer: Kimetsu no Yaiba'
                      year: 2019
                      ids:
                        tvdb: '359476'
                      use_tvdb_anime_seasons: true
                      seasons:
                        - number: 2
                          episodes:
                            - number: 5
                              watched_at: '2026-05-16T22:00:00Z'
              rewatch_explicit_flag:
                summary: >-
                  Rewatch — explicit `is_rewatch: true` per-item flag on a SHOW
                  (Simkl Pro / VIP)
                description: >-
                  Set `is_rewatch: true` on the item AND pass
                  `?allow_rewatch=yes` to force the rewatch path. The VIP
                  response includes `rewatch_id` and `rewatch_status` in
                  `added.statuses[].response`. **Pin the `rewatch_id` on every
                  subsequent write for the same session** to keep multi-call
                  writes from forking into separate sessions — see the
                  [Rewatches guide](/guides/rewatches).
                value:
                  shows:
                    - ids:
                        simkl: 17465
                      is_rewatch: true
                      seasons:
                        - number: 1
                          episodes:
                            - number: 1
                            - number: 2
              rewatch_with_allow_rewatch_query:
                summary: >-
                  Rewatch — re-post episode with `?allow_rewatch=yes` on a
                  previously-completed ANIME (Simkl Pro / VIP)
                description: >-
                  Without `?allow_rewatch=yes`, re-posting a watched episode is
                  a no-op. WITH the query param (and a VIP account), the server
                  creates a rewatch session and the episode counts again. The
                  response includes `rewatch_id` (cache it for subsequent writes
                  in this session) and `rewatch_status`
                  (`active`/`completed`/`closed`).
                value:
                  shows:
                    - ids:
                        simkl: 37089
                        mal: 1
                        anidb: 23
                        anilist: 1
                      is_rewatch: true
                      seasons:
                        - number: 1
                          episodes:
                            - number: 1
                            - number: 2
              rewatch_max_info:
                summary: >-
                  Rewatch — maximum info (every rewatch field populated, all
                  wired to a real session) (Simkl Pro / VIP)
                description: >-
                  Every per-item rewatch field set: `is_rewatch:true`,
                  `rewatch_id` (to resume a specific session), `rewatch_status`
                  (to explicitly close or reactivate it — for TV / anime,
                  `"completed"` is clamped to `"closed"`; the server promotes
                  back to `"completed"` once coverage of every aired regular
                  episode lands), per-episode `watched_at`, plus the standard
                  `rating` and `memo: {text, is_private}` fields. This is what a
                  tracker app's *"close out this rewatch"* button would send.


                  The response echoes every field you set in
                  `added.statuses[].request` AND surfaces the resulting
                  `rewatch_id` + `rewatch_status` in `added.statuses[].response`
                  (so you can confirm the session state Simkl ended up with —
                  useful when you let the server pick the state via
                  auto-transitions).
                value:
                  shows:
                    - ids:
                        simkl: 17465
                      is_rewatch: true
                      rewatch_id: 7482
                      rewatch_status: closed
                      rating: 10
                      memo:
                        text: Rewatched the Stark family arcs ahead of the new book.
                        is_private: false
                      seasons:
                        - number: 1
                          episodes:
                            - number: 3
                              watched_at: '2026-05-16T19:00:00Z'
                            - number: 4
                              watched_at: '2026-05-16T20:00:00Z'
              history_partial_with_not_found:
                summary: 'PARTIAL SUCCESS: unresolvable item lands in not_found.movies'
                description: >-
                  When the server's resolver can't match an item (typo, fuzzy
                  miss, ID not in catalog yet), the request still returns 201 —
                  but the item lands in `response.not_found.<media_type>`
                  instead of `response.added.statuses`. The not_found entries
                  are verbatim copies of the input, so apps can show 'we
                  couldn't add: …' UI and offer manual resolution.
                value:
                  movies:
                    - title: ZZZ-Definitely-Not-A-Real-Movie-Title-XYZ
                      year: 9999
              quirk_empty_body_201_zero:
                summary: 'EDGE CASE: empty body returns 201, not 400'
                description: >-
                  `POST /sync/history` with `{}` does not trigger validation —
                  the server returns `201` with zero counts. Only malformed JSON
                  (unparseable) triggers the 400 `json_error` envelope. Pin this
                  behavior in your client: don't infer success from 200 alone,
                  branch on `response.added > 0` if you care.
                value: {}
            schema:
              $ref: '#/components/schemas/HistoryRequest'
        required: true
      responses:
        '201':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HistoryAddResponse'
              examples:
                realistic_batch:
                  summary: >-
                    Realistic batch — multiple movies + shows with mixed IDs,
                    titles, years
                  description: >-
                    What a tracker app typically sends after a catch-up sync — a
                    mix of media types with whatever identifiers it has on hand.
                    Some entries carry a Simkl ID directly; others use
                    third-party IDs (IMDB / TMDB / TVDB) plus `title` + `year`
                    as fallback. The server resolves every combination to a
                    canonical Simkl record and records the watch events. Use
                    this pattern for backfill from
                    Trakt/Letterboxd/Plex/Jellyfin, where you'll have a varied
                    bag of metadata per item.
                  value:
                    added:
                      movies: 2
                      shows: 2
                      episodes: 65
                      statuses:
                        - request:
                            watched_at: '2026-05-15T22:30:00Z'
                            title: 'Dune: Part Two'
                            year: 2024
                            ids:
                              simkl: 1015859
                            type: movie
                            rating: null
                          response:
                            status: completed
                            simkl_type: movie
                            anime_type: null
                        - request:
                            watched_at: '2026-05-14T20:00:00Z'
                            title: Oppenheimer
                            year: 2023
                            ids:
                              imdb: tt15398776
                              tmdb: '872585'
                            type: movie
                            rating: null
                          response:
                            status: completed
                            simkl_type: movie
                            anime_type: null
                        - request:
                            title: The Last of Us
                            year: 2023
                            ids:
                              simkl: 1411674
                            type: show
                            rating: null
                          response:
                            status: watching
                            simkl_type: tv
                            anime_type: null
                        - request:
                            title: Breaking Bad
                            year: 2008
                            ids:
                              imdb: tt0903747
                            status: completed
                            rating: 10
                            type: show
                          response:
                            status: completed
                            simkl_type: tv
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                add_movie_watched_no_timestamp:
                  summary: Mark a movie watched right now (no `watched_at`)
                  description: >-
                    Minimal shape. Server records the watch event at request
                    time. Use this for live UIs where the user just clicked
                    Watched.
                  value:
                    added:
                      movies: 1
                      shows: 0
                      episodes: 0
                      statuses:
                        - request:
                            ids:
                              simkl: 752138
                            type: movie
                            rating: null
                          response:
                            status: completed
                            simkl_type: movie
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                add_movie_watched_with_timestamp:
                  summary: Mark a movie watched at a specific UTC time
                  description: >-
                    Pin the watch event to an exact moment. Use this for
                    backfilling history from another tracker (Trakt, IMDb
                    watchlist, etc.). `watched_at` is ISO-8601 with `Z` suffix.
                    Omit it to default to the time of the request.
                  value:
                    added:
                      movies: 1
                      shows: 0
                      episodes: 0
                      statuses:
                        - request:
                            watched_at: '2026-05-01T20:00:00Z'
                            ids:
                              simkl: 752138
                            type: movie
                            rating: null
                          response:
                            status: completed
                            simkl_type: movie
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                mark_top_level_episodes_shorthand:
                  summary: 'GRANULARITY: top-level episodes[] (auto-wrapped to season 1)'
                  description: >-
                    Shorthand for the common case of single-season targeting (or
                    anime AniDB sequential numbering). The server auto-wraps to
                    `seasons: [{number: 1, episodes: [...]}]`.
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 2
                      statuses:
                        - request:
                            ids:
                              simkl: 2090
                            type: show
                            rating: null
                          response:
                            status: watching
                            simkl_type: tv
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                add_show_episodes_via_seasons:
                  summary: Mark specific TV episodes watched (S01E01-E02)
                  description: >-
                    Targeted episode-level history. The `seasons` array carries
                    one entry per season with an `episodes` array inside. Each
                    episode can carry its own `watched_at`. Use this for
                    fine-grained backfill or for marking re-watched episodes.
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 2
                      statuses:
                        - request:
                            ids:
                              simkl: 2090
                            type: show
                            rating: null
                          response:
                            status: watching
                            simkl_type: tv
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                mark_specific_episodes_explicit:
                  summary: 'GRANULARITY: mark specific episodes — seasons[].episodes[]'
                  description: >-
                    Episode-level targeting. Each episode can carry its own
                    `watched_at` for accurate per-event timestamps. Use this
                    when syncing from a player that emits one event per episode.
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 3
                      statuses:
                        - request:
                            ids:
                              simkl: 2090
                            type: show
                            rating: null
                          response:
                            status: watching
                            simkl_type: tv
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                mark_whole_season_no_episodes:
                  summary: >-
                    GRANULARITY: mark whole season — seasons[{number:N}] with no
                    episodes[]
                  description: >-
                    Mark every episode of season N watched without enumerating
                    them. Each season entry without an `episodes` array is
                    expanded by the server. Useful when a user marks 'I finished
                    season 3' in one click. Combine multiple seasons in the
                    array to mark several at once.
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 6
                      statuses:
                        - request:
                            ids:
                              simkl: 2090
                            type: show
                            rating: null
                          response:
                            status: watching
                            simkl_type: tv
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                add_show_full_season:
                  summary: Mark an entire TV season watched (season 1, all episodes)
                  description: >-
                    Shorthand for marking every episode of a season. Omit the
                    `episodes` array; the server enumerates the season's
                    episodes automatically. Watch-time defaults to request time
                    for each episode unless you pass a `watched_at` on the
                    season entry.
                  value:
                    added:
                      movies: 0
                      shows: 0
                      episodes: 6
                      statuses:
                        - request:
                            ids:
                              simkl: 2090
                            type: show
                            rating: null
                          response:
                            status: watching
                            simkl_type: tv
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                mark_whole_show_completed:
                  summary: >-
                    GRANULARITY: mark whole show — no seasons/episodes +
                    status=completed
                  description: >-
                    Most efficient way to mark an entire show watched: omit
                    `seasons[]` AND `episodes[]`, set `status: "completed"`. The
                    server iterates every episode of the series and marks them
                    all watched. Response's `added.episodes` is the total count.
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 177
                      statuses:
                        - request:
                            title: The Walking Dead
                            year: 2010
                            ids:
                              simkl: 2090
                            status: completed
                            type: show
                            rating: null
                          response:
                            status: completed
                            simkl_type: tv
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                mixed_types_one_call:
                  summary: >-
                    Mixed types — a movie + a TV episode + an anime episode
                    (anime under `shows[]`)
                  description: >-
                    Tracker apps batch catch-up syncs by sending every
                    newly-watched item in one call. **Anime entries sit under
                    `shows[]`, not `anime[]`** — Simkl resolves to the anime
                    catalog automatically via the IDs (or via fuzzy title+year).
                    The `anime[]` key still works for backwards compatibility
                    but the cleanest pattern is one `shows[]` array for both TV
                    and anime. The response's `added.statuses[]` array echoes
                    each input with the resolved `simkl_type` (`movie` / `tv` /
                    `anime`) and the watchlist status the item ended up in —
                    `anime_type` (`tv` / `movie` / `ova` / `null`) further
                    disambiguates anime catalog entries.
                  value:
                    added:
                      movies: 1
                      shows: 2
                      episodes: 2
                      statuses:
                        - request:
                            ids:
                              simkl: 472214
                            type: movie
                            rating: null
                          response:
                            status: completed
                            simkl_type: movie
                            anime_type: null
                        - request:
                            ids:
                              simkl: 2090
                            type: show
                            rating: null
                          response:
                            status: completed
                            simkl_type: tv
                            anime_type: null
                        - request:
                            ids:
                              simkl: 831411
                              mal: 38000
                              anidb: 14116
                              anilist: 101922
                            type: show
                            rating: null
                          response:
                            status: watching
                            simkl_type: anime
                            anime_type: tv
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                history_rating_plus_status_combined:
                  summary: 'Combined: mark watched + set status + rate in one request'
                  description: >-
                    Per-item `rating` (1-10) is processed by the same handler as
                    `/sync/ratings`. Combine with `status` to do all three
                    actions in a single round-trip — common for 'I just finished
                    this and want to rate it' UI flows.passes `rating` to.
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 177
                      statuses:
                        - request:
                            ids:
                              simkl: 2090
                            status: completed
                            rating: 9
                            type: show
                          response:
                            status: completed
                            simkl_type: tv
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                history_with_private_memo:
                  summary: Mark watched + attach a PRIVATE memo (self-only notes)
                  description: >-
                    Same shape as a public memo, but `is_private: true` keeps
                    the text visible only to the owning user. Use for personal
                    notes, rewatch reminders, spoilers, etc. Read-back requires
                    `/sync/all-items?memos=yes` (read-side gating).
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 177
                      statuses:
                        - request:
                            ids:
                              simkl: 2090
                            status: completed
                            memo:
                              text: Skip S7 next time, too slow
                              is_private: true
                            type: show
                            rating: null
                          response:
                            status: completed
                            simkl_type: tv
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                history_with_public_memo:
                  summary: Mark watched + attach a PUBLIC memo (visible to other users)
                  description: >-
                    `memo` is a per-item field with two sub-fields: `text` (max
                    140 chars) and `is_private` (boolean). `is_private: false`
                    makes the memo visible on the user's public Simkl profile
                    and in the activity feed. Use for spoiler-free reactions and
                    recommendations.
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 177
                      statuses:
                        - request:
                            ids:
                              simkl: 2090
                            status: completed
                            memo:
                              text: Loved every season — best zombie show
                              is_private: false
                            type: show
                            rating: null
                          response:
                            status: completed
                            simkl_type: tv
                            anime_type: null
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                add_anime_episode:
                  summary: Mark an anime episode watched
                  description: >-
                    Anime use the same `shows` body shape as TV — AniDB
                    numbering is single-season, so `seasons[0].number = 1`
                    always. The catalog ID tells the server which catalog (anime
                    vs TV) to dispatch to.
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 1
                      statuses:
                        - request:
                            ids:
                              simkl: 831411
                              mal: 38000
                              anidb: 14116
                              anilist: 101922
                            type: show
                            rating: null
                          response:
                            status: watching
                            simkl_type: anime
                            anime_type: tv
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                anime_use_tvdb_seasons_flag:
                  summary: >-
                    ANIME (Plex/Sonarr): `use_tvdb_anime_seasons: true` with
                    TVDB-only IDs
                  description: >-
                    Anime have two parallel numbering schemes: **AniDB
                    sequential** (flat single-season) and **TVDB per-season**.
                    Players that source metadata from TheTVDB — Plex, Sonarr,
                    Kodi, Jellyfin — only have the TVDB shape and typically lack
                    `mal`/`anidb`/`anilist` IDs. Pass `use_tvdb_anime_seasons:
                    true` and Simkl maps your TVDB-style `season`+`number` to
                    the canonical AniDB record. This example deliberately uses
                    only TVDB+title+year (no anime-only IDs) to model the real
                    Plex/Sonarr scenario.
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 1
                      statuses:
                        - request:
                            title: 'Demon Slayer: Kimetsu no Yaiba'
                            year: 2019
                            ids:
                              tvdb: '359476'
                              tvdbslug: demon-slayer-kimetsu-no-yaiba
                            use_tvdb_anime_seasons: true
                            type: show
                            rating: null
                          response:
                            status: watching
                            simkl_type: anime
                            anime_type: tv
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                rewatch_explicit_flag:
                  summary: >-
                    Rewatch — explicit `is_rewatch: true` per-item flag on a
                    SHOW (Simkl Pro / VIP)
                  description: >-
                    Set `is_rewatch: true` on the item AND pass
                    `?allow_rewatch=yes` to force the rewatch path. The VIP
                    response includes `rewatch_id` and `rewatch_status` in
                    `added.statuses[].response`. **Pin the `rewatch_id` on every
                    subsequent write for the same session** to keep multi-call
                    writes from forking into separate sessions — see the
                    [Rewatches guide](/guides/rewatches).
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 2
                      statuses:
                        - request:
                            ids:
                              simkl: 17465
                            is_rewatch: true
                            type: show
                            rating: null
                          response:
                            status: 3
                            simkl_type: tv
                            anime_type: null
                            rewatch_id: 7482
                            rewatch_status: active
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                rewatch_with_allow_rewatch_query:
                  summary: >-
                    Rewatch — re-post episode with `?allow_rewatch=yes` on a
                    previously-completed ANIME (Simkl Pro / VIP)
                  description: >-
                    Without `?allow_rewatch=yes`, re-posting a watched episode
                    is a no-op. WITH the query param (and a VIP account), the
                    server creates a rewatch session and the episode counts
                    again. The response includes `rewatch_id` (cache it for
                    subsequent writes in this session) and `rewatch_status`
                    (`active`/`completed`/`closed`).
                  value:
                    added:
                      movies: 0
                      shows: 1
                      episodes: 2
                      statuses:
                        - request:
                            ids:
                              simkl: 37089
                              mal: 1
                              anidb: 23
                              anilist: 1
                            is_rewatch: true
                            type: show
                            rating: null
                          response:
                            status: 3
                            simkl_type: anime
                            anime_type: tv
                            rewatch_id: 7484
                            rewatch_status: active
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                rewatch_max_info:
                  summary: >-
                    Rewatch — maximum info (every rewatch field populated, all
                    wired to a real session) (Simkl Pro / VIP)
                  description: >-
                    Every per-item rewatch field set: `is_rewatch:true`,
                    `rewatch_id` (to resume a specific session),
                    `rewatch_status` (to explicitly close/complete/reactivate
                    it), per-episode `watched_at`, plus the standard `rating`
                    and `memo: {text, is_private}` fields. This is what a
                    tracker app's *"finished my rewatch"* button would send.


                    The response echoes every field you set in
                    `added.statuses[].request` AND surfaces the resulting
                    `rewatch_id` + `rewatch_status` in
                    `added.statuses[].response` (so you can confirm the session
                    state Simkl ended up with — useful when you let the server
                    pick the state via auto-transitions).
                  value:
                    added:
                      movies: 0
                      shows: 0
                      episodes: 2
                      statuses:
                        - request:
                            ids:
                              simkl: 17465
                            is_rewatch: true
                            rewatch_id: 7482
                            rewatch_status: closed
                            rating: 10
                            memo:
                              text: >-
                                Rewatched the Stark family arcs ahead of the new
                                book.
                              is_private: false
                            type: show
                          response:
                            status: 3
                            simkl_type: tv
                            anime_type: null
                            rewatch_id: 7482
                            rewatch_status: closed
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
                history_partial_with_not_found:
                  summary: 'PARTIAL SUCCESS: unresolvable item lands in not_found.movies'
                  description: >-
                    When the server's resolver can't match an item (typo, fuzzy
                    miss, ID not in catalog yet), the request still returns 201
                    — but the item lands in `response.not_found.<media_type>`
                    instead of `response.added.statuses`. The not_found entries
                    are verbatim copies of the input, so apps can show 'we
                    couldn't add: …' UI and offer manual resolution.
                  value:
                    added:
                      movies: 0
                      shows: 0
                      episodes: 0
                      statuses: []
                    not_found:
                      movies:
                        - title: ZZZ-Definitely-Not-A-Real-Movie-Title-XYZ
                          year: 9999
                          rating: null
                      shows: []
                      episodes: []
                quirk_empty_body_201_zero:
                  summary: 'EDGE CASE: empty body returns 201, not 400'
                  description: >-
                    `POST /sync/history` with `{}` does not trigger validation —
                    the server returns `201` with zero counts. Only malformed
                    JSON (unparseable) triggers the 400 `json_error` envelope.
                    Pin this behavior in your client: don't infer success from
                    200 alone, branch on `response.added > 0` if you care.
                  value:
                    added:
                      movies: 0
                      shows: 0
                      episodes: 0
                      statuses: []
                    not_found:
                      movies: []
                      shows: []
                      episodes: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '412':
          $ref: '#/components/responses/ClientIdFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - clientId: []
          bearerAuth: []
        - simklApiKey: []
          bearerAuth: []
      x-codeSamples:
        - lang: curl
          label: realistic_batch
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "movies": [
                {
                  "watched_at": "2026-05-15T22:30:00Z",
                  "title": "Dune: Part Two",
                  "year": 2024,
                  "ids": {
                    "simkl": 1015859
                  }
                },
                {
                  "watched_at": "2026-05-14T20:00:00Z",
                  "title": "Oppenheimer",
                  "year": 2023,
                  "ids": {
                    "imdb": "tt15398776",
                    "tmdb": "872585"
                  }
                }
              ],
              "shows": [
                {
                  "title": "The Last of Us",
                  "year": 2023,
                  "ids": {
                    "simkl": 1411674
                  },
                  "seasons": [
                    {
                      "number": 1,
                      "episodes": [
                        {
                          "number": 1,
                          "watched_at": "2026-05-13T19:00:00Z"
                        },
                        {
                          "number": 2,
                          "watched_at": "2026-05-13T20:00:00Z"
                        },
                        {
                          "number": 3,
                          "watched_at": "2026-05-14T18:30:00Z"
                        }
                      ]
                    }
                  ]
                },
                {
                  "title": "Breaking Bad",
                  "year": 2008,
                  "ids": {
                    "imdb": "tt0903747"
                  },
                  "status": "completed",
                  "rating": 10
                }
              ]
            }'
        - lang: curl
          label: add_movie_watched_no_timestamp
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "movies": [
                {
                  "ids": {
                    "simkl": 752138
                  }
                }
              ]
            }'
        - lang: curl
          label: add_movie_watched_with_timestamp
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "movies": [
                {
                  "watched_at": "2026-05-01T20:00:00Z",
                  "ids": {
                    "simkl": 752138
                  }
                }
              ]
            }'
        - lang: curl
          label: mark_top_level_episodes_shorthand
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 2090
                  },
                  "episodes": [
                    {
                      "number": 1
                    },
                    {
                      "number": 2
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: add_show_episodes_via_seasons
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 2090
                  },
                  "seasons": [
                    {
                      "number": 1,
                      "episodes": [
                        {
                          "number": 1,
                          "watched_at": "2026-05-01T20:00:00Z"
                        },
                        {
                          "number": 2,
                          "watched_at": "2026-05-01T21:00:00Z"
                        }
                      ]
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: mark_specific_episodes_explicit
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 2090
                  },
                  "seasons": [
                    {
                      "number": 1,
                      "episodes": [
                        {
                          "number": 1
                        },
                        {
                          "number": 2
                        },
                        {
                          "number": 3
                        }
                      ]
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: mark_whole_season_no_episodes
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 2090
                  },
                  "seasons": [
                    {
                      "number": 1
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: add_show_full_season
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 2090
                  },
                  "seasons": [
                    {
                      "number": 1
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: mark_whole_show_completed
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "title": "The Walking Dead",
                  "year": 2010,
                  "ids": {
                    "simkl": 2090
                  },
                  "status": "completed"
                }
              ]
            }'
        - lang: curl
          label: mixed_types_one_call
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "movies": [
                {
                  "ids": {
                    "simkl": 472214
                  }
                }
              ],
              "shows": [
                {
                  "ids": {
                    "simkl": 2090
                  },
                  "seasons": [
                    {
                      "number": 1,
                      "episodes": [
                        {
                          "number": 1
                        }
                      ]
                    }
                  ]
                },
                {
                  "ids": {
                    "simkl": 831411,
                    "mal": 38000,
                    "anidb": 14116,
                    "anilist": 101922
                  },
                  "seasons": [
                    {
                      "number": 1,
                      "episodes": [
                        {
                          "number": 1
                        }
                      ]
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: history_rating_plus_status_combined
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 2090
                  },
                  "status": "completed",
                  "rating": 9
                }
              ]
            }'
        - lang: curl
          label: history_with_private_memo
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 2090
                  },
                  "status": "completed",
                  "memo": {
                    "text": "Skip S7 next time, too slow",
                    "is_private": true
                  }
                }
              ]
            }'
        - lang: curl
          label: history_with_public_memo
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 2090
                  },
                  "status": "completed",
                  "memo": {
                    "text": "Loved every season — best zombie show",
                    "is_private": false
                  }
                }
              ]
            }'
        - lang: curl
          label: add_anime_episode
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 831411,
                    "mal": 38000,
                    "anidb": 14116,
                    "anilist": 101922
                  },
                  "seasons": [
                    {
                      "number": 1,
                      "episodes": [
                        {
                          "number": 1
                        }
                      ]
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: anime_use_tvdb_seasons_flag
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "title": "Demon Slayer: Kimetsu no Yaiba",
                  "year": 2019,
                  "ids": {
                    "tvdb": "359476",
                    "tvdbslug": "demon-slayer-kimetsu-no-yaiba"
                  },
                  "use_tvdb_anime_seasons": true,
                  "seasons": [
                    {
                      "number": 2,
                      "episodes": [
                        {
                          "number": 5,
                          "watched_at": "2026-05-16T22:00:00Z"
                        }
                      ]
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: rewatch_explicit_flag
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?allow_rewatch=yes&client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 17465
                  },
                  "is_rewatch": true,
                  "seasons": [
                    {
                      "number": 1,
                      "episodes": [
                        {
                          "number": 1
                        },
                        {
                          "number": 2
                        }
                      ]
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: rewatch_with_allow_rewatch_query
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?allow_rewatch=yes&client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 37089,
                    "mal": 1,
                    "anidb": 23,
                    "anilist": 1
                  },
                  "is_rewatch": true,
                  "seasons": [
                    {
                      "number": 1,
                      "episodes": [
                        {
                          "number": 1
                        },
                        {
                          "number": 2
                        }
                      ]
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: rewatch_max_info
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?allow_rewatch=yes&client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "shows": [
                {
                  "ids": {
                    "simkl": 17465
                  },
                  "is_rewatch": true,
                  "rewatch_id": 7482,
                  "rewatch_status": "completed",
                  "rating": 10,
                  "memo": {
                    "text": "Rewatched the Stark family arcs ahead of the new book.",
                    "is_private": false
                  },
                  "seasons": [
                    {
                      "number": 1,
                      "episodes": [
                        {
                          "number": 3,
                          "watched_at": "2026-05-16T19:00:00Z"
                        },
                        {
                          "number": 4,
                          "watched_at": "2026-05-16T20:00:00Z"
                        }
                      ]
                    }
                  ]
                }
              ]
            }'
        - lang: curl
          label: history_partial_with_not_found
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{
              "movies": [
                {
                  "title": "ZZZ-Definitely-Not-A-Real-Movie-Title-XYZ",
                  "year": 9999
                }
              ]
            }'
        - lang: curl
          label: quirk_empty_body_201_zero
          source: >-
            curl -X POST
            'https://api.simkl.com/sync/history?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0'
            \
              -H 'User-Agent: my-app-name/1.0' \
              -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
              -H 'Content-Type: application/json' \
              -d '{}'
components:
  parameters:
    AllowRewatchQuery:
      name: allow_rewatch
      in: query
      required: false
      schema:
        type: string
        enum:
          - 'yes'
          - 'no'
        default: 'no'
      example: 'yes'
      description: >-
        Opt into rewatch tracking. When `yes`, `POST /sync/history` records an
        additional rewatch session instead of being a no-op for already-watched
        items, and `GET /sync/all-items` returns one extra entry per saved
        rewatch session alongside the item's normal entry. Available to Simkl
        **Pro** and **VIP** users — non-Pro callers see no effect even with the
        flag set.


        ⚠️ **Do not enable this flag until you've read the [Rewatches
        guide](/guides/rewatches) end-to-end and implemented the precautions.**
        Used carelessly (on retries, on every scrobble event, on importer
        re-runs, without pinning `rewatch_id` after the first write), it will
        pollute the user's history stats and rewatches panel with phantom
        sessions. The flag should be gated behind explicit user intent — a
        dedicated "Rewatch" button — never on background or automated flows.
        Also expose a per-user *Track rewatches* toggle in your app's settings
        (default off) — not every user wants the rewatch-session complexity.


        Limits: up to **50 rewatches per item** (movie, show, or anime), and any
        two watch events on the **same item** (movie or episode) must be at
        least **2 days apart** — a new rewatch within 48 hours of the previous
        watch of that same item collapses into the same session (it's a rewatch,
        not a rewind 😄). Full walkthrough — session lifecycle (`active` /
        `completed` / `closed` with bidirectional transitions), episode-level
        tracking, reading sessions back from `GET /sync/all-items`, and
        ready-made code for simkl.com-style UI patterns — in the [Rewatches
        guide](/guides/rewatches).
    ClientIdQuery:
      name: client_id
      in: query
      required: true
      description: >-
        Your **`client_id`** from your [Simkl developer
        settings](https://simkl.com/settings/developer/). Required on every
        request.
      schema:
        type: string
      example: YOUR_CLIENT_ID
    AppNameQuery:
      name: app-name
      in: query
      required: true
      description: >-
        Short, lowercase identifier for your app (e.g. `plex-scrobbler`,
        `kodi-bridge`). Helps Simkl identify which apps are using the API.
      schema:
        type: string
      example: my-app
    AppVersionQuery:
      name: app-version
      in: query
      required: true
      description: >-
        Your app's current version (e.g. `1.0`, `2.4.1`). Helps Simkl debug
        issues you report.
      schema:
        type: string
      example: '1.0'
    UserAgentHeader:
      name: User-Agent
      in: header
      required: true
      description: >-
        Descriptive identifier for your app, ideally `name/version`. Examples:
        `PlexMediaServer/1.43.1.10540`, `kodi-simkl/0.9.2`, `MyApp/2.4.1
        (https://myapp.com)`.
      schema:
        type: string
      example: my-app/1.0
  schemas:
    HistoryRequest:
      type: object
      description: >-
        Body for `/sync/history` and `/sync/history/remove`. Items go under
        `movies[]`, `shows[]`, or `anime[]` — Simkl resolves anime titles
        correctly under either `shows[]` or `anime[]`, so match the field to
        your data type when known.
      properties:
        movies:
          type: array
          items:
            $ref: '#/components/schemas/Movie'
        shows:
          type: array
          items:
            $ref: '#/components/schemas/Show'
        episodes:
          type: array
          description: >-
            Top-level convenience array for marking episode-level history
            without nesting. The server wraps each entry into a synthetic
            single-season show. Each item carries the same shape as items under
            `shows[].seasons[].episodes[]` plus the parent `show` reference.
          items:
            type: object
            additionalProperties: true
        anime:
          type: array
          items:
            $ref: '#/components/schemas/Show'
          description: Array of anime entries (same shape as `shows[]`).
    HistoryAddResponse:
      type: object
      description: >-
        Response from `POST /sync/history`. Note: `not_found.shows` includes any
        anime entries that failed to resolve, regardless of whether they were
        sent under `anime[]` or `shows[]` — there is no separate
        `not_found.anime` bucket.
      properties:
        added:
          type: object
          properties:
            movies:
              type: integer
              description: New items added to the user's `Completed` movies list.
            shows:
              type: integer
              description: New shows added to the user's library.
            episodes:
              type: integer
              description: Number of episodes marked watched.
            statuses:
              type: array
              items:
                $ref: '#/components/schemas/SyncStatusItem'
        not_found:
          $ref: '#/components/schemas/NotFoundReport'
    Movie:
      type: object
      description: >-
        Standard movie object. See the [Standard Media
        Objects](/conventions/standard-media-objects) guide.
      properties:
        title:
          type: string
          example: 'Terminator 3: Rise of the Machines'
        year:
          type: integer
          example: 2003
        ids:
          $ref: '#/components/schemas/Ids'
      required:
        - ids
    Show:
      type: object
      description: >-
        Standard show object. May include nested seasons/episodes for partial
        sync.
      properties:
        title:
          type: string
          example: The Walking Dead
        year:
          type: integer
          example: 2010
        ids:
          $ref: '#/components/schemas/Ids'
        seasons:
          type: array
          items:
            type: object
            properties:
              number:
                type: integer
              episodes:
                type: array
                items:
                  $ref: '#/components/schemas/Episode'
      required:
        - ids
    SyncStatusItem:
      type: object
      description: Per-item result of a sync write.
      properties:
        request:
          type: object
          description: Echo of the input item.
        response:
          type: object
          properties:
            status:
              type: string
              enum:
                - watching
                - plantowatch
                - hold
                - dropped
                - completed
                - removed
              description: Resulting list status.
            simkl_type:
              type: string
              enum:
                - movie
                - tv
                - anime
            anime_type:
              type:
                - string
                - 'null'
              description: >-
                Type 4 null — data not on file in that field's slot. See [Null
                and missing values](/conventions/null-values).
        rewatch_id:
          type: integer
          description: >-
            Present when a rewatch session was created or resumed by this call.
            Use this value on subsequent writes to update the same session.
        rewatch_status:
          type: string
          enum:
            - active
            - completed
            - closed
          description: >-
            Lifecycle state of the rewatch session referenced by `rewatch_id`.
            Writable values: `active`, `closed`, and (movies only) `completed`.
            For TV / anime, `completed` is silently clamped to `closed` — only
            coverage of every aired regular episode (specials in season 0 don't
            count) promotes a show session to `completed`. A `closed` write
            against a coverage-earned `completed` session is a no-op. Resuming a
            finished session without `rewatch_id` requires a `last_watched_at` /
            `watched_at` that pinpoints the target row; bare `is_rewatch: true`
            (no `rewatch_status`) always targets the singleton `active` session.
            See [Rewatches guide → Session
            states](/guides/rewatches#session-states).
    NotFoundReport:
      type: object
      description: >-
        Items Simkl could not match. Inspect this to surface diagnostics back to
        your users. Anime entries that fail to resolve land in `shows`
        regardless of which top-level array they were sent under — there is no
        separate `anime` bucket in not_found.
      properties:
        movies:
          type: array
          items:
            $ref: '#/components/schemas/Movie'
        shows:
          type: array
          items:
            $ref: '#/components/schemas/Show'
        episodes:
          type: array
          items:
            $ref: '#/components/schemas/Show'
    ErrorResponse:
      type: object
      description: >-
        Standard error envelope for 4xx and 5xx responses. Branch on `error`
        (machine-readable identifier) — `message`, when present, is
        human-readable guidance and is **not stable across releases**.
      properties:
        error:
          type: string
          description: >-
            Machine-readable error identifier. Stable across responses — use
            this for programmatic branching in client code. Examples:
            `user_token_failed`, `client_id_failed`, `empty_field`,
            `wrong_parameter`, `id_err`, `rate_limit`.
          example: user_token_failed
        code:
          type: integer
          description: >-
            HTTP status code echoed in the body for convenience — same value as
            the response status line. Always an integer.
          example: 401
        message:
          type: string
          description: >-
            Human-readable error guidance. Optional — present on most errors,
            but not guaranteed. May reference the specific field or value that
            triggered the error. NOT stable; do not parse.
          example: Your client_id is wrong. Try another one
      required:
        - error
        - code
    Ids:
      type: object
      description: >-
        External and internal identifiers for an item. Pass as many as you have
        — Simkl resolves to the canonical record.
      properties:
        simkl:
          type: integer
          description: Simkl internal ID. Most reliable.
          example: 53536
        slug:
          type: string
          description: URL-safe slug returned in responses.
          example: attack-on-titan
        imdb:
          type: string
          description: IMDb ID.
          example: tt0181852
        tmdb:
          type: string
          description: TMDb ID.
          example: '296'
        tvdb:
          oneOf:
            - type: integer
            - type: string
          description: TVDB ID or slug.
          example: 153021
        mal:
          type: string
          description: MyAnimeList ID.
          example: '4246'
        anidb:
          type: string
          description: AniDB ID. Specifying just this is enough for anime lookups.
          example: '10846'
        anilist:
          type: string
          description: AniList ID.
          example: '21'
        kitsu:
          type: string
          description: Kitsu ID.
          example: '12'
        anisearch:
          type: string
          description: aniSearch ID.
          example: '2227'
        animeplanet:
          type: string
          description: Anime-Planet slug.
          example: one-piece
        livechart:
          type: string
          description: LiveChart ID.
          example: '321'
        letterboxd:
          type: string
          description: Letterboxd slug.
          example: the-truman-show
        netflix:
          type: string
          description: Netflix movie ID.
          example: '70210890'
        hulu:
          type: string
          description: Hulu episode ID.
        crunchyroll:
          type: string
          description: Crunchyroll episode ID.
        traktslug:
          type: string
          description: Trakt slug.
          example: john-wick-chapter-4-2023
      additionalProperties: true
      example:
        simkl: 53536
        imdb: tt0181852
        tmdb: 296
    Episode:
      type: object
      description: Episode reference. Use `season` + `number`, or `ids`.
      properties:
        season:
          type: integer
          minimum: 0
          example: 1
        number:
          type: integer
          minimum: 1
          example: 2
        watched_at:
          type: string
          format: date-time
          example: '2014-09-01T09:10:11Z'
          description: ISO-8601 GMT timestamp.
        ids:
          $ref: '#/components/schemas/Ids'
  responses:
    BadRequest:
      description: Bad request — a required field is missing or has the wrong shape.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: empty_field
            code: 400
            message: Missed "to" parameter
    Unauthorized:
      description: >-
        Missing or invalid user access token. Provide a valid `Authorization:
        Bearer <access_token>` header.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: user_token_failed
            code: 401
    ClientIdFailed:
      description: >-
        Your `client_id` is missing, wrong, disabled, or has hit a request
        limit. Verify in your [developer
        settings](https://simkl.com/settings/developer/).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: client_id_failed
            code: 412
            message: Your client_id is wrong. Try another one
    RateLimited:
      description: >-
        Too many requests in too short a window. Back off and retry with
        exponential backoff — see [Rate limits](/resources/rate-limits) for the
        playbook.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: rate_limit
            code: 429
    ServerError:
      description: >-
        Something is broken on Simkl's side. Retry after a short delay. If it
        persists, report on the [Simkl Discord](https://discord.gg/MJsWNE4).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: internal
            code: 500
  securitySchemes:
    clientId:
      type: apiKey
      in: query
      name: client_id
      description: >-
        Preferred form: your `client_id` as a URL query parameter on every
        request. Self-describing in logs and curl commands. See [Headers and
        required parameters](/conventions/headers).
      x-default: YOUR_CLIENT_ID
    simklApiKey:
      type: apiKey
      in: header
      name: simkl-api-key
      description: >-
        Optional alias for the `client_id` query parameter. Simkl accepts your
        `client_id` either as the `simkl-api-key` request header **or** as the
        `?client_id=…` query parameter — pick one. The query-parameter form is
        preferred because it makes the request fully self-describing in URL
        form.
      x-default: YOUR_CLIENT_ID
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        OAuth 2.0 or PIN-flow `access_token`. Required for endpoints that read
        or modify the user's library, scrobble session, ratings, settings, or
        playbacks. See [Authentication](/authentication).
      x-default: YOUR_ACCESS_TOKEN

````