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

# Recently changed catalog items

> Returns Simkl catalog IDs whose metadata changed in the last **N** days, grouped by type. Use it to keep the items already on a user's watchlist fresh — when a show airs a new episode, an upcoming title starts airing, or a movie's metadata is updated, the corresponding ID appears here.

<Tip>
**If all you need is "which episodes are airing soon?"** — use the [Calendar data files](/api-reference/calendar) on `data.simkl.in` instead. They're CDN-cached and give you every upcoming episode in a single fast call:

- **Rolling window** (`/calendar/{type}.json`) — yesterday + the next ~33 days. The default for "what's on now and next".
- **Monthly archive** (`/calendar/{year}/{month}/{type}.json`) — fetch previous, current, and next month separately when you need a wider calendar grid view (e.g. a 3-month strip).

Reserve `/changes` for the wider job of tracking catalog metadata updates (status flips, ratings, posters, runtimes) on items already on the user's watchlist.
</Tip>

#### How tracking apps use it

You already know which items the user is tracking from the [Sync guide](/guides/sync) loop ([`GET /sync/activities`](/api-reference/simkl/get-activities) + [`GET /sync/all-items`](/api-reference/simkl/get-all-items) with `date_from=`). That tells you when the **user** touched their lists. `/changes` is the complementary call — it tells you when **Simkl's catalog metadata** for any item moved, independent of whether the user touched their list. New episodes that just aired, a show whose status flipped from *upcoming* to *airing*, a movie whose runtime / poster / overview was updated.

#### When to actually call it

Treat it like Sync: **trigger on a user-visible event, never on a background timer.** The intended cadence is **at most once per day per user**, gated on a stored timestamp:

| Trigger | What to do |
|---|---|
| App launch / wake-from-background | If `now() − last_changes_poll ≥ 24 h`, run the loop below and save `now()` as `last_changes_poll`. If less than 24 h, skip — the 14-day response window means the same IDs will still be there tomorrow. |
| Manual refresh button | Always allow — bypasses the 24 h gate so the user can force a check. |
| Never | Background `setInterval` timers, per-user crons, real-time loops, polling on every screen transition. These will get the `client_id` rate-limited. |

#### The loop (when the trigger fires)

1. Call `/changes?date_from=<last_changes_poll>`. Narrow with `type=` to only the catalogs the user has items in (skip `anime` if the user has no anime, etc.).
2. **Intersect in your client:** `{IDs the response returned} ∩ {IDs the user has on any watchlist}`. You already have the user's watchlist locally from the Sync loop — this is a Set lookup, ~microseconds.
3. **Apply the skip rules below** to the intersection. Most items get dropped here — only the ones where new metadata is plausible survive.
4. For each surviving ID, refetch the matching cached endpoint:

| Refetch for | Endpoint |
|---|---|
| Movie metadata (poster, overview, ratings, release date) | [`GET /movies/{id}`](/api-reference/simkl/get-movie) |
| TV show metadata + new episode counts / status | [`GET /tv/{id}`](/api-reference/simkl/get-tv-show) and/or [`GET /tv/episodes/{id}`](/api-reference/simkl/get-tv-episodes) |
| Anime metadata + new episodes | [`GET /anime/{id}`](/api-reference/simkl/get-anime) and/or [`GET /anime/episodes/{id}`](/api-reference/simkl/get-anime-episodes) |

The detail endpoints are edge-cached by Simkl ID, so popular titles come straight from Cloudflare without hitting origin. **When an item's metadata or episodes change, Simkl automatically purges its Cloudflare cache entry** — so a refetch right after `/changes` flagged the ID is guaranteed to return the fresh data, never a stale edge copy. After the refetch, save `now()` as `last_changes_poll`.

#### Skip rules — when not to refetch

**First, restrict the intersect to active lists.** Items on the user's `completed` and `dropped` lists are titles they're done with — there's no UX win in refreshing metadata for a show they're never opening again. Intersect `/changes` only against items on `watching`, `plantowatch`, and `hold` (plus their anime equivalents); drop the rest before you even consider a refetch.

After that filter, the remaining items still benefit from status-based throttling. Cache the last-known `status` (TV/anime) or release state (movies) for each item in the user's watchlist, and track the last time you refetched each item. Use the rules below — most items will sit in a state where new metadata is implausible.

