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

# OAuth flow

> Authorize a user, get a code, exchange it for an access token.

There's no reinventing the wheel here — the API uses OAuth 2.0.

Requesting user-associated information requires a `token` that needs to be included in all request headers made to the API.

To obtain the `client_id` and `client_secret`, please [create an app first](https://simkl.com/settings/developer/).

<Warning>
  **Two domains, two roles.** OAuth uses **two different hosts** — easy to mix up, and the most common cause of "404 Not Found" during integration:

  | Endpoint               | Host                | What it does                                                                                        |
  | ---------------------- | ------------------- | --------------------------------------------------------------------------------------------------- |
  | `GET /oauth/authorize` | **`simkl.com`**     | Browser-facing consent page. The user lands here, signs in, and approves your app.                  |
  | `POST /oauth/token`    | **`api.simkl.com`** | Server-to-server code exchange. Your backend posts the `code` here and gets back an `access_token`. |

  If your authorize URL points at `api.simkl.com` you'll get a 404 — it has to be `simkl.com`.
</Warning>

To make calls on behalf of a user you have to obtain an `access_token`. To do this, first send the user to **`https://simkl.com/oauth/authorize`** to receive a `code`, then post it in JSON format to **`https://api.simkl.com/oauth/token`**. The response contains the `access_token`.

## STEP 1 — Authorize (`simkl.com`)

Open a URL like the one below in the user's browser or a Custom Tab (do **not** use a WebView on mobile):

```
https://simkl.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=http://yourdomain.com/oauth.html&state=RANDOM_CSRF_TOKEN&app-name=my-app-name&app-version=1.0
```

Simkl will redirect back to your `redirect_uri` with `?code=...` appended (and `&state=...` if you sent one).

<Note>
  Build this URL against `https://simkl.com` — **not** `https://api.simkl.com`. The API host has no `/oauth/authorize` page.
</Note>

<Tip>
  **Use `state` for CSRF protection.** Generate a random string before redirecting and store it on your session. When the redirect arrives, verify the `state` echoed back matches what you sent. If it doesn't, reject the request — someone else may have started a flow on your behalf.
</Tip>

<Warning>
  **User denial doesn't return an `error=` parameter.** If the user clicks "No" on the consent screen, Simkl redirects to `/` on simkl.com — **not** back to your `redirect_uri` with `error=access_denied`. Treat any flow where the redirect never lands within a sensible timeout (e.g. 5 minutes) as a denial / cancellation and let the user retry.
</Warning>

## STEP 2 — Exchange code for token (`api.simkl.com`)

POST the `code` to `https://api.simkl.com/oauth/token` with your `client_id`, `client_secret`, `redirect_uri`, and `grant_type=authorization_code`.

<Note>
  **Both content-types and both credential locations work.** Simkl's `POST /oauth/token` accepts:

  * `Content-Type: application/x-www-form-urlencoded` (the RFC 6749 §3.2 default) **or** `Content-Type: application/json` — pick whichever your HTTP client prefers
  * Client credentials in the request body (`client_id` + `client_secret` parameters) **or** in the `Authorization: Basic` header (RFC 6749 §2.3.1) — both paths are honored

  That means **off-the-shelf OAuth libraries work out-of-the-box** with no custom encoding or auth-method config. The two equivalent ways to call the token endpoint:

  <CodeGroup>
    ```bash form-encoded (RFC 6749 §3.2 default) theme={"theme":{"light":"github-light","dark":"vesper"}}
    curl -X POST https://api.simkl.com/oauth/token \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -H "User-Agent: my-app-name/1.0" \
      --data-urlencode "client_id=YOUR_CLIENT_ID" \
      --data-urlencode "client_secret=YOUR_CLIENT_SECRET" \
      --data-urlencode "code=AUTHORIZATION_CODE" \
      --data-urlencode "redirect_uri=YOUR_REDIRECT_URI" \
      --data-urlencode "grant_type=authorization_code"
    ```

    ```bash JSON body theme={"theme":{"light":"github-light","dark":"vesper"}}
    curl -X POST https://api.simkl.com/oauth/token \
      -H "Content-Type: application/json" \
      -H "User-Agent: my-app-name/1.0" \
      -d '{
        "client_id":     "YOUR_CLIENT_ID",
        "client_secret": "YOUR_CLIENT_SECRET",
        "code":          "AUTHORIZATION_CODE",
        "redirect_uri":  "YOUR_REDIRECT_URI",
        "grant_type":    "authorization_code"
      }'
    ```
  </CodeGroup>

  For library-specific examples (Python, Node, Java, Go, PHP), see [OAuth client libraries](/api-reference/oauth-libraries) — most are zero-config.
</Note>

The successful response contains your `access_token`:

```json theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "access_token": "...",
  "token_type":   "bearer",
  "scope":        "public",
  "expires_in":   157680000
}
```

`expires_in` is **5 years in seconds** — effectively infinite for any realistic session. There's no `refresh_token`; Simkl tokens are long-lived and the API has no refresh-token grant. If a 401 arrives before that lifetime elapses, the user revoked your app at [Connected Apps settings](https://simkl.com/settings/connected-apps/) — restart the flow from STEP 1.

<Note>
  **The authorization `code` is single-use.** As soon as you POST it to `/oauth/token`, the server deletes it — even if the exchange fails (network error, validation mismatch, etc.). If your exchange fails for any reason, don't retry with the same `code` — restart from STEP 1.
</Note>

<Note>
  **Same user, same token.** If the same user runs the OAuth flow twice for your app — whether via the standard flow, PKCE, or PIN — Simkl returns the **same `access_token`** both times (it increments an internal usage counter but doesn't rotate the token). Storing the latest response is safe; you don't need to invalidate older tokens of your own because there aren't multiple ones.
</Note>

<Warning>
  **Public clients (mobile, SPA, browser extensions, desktop binaries) must not embed `client_secret`.** Anything compiled into the user's app should be considered leaked. Use the [Public PKCE flow](/api-reference/oauth-pkce) (`code_verifier` + `code_challenge`) instead — same browser-based UX, no secret required.
</Warning>

## See also

<CardGroup cols={2}>
  <Card title="Choose a flow" icon="shield-halved" href="/api-reference/auth">
    Per-platform recommendations, code samples, comparison across all three flows.
  </Card>

  <Card title="Public PKCE" icon="shield-keyhole" href="/api-reference/oauth-pkce">
    The variant for mobile, SPA, browser extensions, and desktop binaries — same browser UX, no `client_secret` required.
  </Card>

  <Card title="PIN flow" icon="key" href="/api-reference/pin">
    The alternative for TVs, consoles, watches, CLIs, and media-server plugins — no `client_secret` and no redirect required.
  </Card>

  <Card title="GET /oauth/authorize" icon="arrow-right" href="/api-reference/simkl/authorize">
    Endpoint reference — every accepted query parameter, including PKCE.
  </Card>

  <Card title="POST /oauth/token" icon="key" href="/api-reference/simkl/exchange-token">
    Endpoint reference — body fields, response shape, error codes, interactive playground.
  </Card>
</CardGroup>
