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

# Token (V2)

> Exchanges a grant for an access token. One endpoint serves all three AUTH V2 grants, selected by `grant_type`:

| `grant_type` | Use it to |
| --- | --- |
| `authorization_code` | Turn a `code` from the consent redirect into tokens. |
| `refresh_token` | Get a fresh access token without involving the user. |
| `urn:ietf:params:oauth:grant-type:device_code` | Poll for approval during the device flow. |

This endpoint is on `api.simkl.com`, while the consent page is on `simkl.com`. Rather than hardcoding both, a library that reads RFC 8414 metadata can take the single issuer setting `https://simkl.com` and resolve `token_endpoint` itself -- the same document also advertises all three grants above. See [Discovery](/api-reference/auth-v2#discovery).

### Client authentication

Apps registered as **Server apps & services** must authenticate. Send the credentials either as an `Authorization: Basic` header (RFC 6749 section 2.3.1) or as `client_id` and `client_secret` parameters in the body. Both are accepted.

**Mobile, desktop & browser apps** and **TV, devices & command line** registrations have no secret and send only `client_id`.

Client authentication is checked **before** the grant is looked at, and a failure is `401 invalid_client`, not `400`. If a server app omits its secret it gets `invalid_client` no matter what else is wrong with the request, which can mask a second problem behind it.

### Body format

Both `application/x-www-form-urlencoded` (the RFC 6749 section 3.2 default) and `application/json` are accepted. Every parameter must be a plain string. Sending an array or object for any parameter returns `400 invalid_request` naming the offending parameter.

### Authorization code grant

Requires `code`, `code_verifier` and the same `redirect_uri` you sent to the authorize endpoint.

**The code is consumed the moment it reaches this endpoint, before any validation runs.** A wrong `code_verifier`, a mismatched `redirect_uri` or a foreign `client_id` all destroy the code. You cannot correct the request and retry with the same code -- the user has to authorize again. Each successful exchange creates a new, independent grant; authorizing a second time does not revoke the first.

### Refresh token grant

Requires `refresh_token`. The same refresh token is returned, with its 180-day window slid forward. **The previous access token stops working immediately**, so a single grant supports exactly one live access token -- two processes sharing one refresh token will keep cutting each other off. Give them separate authorizations.

A `scope` narrower than the original grant is ignored rather than applied; a wider one is rejected with `invalid_scope`. A grant's scope can never shrink.

### Device code grant

Requires `device_code`. Poll no faster than the `interval` returned by `POST /oauth2/device`.

- `authorization_pending` -- keep polling at the same interval. This is also what you get if the user declined, because declining writes nothing; your own timeout at `expires_in` is the only signal you will get.
- `slow_down` -- add 5 seconds to your interval and **wait**. The poll timer is reset by the attempt itself, so retrying immediately keeps you permanently rate-limited.
- `expired_token` -- the code is dead. Start a new device authorization.

Every `/oauth2/*` response is sent with `Cache-Control: no-store` and `Pragma: no-cache`. Do not cache these responses, including the errors.



## OpenAPI

````yaml /openapi.json post /oauth2/token
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 V2
    description: >-
      Standards-compliant OAuth 2.0 with PKCE, scopes and refresh tokens. The
      recommended way to authenticate new apps.
  - 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.
  - name: Lists
    description: >-
      Custom lists, read-only and in beta. Requires a Simkl PRO or VIP access
      token.
externalDocs:
  description: Simkl API documentation
  url: https://api.simkl.org
paths:
  /oauth2/token:
    post:
      tags:
        - OAuth V2
      summary: Token (V2)
      description: >-
        Exchanges a grant for an access token. One endpoint serves all three
        AUTH V2 grants, selected by `grant_type`:


        | `grant_type` | Use it to |

        | --- | --- |

        | `authorization_code` | Turn a `code` from the consent redirect into
        tokens. |

        | `refresh_token` | Get a fresh access token without involving the user.
        |

        | `urn:ietf:params:oauth:grant-type:device_code` | Poll for approval
        during the device flow. |


        This endpoint is on `api.simkl.com`, while the consent page is on
        `simkl.com`. Rather than hardcoding both, a library that reads RFC 8414
        metadata can take the single issuer setting `https://simkl.com` and
        resolve `token_endpoint` itself -- the same document also advertises all
        three grants above. See [Discovery](/api-reference/auth-v2#discovery).


        ### Client authentication


        Apps registered as **Server apps & services** must authenticate. Send
        the credentials either as an `Authorization: Basic` header (RFC 6749
        section 2.3.1) or as `client_id` and `client_secret` parameters in the
        body. Both are accepted.


        **Mobile, desktop & browser apps** and **TV, devices & command line**
        registrations have no secret and send only `client_id`.


        Client authentication is checked **before** the grant is looked at, and
        a failure is `401 invalid_client`, not `400`. If a server app omits its
        secret it gets `invalid_client` no matter what else is wrong with the
        request, which can mask a second problem behind it.


        ### Body format


        Both `application/x-www-form-urlencoded` (the RFC 6749 section 3.2
        default) and `application/json` are accepted. Every parameter must be a
        plain string. Sending an array or object for any parameter returns `400
        invalid_request` naming the offending parameter.


        ### Authorization code grant


        Requires `code`, `code_verifier` and the same `redirect_uri` you sent to
        the authorize endpoint.


        **The code is consumed the moment it reaches this endpoint, before any
        validation runs.** A wrong `code_verifier`, a mismatched `redirect_uri`
        or a foreign `client_id` all destroy the code. You cannot correct the
        request and retry with the same code -- the user has to authorize again.
        Each successful exchange creates a new, independent grant; authorizing a
        second time does not revoke the first.


        ### Refresh token grant


        Requires `refresh_token`. The same refresh token is returned, with its
        180-day window slid forward. **The previous access token stops working
        immediately**, so a single grant supports exactly one live access token
        -- two processes sharing one refresh token will keep cutting each other
        off. Give them separate authorizations.


        A `scope` narrower than the original grant is ignored rather than
        applied; a wider one is rejected with `invalid_scope`. A grant's scope
        can never shrink.


        ### Device code grant


        Requires `device_code`. Poll no faster than the `interval` returned by
        `POST /oauth2/device`.


        - `authorization_pending` -- keep polling at the same interval. This is
        also what you get if the user declined, because declining writes
        nothing; your own timeout at `expires_in` is the only signal you will
        get.

        - `slow_down` -- add 5 seconds to your interval and **wait**. The poll
        timer is reset by the attempt itself, so retrying immediately keeps you
        permanently rate-limited.

        - `expired_token` -- the code is dead. Start a new device authorization.


        Every `/oauth2/*` response is sent with `Cache-Control: no-store` and
        `Pragma: no-cache`. Do not cache these responses, including the errors.
      operationId: post-oauth2-token
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OAuth2TokenRequest'
            example:
              grant_type: authorization_code
              client_id: YOUR_CLIENT_ID
              code: AUTHORIZATION_CODE
              redirect_uri: YOUR_REDIRECT_URI
              code_verifier: YOUR_CODE_VERIFIER
          application/json:
            schema:
              $ref: '#/components/schemas/OAuth2TokenRequest'
            example:
              grant_type: refresh_token
              client_id: YOUR_CLIENT_ID
              refresh_token: simkl_rt_YOUR_REFRESH_TOKEN
      responses:
        '200':
          description: Tokens issued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2TokenResponse'
              example:
                access_token: simkl_at_EXAMPLEACCESSTOKENVALUE00000000000
                token_type: Bearer
                expires_in: 604800
                refresh_token: simkl_rt_EXAMPLEREFRESHTOKENVALUE0000000000
                scope: media:read media:write
        '400':
          description: >-
            The grant was rejected, or the request was malformed. Also carries
            the device-flow polling states `authorization_pending` and
            `slow_down`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2Error'
              example:
                error: invalid_grant
                error_description: Authorization code is invalid, expired, or already used
        '401':
          description: Client authentication failed. Raised before the grant is examined.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuth2Error'
              example:
                error: invalid_client
                error_description: Client authentication failed
      security:
        - {}
components:
  schemas:
    OAuth2TokenRequest:
      type: object
      description: >-
        Parameters for `POST /oauth2/token`. Which ones apply depends on
        `grant_type`; every value must be a plain string.
      required:
        - grant_type
        - client_id
      properties:
        grant_type:
          type: string
          enum:
            - authorization_code
            - refresh_token
            - urn:ietf:params:oauth:grant-type:device_code
          description: Selects the grant.
        client_id:
          type: string
          description: Your app's client ID.
        client_secret:
          type: string
          description: >-
            Apps registered as **Server apps & services** only. Only needed if
            you are not using `Authorization: Basic`. Never ship this inside an
            app users install.
        code:
          type: string
          description: >-
            `authorization_code` grant. Single-use, and consumed even by a
            failed exchange.
        code_verifier:
          type: string
          description: >-
            `authorization_code` grant. 43 to 128 characters from `A-Z a-z 0-9 -
            . _ ~`. A malformed verifier is reported as `invalid_grant`, the
            same error as a genuine mismatch.
        redirect_uri:
          type: string
          description: >-
            `authorization_code` grant. Must match the one used to obtain the
            code.
        refresh_token:
          type: string
          description: '`refresh_token` grant.'
        device_code:
          type: string
          description: Device grant. From `POST /oauth2/device`.
        scope:
          type: string
          description: >-
            `refresh_token` grant only, and rarely useful: a narrower scope is
            ignored and a wider one is rejected.
    OAuth2TokenResponse:
      type: object
      description: >-
        RFC 6749 section 5.1 token response. Identical in shape for all three
        grants.
      required:
        - access_token
        - token_type
        - expires_in
        - refresh_token
        - scope
      properties:
        access_token:
          type: string
          description: >-
            Bearer token, 43 characters, prefixed `simkl_at_`. Send as
            `Authorization: Bearer ...` on every request.
        token_type:
          type: string
          enum:
            - Bearer
          description: >-
            Always the capitalised string `Bearer`. AUTH V1 returns lowercase
            `bearer`, so compare case-insensitively if you support both.
        expires_in:
          type: integer
          description: Seconds until the access token expires. Always 604800, i.e. 7 days.
          examples:
            - 604800
        refresh_token:
          type: string
          description: >-
            43 characters, prefixed `simkl_rt_`. Valid for 180 days, and the
            window slides forward each time you use it. Non-rotating: the same
            string is returned every refresh, so store it once.
        scope:
          type: string
          enum:
            - media:read
            - media:read media:write
          description: >-
            The scope actually granted. Always exactly one of these two values,
            whatever you asked for. Check it rather than assuming your request
            was honoured.
    OAuth2Error:
      type: object
      description: >-
        RFC 6749 section 5.2 error envelope. `error` is a fixed machine-readable
        code; `error_description` is human-readable and may change, so branch on
        `error` only.
      required:
        - error
      properties:
        error:
          type: string
          description: Machine-readable error code.
          enum:
            - invalid_request
            - invalid_client
            - invalid_grant
            - invalid_scope
            - unsupported_grant_type
            - expired_token
            - authorization_pending
            - slow_down
            - server_error
        error_description:
          type: string
          description: >-
            Human-readable explanation. For display and logging, not for
            branching.
  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

````