| Last-known state | When to actually refetch |
|---|---|
| TV / anime, `status: airing` | Every time the ID appears in `/changes` — new episodes can drop any week. |
| TV / anime, `status: tba` (upcoming) | Weekly — what matters is the moment the status flips to `airing` and the first real air date locks in. |
| TV / anime, `status: ended`, ended **less than 30 days ago** | Every time the ID appears — late corrections to ratings / episode counts happen here. |
| TV / anime, `status: ended`, ended **more than 30 days ago** | Skip. Refetch once a month at most. Metadata on a finished show is effectively frozen. |
| Movie, released **less than 6 months ago** | Every time the ID appears — ratings / poster art / overview tend to churn early. |
| Movie, released **more than 6 months ago** | Skip. Refetch quarterly at most. |
| Movie, unreleased / `tba` | Weekly — you mostly care about the release-date update. |

The intersect + skip combination typically reduces a `/changes` response of tens of thousands of IDs down to a handful of detail-endpoint refetches per day per user — even for power users with very large libraries.

#### Query parameters

| Param | Default | Notes |
|---|---|---|
| `date_from` | 14 days ago | ISO date (e.g. `YYYY-MM-DD`). The server **clamps** to no older than 14 days ago — older values silently snap to the cap. Invalid values (e.g. `BOGUS`) silently fall back to the default. Future dates return `{}`. |
| `type` | `anime,shows,movies` | CSV. Any combination of `anime`, `shows`, `movies`. Unknown values silently bucket as anime — keep the values in the listed set. Use it to skip catalogs the user doesn't have anything on. |

#### Response shape

An object with up to three keys (`movies`, `shows`, `anime`), each an array of integer Simkl IDs. **Keys are omitted when their bucket is empty.** If nothing changed in the window, the response is `{}` (an empty object, NOT `[]`).

```json
{
  "movies": [
    56145,
    1029384
  ],
  "shows": [
    17465,
    92834
  ],
  "anime": [
    39687
  ]
}
```

IDs are returned in no particular order. Items modified in the **last 5 minutes are excluded** so partially-written records don't leak into the delta. **Each response contains at most 50,000 IDs** — if the catalog produces more (rare; only on very wide windows across all three types), narrow the call with `type=` to fit under the cap.




## OpenAPI

