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

# Exchange an authorization code for an access token

> Step 2 of OAuth 2.0. POST your authorization `code` here to receive an `access_token`. The success response carries `{access_token, token_type: "bearer", scope: "public", expires_in: 157680000}` — about 5 years. Save the token securely. **No refresh token is issued**; if a 401 arrives before that lifetime (user revoked from [Connected Apps](https://simkl.com/settings/connected-apps/)), send the user back through `/oauth/authorize` for a fresh consent.

#### Wire format

The endpoint accepts both `application/x-www-form-urlencoded` (the RFC 6749 §3.2 default that most OAuth libraries use) and `application/json` — pick whichever your HTTP client prefers. Client credentials can be sent in **either** the request body (`client_id` + `client_secret`) **or** the `Authorization: Basic <base64(client_id:client_secret)>` header (RFC 6749 §2.3.1). All four combinations are equivalent. **Off-the-shelf OAuth libraries work out-of-the-box** with default configuration; see [OAuth client libraries](/api-reference/oauth-libraries) for live-tested examples across every popular runtime.

Two flows share this endpoint, distinguished by which secret you send:

- **Confidential clients** (server-side web apps) send `client_secret` + `redirect_uri`.
- **Public clients** (mobile, SPA, desktop, browser extensions) send `code_verifier` instead — no secret required. See the [Public PKCE walkthrough](/api-reference/oauth-pkce).

#### Body fields

| Field | Required | Notes |
|---|---|---|
| `grant_type` | yes | Must be `authorization_code`. |
| `code` | yes | Authorization code returned to your `redirect_uri` (or, for PKCE-without-registered-URI, displayed on simkl.com). |
| `client_id` | yes (in body or Basic Auth header) | Your app's `client_id`. |
| `client_secret` | conditional | Confidential flow only. Mutually exclusive with `code_verifier`. May be sent in the body or in the `Authorization: Basic` header. |
| `code_verifier` | conditional | PKCE flow only. The original verifier you generated locally; Simkl re-derives the challenge and matches it against what you sent on `/oauth/authorize`. |
| `redirect_uri` | conditional | Required on the confidential flow (must match the URL registered for your app **byte-for-byte**). On PKCE, required only if you sent one to `/oauth/authorize` — and then it must match that one. |

#### Errors

All failures return JSON with an `error` field (and usually a `message` field too). 401 responses additionally carry an RFC 6750 §3 `WWW-Authenticate: Bearer realm="api.simkl.com", error="..."` header.

| Status | `error` | When |
|---|---|---|
| 403 | `empty_field` | A required body field is missing (`code`, `client_id`, `grant_type`, or both `client_secret`/`code_verifier`). |
| 403 | `redirect_failed` | `redirect_uri` doesn't match the URL registered for the app. |
| 401 | `secret_error` | Wrong `client_secret` (confidential flow) **or** PKCE verification failed (`message: "PKCE verification failed"`). |
| 401 | `grant_error` | The `code` is invalid, expired, or already used. Codes are single-use — restart from `/oauth/authorize`. |

<CardGroup cols={2}>
  <Card title="OAuth 2.0 walkthrough" icon="lock" href="/api-reference/oauth" horizontal>
    Confidential-client (server-side) flow.
  </Card>
  <Card title="Public PKCE walkthrough" icon="shield-keyhole" href="/api-reference/oauth-pkce" horizontal>
    Public-client flow with `code_verifier`.
  </Card>
</CardGroup>



## OpenAPI

````yaml /openapi.json post /oauth/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 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:
  /oauth/token:
    post:
      tags:
        - OAuth 2.0
      summary: Exchange an authorization code for an access token
      description: >-
        Step 2 of OAuth 2.0. POST your authorization `code` here to receive an
        `access_token`. The success response carries `{access_token, token_type:
        "bearer", scope: "public", expires_in: 157680000}` — about 5 years. Save
        the token securely. **No refresh token is issued**; if a 401 arrives
        before that lifetime (user revoked from [Connected
        Apps](https://simkl.com/settings/connected-apps/)), send the user back
        through `/oauth/authorize` for a fresh consent.


        #### Wire format


        The endpoint accepts both `application/x-www-form-urlencoded` (the RFC
        6749 §3.2 default that most OAuth libraries use) and `application/json`
        — pick whichever your HTTP client prefers. Client credentials can be
        sent in **either** the request body (`client_id` + `client_secret`)
        **or** the `Authorization: Basic <base64(client_id:client_secret)>`
        header (RFC 6749 §2.3.1). All four combinations are equivalent.
        **Off-the-shelf OAuth libraries work out-of-the-box** with default
        configuration; see [OAuth client
        libraries](/api-reference/oauth-libraries) for live-tested examples
        across every popular runtime.


        Two flows share this endpoint, distinguished by which secret you send:


        - **Confidential clients** (server-side web apps) send `client_secret` +
        `redirect_uri`.

        - **Public clients** (mobile, SPA, desktop, browser extensions) send
        `code_verifier` instead — no secret required. See the [Public PKCE
        walkthrough](/api-reference/oauth-pkce).


        #### Body fields


        | Field | Required | Notes |

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

        | `grant_type` | yes | Must be `authorization_code`. |

        | `code` | yes | Authorization code returned to your `redirect_uri` (or,
        for PKCE-without-registered-URI, displayed on simkl.com). |

        | `client_id` | yes (in body or Basic Auth header) | Your app's
        `client_id`. |

        | `client_secret` | conditional | Confidential flow only. Mutually
        exclusive with `code_verifier`. May be sent in the body or in the
        `Authorization: Basic` header. |

        | `code_verifier` | conditional | PKCE flow only. The original verifier
        you generated locally; Simkl re-derives the challenge and matches it
        against what you sent on `/oauth/authorize`. |

        | `redirect_uri` | conditional | Required on the confidential flow (must
        match the URL registered for your app **byte-for-byte**). On PKCE,
        required only if you sent one to `/oauth/authorize` — and then it must
        match that one. |


        #### Errors


        All failures return JSON with an `error` field (and usually a `message`
        field too). 401 responses additionally carry an RFC 6750 §3
        `WWW-Authenticate: Bearer realm="api.simkl.com", error="..."` header.


        | Status | `error` | When |

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

        | 403 | `empty_field` | A required body field is missing (`code`,
        `client_id`, `grant_type`, or both `client_secret`/`code_verifier`). |

        | 403 | `redirect_failed` | `redirect_uri` doesn't match the URL
        registered for the app. |

        | 401 | `secret_error` | Wrong `client_secret` (confidential flow)
        **or** PKCE verification failed (`message: "PKCE verification failed"`).
        |

        | 401 | `grant_error` | The `code` is invalid, expired, or already used.
        Codes are single-use — restart from `/oauth/authorize`. |


        <CardGroup cols={2}>
          <Card title="OAuth 2.0 walkthrough" icon="lock" href="/api-reference/oauth" horizontal>
            Confidential-client (server-side) flow.
          </Card>
          <Card title="Public PKCE walkthrough" icon="shield-keyhole" href="/api-reference/oauth-pkce" horizontal>
            Public-client flow with `code_verifier`.
          </Card>
        </CardGroup>
      operationId: post-oauth-token
      parameters:
        - $ref: '#/components/parameters/ClientIdQuery'
        - $ref: '#/components/parameters/AppNameQuery'
        - $ref: '#/components/parameters/AppVersionQuery'
        - $ref: '#/components/parameters/UserAgentHeader'
      requestBody:
        content:
          application/json:
            example:
              code: d7be48f1559a6d794w1925237c626326c7dddfsa559a6d794w1925137c626313
              client_id: c7be48f1559a6d794w1925237c626326c7bsdfsa559a6d794w1925137c626352
              client_secret: a9be48f1529a2d794w1925237c626326c7dddfsa559a6d794w1925137c626321
              redirect_uri: https://yourdomain.com/oauth.html
              grant_type: authorization_code
            schema:
              $ref: '#/components/schemas/OAuthTokenRequest'
        required: true
      responses:
        '200':
          description: Valid `code` will generate you the `access_token`
          headers: {}
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthTokenResponse'
              example:
                access_token: YOUR_ACCESS_TOKEN
                token_type: bearer
                scope: public
        '401':
          description: Invalid code will give you a 401 error
          headers: {}
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
              example:
                error: grant_error
                message: Bad code provided.
        '403':
          $ref: '#/components/responses/Forbidden'
      security:
        - simklApiKey: []
      x-codeSamples:
        - lang: curl
          label: cURL
          source: |-
            curl -X POST https://api.simkl.com/oauth/token \
              -H "Content-Type: application/json" \
              -d '{
                "code":          "AUTHORIZATION_CODE",
                "client_id":     "YOUR_CLIENT_ID",
                "client_secret": "YOUR_CLIENT_SECRET",
                "redirect_uri":  "https://yourdomain.com/oauth.html",
                "grant_type":    "authorization_code"
              }'
        - lang: curl
          label: cURL (PKCE)
          source: |-
            # Public clients (mobile / SPA / desktop) — PKCE variant.
            # Replace YOUR_CODE_VERIFIER with the verifier you generated locally
            # (the same one whose SHA-256 you sent as code_challenge in step 1).
            curl -X POST https://api.simkl.com/oauth/token \
              -H "Content-Type: application/json" \
              -d '{
                "code":          "AUTHORIZATION_CODE",
                "client_id":     "YOUR_CLIENT_ID",
                "code_verifier": "YOUR_CODE_VERIFIER",
                "grant_type":    "authorization_code"
              }'
        - lang: JavaScript
          label: Node
          source: |-
            const res = await fetch('https://api.simkl.com/oauth/token', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                code:          'AUTHORIZATION_CODE',
                client_id:     'YOUR_CLIENT_ID',
                client_secret: 'YOUR_CLIENT_SECRET',
                redirect_uri:  'https://yourdomain.com/oauth.html',
                grant_type:    'authorization_code',
              }),
            });
            const { access_token } = await res.json();
        - lang: Python
          label: Python
          source: |-
            import requests
            res = requests.post('https://api.simkl.com/oauth/token', json={
                'code':          'AUTHORIZATION_CODE',
                'client_id':     'YOUR_CLIENT_ID',
                'client_secret': 'YOUR_CLIENT_SECRET',
                'redirect_uri':  'https://yourdomain.com/oauth.html',
                'grant_type':    'authorization_code',
            })
            access_token = res.json()['access_token']
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:
    OAuthTokenRequest:
      type: object
      required:
        - code
        - client_id
        - grant_type
      properties:
        code:
          type: string
          description: Authorization code returned to your `redirect_uri`.
        client_id:
          type: string
          description: Your application's client_id.
        client_secret:
          type: string
          description: >-
            Your application's `client_secret`. Confidential flow only —
            mutually exclusive with `code_verifier`. Public clients (mobile /
            SPA / desktop) MUST NOT embed this value in shipped code; use the
            [PKCE flow](/api-reference/oauth-pkce) instead.
        redirect_uri:
          type: string
          format: uri
          example: https://yourdomain.com/oauth.html
          description: >-
            URL where the user was sent back with the `code`. Required on the
            confidential flow. On PKCE, required only if you sent one to
            `/oauth/authorize`. When sent, must match a URL registered for the
            app **byte-for-byte** (scheme, host, port, path, trailing slash,
            casing).
        code_verifier:
          type: string
          description: >-
            PKCE code verifier (43–128 unreserved characters). Required on the
            PKCE flow — mutually exclusive with `client_secret`. Send the
            **original verifier**; Simkl re-derives the SHA-256 challenge and
            matches it against the `code_challenge` you sent on
            `/oauth/authorize`. On mismatch the response is `401 secret_error`
            with `"PKCE verification failed"` in the message field.
        grant_type:
          type: string
          enum:
            - authorization_code
          default: authorization_code
      oneOf:
        - required:
            - client_secret
            - redirect_uri
          title: Standard (client_secret)
        - required:
            - code_verifier
          title: PKCE (code_verifier)
      description: >-
        Body for `POST /oauth/token`. Send `grant_type=authorization_code` plus
        the `code` you received at `redirect_uri`. Either `client_secret`
        (standard flow) or `code_verifier` (PKCE) must be present.
    OAuthTokenResponse:
      type: object
      required:
        - access_token
        - token_type
        - scope
        - expires_in
      properties:
        access_token:
          type: string
          description: >-
            Long-lived bearer token. Save it securely; there is no refresh-token
            equivalent.
          example: abc123def456
        token_type:
          type: string
          description: >-
            Always `bearer` (lowercase, despite RFC 6750's canonical `Bearer`
            capitalization). Use case-insensitive matching when comparing.
          example: bearer
        scope:
          type: string
          description: >-
            Currently always `public`. Simkl does not have a scope system —
            tokens implicitly grant all permissions the user has approved for
            the app. The `scope` field is a placeholder kept for compatibility
            with OAuth 2.0 token-response shape; ignore its value.
          example: public
          enum:
            - public
        expires_in:
          type: integer
          description: >-
            Token lifetime in seconds (RFC 6749 §5.1). Always `157680000` (about
            5 years). The value is effectively infinite for practical session
            lengths — Simkl tokens remain valid until the user revokes the app
            from [Connected Apps
            settings](https://simkl.com/settings/connected-apps/). There is no
            `refresh_token`; if a 401 arrives before this lifetime expires, the
            user revoked your app — re-run the OAuth flow.
          example: 157680000
      description: >-
        Successful response from `POST /oauth/token`. Carries `access_token`,
        `token_type: bearer`, `scope: public`, and `expires_in: 157680000` (5
        years). Notable omission vs RFC 6749 §5.1: no `refresh_token` — Simkl
        tokens are long-lived but not refreshable. A 401 on a subsequent
        authenticated call means the user revoked the app; re-run OAuth.
    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:
    Forbidden:
      description: >-
        Refused. The request was understood but the caller is not permitted to
        perform it — for example, a feature gated to Simkl Pro / VIP, or an
        action the user has not consented to.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: forbidden
            code: 403
  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

````