````yaml /openapi.json get /changes
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:
  /changes:
    get:
      tags:
        - Changes
      summary: Recently changed catalog items
      description: >
        Returns Simkl catalog IDs whose metadata changed in the last **N** days,
        grouped by type. Use it to keep the items already on a user's watchlist
        fresh — when a show airs a new episode, an upcoming title starts airing,
        or a movie's metadata is updated, the corresponding ID appears here.


        <Tip>

        **If all you need is "which episodes are airing soon?"** — use the
        [Calendar data files](/api-reference/calendar) on `data.simkl.in`
        instead. They're CDN-cached and give you every upcoming episode in a
        single fast call:


        - **Rolling window** (`/calendar/{type}.json`) — yesterday + the next
        ~33 days. The default for "what's on now and next".

        - **Monthly archive** (`/calendar/{year}/{month}/{type}.json`) — fetch
        previous, current, and next month separately when you need a wider
        calendar grid view (e.g. a 3-month strip).


        Reserve `/changes` for the wider job of tracking catalog metadata
        updates (status flips, ratings, posters, runtimes) on items already on
        the user's watchlist.

        </Tip>


        #### How tracking apps use it


        You already know which items the user is tracking from the [Sync
        guide](/guides/sync) loop ([`GET
        /sync/activities`](/api-reference/simkl/get-activities) + [`GET
        /sync/all-items`](/api-reference/simkl/get-all-items) with
        `date_from=`). That tells you when the **user** touched their lists.
        `/changes` is the complementary call — it tells you when **Simkl's
        catalog metadata** for any item moved, independent of whether the user
        touched their list. New episodes that just aired, a show whose status
        flipped from *upcoming* to *airing*, a movie whose runtime / poster /
        overview was updated.


        #### When to actually call it


        Treat it like Sync: **trigger on a user-visible event, never on a
        background timer.** The intended cadence is **at most once per day per
        user**, gated on a stored timestamp:


        | Trigger | What to do |

        |---|---|

        | App launch / wake-from-background | If `now() − last_changes_poll ≥ 24
        h`, run the loop below and save `now()` as `last_changes_poll`. If less
        than 24 h, skip — the 14-day response window means the same IDs will
        still be there tomorrow. |

        | Manual refresh button | Always allow — bypasses the 24 h gate so the
        user can force a check. |

        | Never | Background `setInterval` timers, per-user crons, real-time
        loops, polling on every screen transition. These will get the
        `client_id` rate-limited. |


        #### The loop (when the trigger fires)


        1. Call `/changes?date_from=<last_changes_poll>`. Narrow with `type=` to
        only the catalogs the user has items in (skip `anime` if the user has no
        anime, etc.).

        2. **Intersect in your client:** `{IDs the response returned} ∩ {IDs the
        user has on any watchlist}`. You already have the user's watchlist
        locally from the Sync loop — this is a Set lookup, ~microseconds.

        3. **Apply the skip rules below** to the intersection. Most items get
        dropped here — only the ones where new metadata is plausible survive.

        4. For each surviving ID, refetch the matching cached endpoint:


        | Refetch for | Endpoint |

        |---|---|

        | Movie metadata (poster, overview, ratings, release date) | [`GET
        /movies/{id}`](/api-reference/simkl/get-movie) |

        | TV show metadata + new episode counts / status | [`GET
        /tv/{id}`](/api-reference/simkl/get-tv-show) and/or [`GET
        /tv/episodes/{id}`](/api-reference/simkl/get-tv-episodes) |

        | Anime metadata + new episodes | [`GET
        /anime/{id}`](/api-reference/simkl/get-anime) and/or [`GET
        /anime/episodes/{id}`](/api-reference/simkl/get-anime-episodes) |


        The detail endpoints are edge-cached by Simkl ID, so popular titles come
        straight from Cloudflare without hitting origin. **When an item's
        metadata or episodes change, Simkl automatically purges its Cloudflare
        cache entry** — so a refetch right after `/changes` flagged the ID is
        guaranteed to return the fresh data, never a stale edge copy. After the
        refetch, save `now()` as `last_changes_poll`.


        #### Skip rules — when not to refetch


        **First, restrict the intersect to active lists.** Items on the user's
        `completed` and `dropped` lists are titles they're done with — there's
        no UX win in refreshing metadata for a show they're never opening again.
        Intersect `/changes` only against items on `watching`, `plantowatch`,
        and `hold` (plus their anime equivalents); drop the rest before you even
        consider a refetch.


        After that filter, the remaining items still benefit from status-based
        throttling. Cache the last-known `status` (TV/anime) or release state
        (movies) for each item in the user's watchlist, and track the last time
        you refetched each item. Use the rules below — most items will sit in a
        state where new metadata is implausible.


        | Last-known state | When to actually refetch |

        |---|---|

        | TV / anime, `status: airing` | Every time the ID appears in `/changes`
        — new episodes can drop any week. |

        | TV / anime, `status: tba` (upcoming) | Weekly — what matters is the
        moment the status flips to `airing` and the first real air date locks
        in. |

        | TV / anime, `status: ended`, ended **less than 30 days ago** | Every
        time the ID appears — late corrections to ratings / episode counts
        happen here. |

        | TV / anime, `status: ended`, ended **more than 30 days ago** | Skip.
        Refetch once a month at most. Metadata on a finished show is effectively
        frozen. |

        | Movie, released **less than 6 months ago** | Every time the ID appears
        — ratings / poster art / overview tend to churn early. |

        | Movie, released **more than 6 months ago** | Skip. Refetch quarterly
        at most. |

        | Movie, unreleased / `tba` | Weekly — you mostly care about the
        release-date update. |


        The intersect + skip combination typically reduces a `/changes` response
        of tens of thousands of IDs down to a handful of detail-endpoint
        refetches per day per user — even for power users with very large
        libraries.


        #### Query parameters


        | Param | Default | Notes |

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

        | `date_from` | 14 days ago | ISO date (e.g. `YYYY-MM-DD`). The server
        **clamps** to no older than 14 days ago — older values silently snap to
        the cap. Invalid values (e.g. `BOGUS`) silently fall back to the
        default. Future dates return `{}`. |

        | `type` | `anime,shows,movies` | CSV. Any combination of `anime`,
        `shows`, `movies`. Unknown values silently bucket as anime — keep the
        values in the listed set. Use it to skip catalogs the user doesn't have
        anything on. |


        #### Response shape


        An object with up to three keys (`movies`, `shows`, `anime`), each an
        array of integer Simkl IDs. **Keys are omitted when their bucket is
        empty.** If nothing changed in the window, the response is `{}` (an
        empty object, NOT `[]`).


        ```json

        {
          "movies": [
            56145,
            1029384
          ],
          "shows": [
            17465,
            92834
          ],
          "anime": [
            39687
          ]
        }

        ```


        IDs are returned in no particular order. Items modified in the **last 5
        minutes are excluded** so partially-written records don't leak into the
        delta. **Each response contains at most 50,000 IDs** — if the catalog
        produces more (rare; only on very wide windows across all three types),
        narrow the call with `type=` to fit under the cap.
      operationId: get-changes
      parameters:
        - name: date_from
          in: query
          description: >-
            ISO date (`YYYY-MM-DD`). Items modified at or after this timestamp
            are returned. **Server caps at 14 days ago** — older values silently
            snap to the cap. Invalid values (e.g. `BOGUS`) silently fall back to
            the default. Future dates return `{}`. Defaults to 14 days ago when
            omitted.
          schema:
            type: string
            format: date
          example: '2026-05-11'
        - name: type
          in: query
          description: >-
            CSV of types to include. Any combination of `anime`, `shows`,
            `movies`. Unknown values silently bucket as anime — keep the values
            in the listed set.
          schema:
            type: string
            default: anime,shows,movies
          example: shows,movies
        - $ref: '#/components/parameters/ClientIdQuery'
        - $ref: '#/components/parameters/AppNameQuery'
        - $ref: '#/components/parameters/AppVersionQuery'
        - $ref: '#/components/parameters/UserAgentHeader'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangesResponse'
              examples:
                since_last_sync_24h:
                  summary: >-
                    Daily poll — `date_from` set to the previous successful sync
                    (~24 h ago)
                  value:
                    shows:
                      - 2
                      - 40
                      - 500
                      - ... ~825 more
                    anime:
                      - 44822
                      - 2017210
                      - 2254562
                      - ... ~20 more
                    movies:
                      - 53078
                      - 53080
                      - 53082
                      - ... ~8,200 more
                type_movies_only:
                  summary: >-
                    type=movies — single-bucket filter (skip catalogs the user
                    has nothing on)
                  value:
                    movies:
                      - 53078
                      - 53080
                      - 53082
                      - ... ~8,200 more
                type_csv_two_buckets:
                  summary: type=shows,movies — CSV, excludes anime
                  value:
                    shows:
                      - 2
                      - 40
                      - 500
                      - ... ~825 more
                    movies:
                      - 53078
                      - 53080
                      - 53082
                      - ... ~8,200 more
                default_all_types_14_days:
                  summary: >-
                    No filters — full 14-day delta across all 3 buckets (use
                    only for first-time backfill, not for routine polls)
                  value:
                    shows:
                      - 1
                      - 2
                      - 40
                      - 43
                      - ... ~5,200 more
                    movies:
                      - 2152693
                      - 1875405
                      - 2559003
                      - ... ~42,000 more
                    anime:
                      - 41314
                      - 36602
                      - 38636
                      - ... ~115 more
                unknown_type_buckets_as_anime:
                  summary: Silent-fallback sentinel — type=foo buckets as anime
                  value:
                    anime:
                      - 41314
                      - 36602
                      - 38636
                      - ... ~115 more
                future_date_returns_empty_object:
                  summary: Future date_from — empty `{}` (NOT `[]`)
                  value: {}
        '412':
          $ref: '#/components/responses/ClientIdFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
      security:
        - clientId: []
      x-codeSamples:
        - lang: Shell
          label: since_last_sync_24h
          source: |-
            curl -H "User-Agent: my-app-name/1.0" \
              "https://api.simkl.com/changes?date_from=2026-05-17&client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0"
        - lang: Shell
          label: type_movies_only
          source: |-
            curl -H "User-Agent: my-app-name/1.0" \
              "https://api.simkl.com/changes?date_from=2026-05-17&type=movies&client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0"
        - lang: Shell
          label: type_csv_two_buckets
          source: |-
            curl -H "User-Agent: my-app-name/1.0" \
              "https://api.simkl.com/changes?date_from=2026-05-17&type=shows,movies&client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0"
        - lang: Shell
          label: default_all_types_14_days
          source: |-
            curl -H "User-Agent: my-app-name/1.0" \
              "https://api.simkl.com/changes?client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0"
        - lang: Shell
          label: unknown_type_buckets_as_anime
          source: |-
            curl -H "User-Agent: my-app-name/1.0" \
              "https://api.simkl.com/changes?type=foo&client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0"
        - lang: Shell
          label: future_date_returns_empty_object
          source: |-
            curl -H "User-Agent: my-app-name/1.0" \
              "https://api.simkl.com/changes?date_from=2030-01-01&client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0"
components:
  parameters:
    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:
    ChangesResponse:
      title: Changes response
      type: object
      description: >-
        Catalog IDs that changed in the requested window, grouped by type. Each
        key is OPTIONAL — keys whose bucket is empty are omitted entirely. An
        all-empty response is `{}`.
      additionalProperties: false
      properties:
        movies:
          type: array
          items:
            type: integer
          description: Simkl IDs of movies whose metadata changed in the window.
        shows:
          type: array
          items:
            type: integer
          description: Simkl IDs of TV shows whose metadata changed in the window.
        anime:
          type: array
          items:
            type: integer
          description: Simkl IDs of anime whose metadata changed in the window.
    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
  responses:
    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

````