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

# Choose a flow

> OAuth or PIN — pick the one that matches your platform and ship in under an hour.

Every API call that touches user data needs a user `access_token`. Simkl gives you three ways to get one — they're all variants of OAuth 2.0 from the user's perspective, but the integration story is very different. Pick the one that matches the device your app runs on; once you have a token, the rest of the API is identical.

<Columns cols={3}>
  <Card title="OAuth 2.0" icon="lock" href="/api-reference/oauth" horizontal>
    For **server-side web apps** that can keep a `client_secret`. The user logs in via their browser; your backend exchanges the `code` for a token.
  </Card>

  <Card title="Public PKCE" icon="shield-keyhole" href="/api-reference/oauth-pkce" horizontal>
    For **mobile, SPA, browser extensions, desktop binaries** — any client where you can't safely embed `client_secret`. Same browser-based UX, no secret required.
  </Card>

  <Card title="PIN" icon="key" href="/api-reference/pin" horizontal>
    For **TVs, consoles, watches, CLIs, and media-server plugins**. Show a 5-character code; the user enters it on their phone.
  </Card>
</Columns>

## Find your platform

<Tabs>
  <Tab title="Mobile">
    | Platform              | Use       | Recommended UI                                              |
    | --------------------- | --------- | ----------------------------------------------------------- |
    | **iOS / iPadOS**      | OAuth 2.0 | `ASWebAuthenticationSession` (iOS 12+)                      |
    | **Android**           | OAuth 2.0 | Chrome **Custom Tabs** (`androidx.browser`)                 |
    | **React Native**      | OAuth 2.0 | `expo-web-browser` or `react-native-app-auth`               |
    | **Flutter**           | OAuth 2.0 | `flutter_web_auth_2`                                        |
    | **Capacitor / Ionic** | OAuth 2.0 | `@capacitor/browser`                                        |
    | **watchOS / Wear OS** | **PIN**   | The watch shows the code; the user enters it on their phone |

    <Danger>
      **Don't use an embedded `WebView` for OAuth on mobile.** Identity providers used by Simkl's login page — **Google sign-in**, **email auth**, and others — refuse to render inside embedded WebViews (Android `WebView`, iOS `WKWebView`). Users hit a blank screen or a "this browser is not supported" error and can't log in.

      Use the platform's secure web-auth session instead — it runs in the user's real browser context, shares their existing login cookies, and is accepted by every provider:

      * **iOS / iPadOS** → [`ASWebAuthenticationSession`](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession). Mandatory — the App Store rejects the older `SFAuthenticationSession`. On iOS 17.4+, prefer the newer initializer that handles universal links cleanly.
      * **Android** → [Chrome **Custom Tabs**](https://developer.chrome.com/docs/android/custom-tabs) (`androidx.browser:browser`). For Chrome-only apps you can opt into the newer purpose-built [**Auth Tab**](https://developer.chrome.com/docs/android/custom-tabs/guide-auth-tab) — Custom Tabs remains the broadest-compatibility default.
    </Danger>
  </Tab>

  <Tab title="Web">
    | Platform                                            | Use       | How                                                                        |
    | --------------------------------------------------- | --------- | -------------------------------------------------------------------------- |
    | **SPA (React, Vue, Svelte)**                        | OAuth 2.0 | Redirect → backend exchanges `code` → backend sets httpOnly cookie         |
    | **Next.js / Remix / SvelteKit**                     | OAuth 2.0 | Server route handles the redirect and token exchange                       |
    | **Server-rendered (Rails, Django, Laravel, Rails)** | OAuth 2.0 | Standard server-side OAuth                                                 |
    | **Browser extension**                               | OAuth 2.0 | `chrome.identity.launchWebAuthFlow` / `browser.identity.launchWebAuthFlow` |
  </Tab>

  <Tab title="Desktop">
    | Platform                               | Use       | How                                                                               |
    | -------------------------------------- | --------- | --------------------------------------------------------------------------------- |
    | **macOS / Windows / Linux** native     | OAuth 2.0 | Open the **system browser**; receive the redirect via a localhost loopback server |
    | **Electron / Tauri**                   | OAuth 2.0 | System browser + loopback. Don't embed the auth page in your app window           |
    | **Cross-platform GUI (Qt, GTK, .NET)** | OAuth 2.0 | Same — system browser + loopback                                                  |
  </Tab>

  <Tab title="TV & console">
    | Platform                      | Use |
    | ----------------------------- | --- |
    | **Apple TV (tvOS)**           | PIN |
    | **Android TV / Google TV**    | PIN |
    | **Fire TV**                   | PIN |
    | **Roku**                      | PIN |
    | **Samsung Tizen / LG webOS**  | PIN |
    | **PlayStation, Xbox, Switch** | PIN |
    | **Steam Deck (gamepad mode)** | PIN |

    <Tip>
      Typing on a remote or controller is painful. PIN flow lets the user authorize from their phone in seconds.
    </Tip>
  </Tab>

  <Tab title="Headless / plugin">
    | Platform                                 | Use |
    | ---------------------------------------- | --- |
    | **CLI tool, system service, daemon**     | PIN |
    | **Plex / Jellyfin / Emby / Kodi plugin** | PIN |
    | **Discord / Slack bot, Telegram bot**    | PIN |
    | **Hardware/IoT, NAS app**                | PIN |
  </Tab>
</Tabs>

## At-a-glance comparison

|                           | OAuth 2.0                                   | Public PKCE                                                                             | PIN                                                             |
| ------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| **User experience**       | Tap login → browser → approve → back to app | Same as OAuth 2.0                                                                       | App shows code → user types it at simkl.com/pin → app continues |
| **HTTP calls**            | 1 redirect + 1 token POST                   | 1 redirect + 1 token POST                                                               | 1 code request + N polls                                        |
| **Time to token**         | \~5 seconds                                 | \~5 seconds                                                                             | 30 seconds – 2 minutes                                          |
| **Needs `client_secret`** | Yes                                         | **No** — uses `code_verifier` + `code_challenge`                                        | No                                                              |
| **Needs `redirect_uri`**  | Yes (pre-registered, byte-for-byte)         | Yes, OR omit entirely if no redirect URI is registered (consent completes on simkl.com) | No                                                              |
| **Code TTL**              | Short-lived — exchange immediately          | Short-lived — exchange immediately                                                      | 15 minutes (`expires_in: 900`)                                  |
| **Best for**              | Server-side web apps                        | Mobile, SPA, browser extensions, desktop binaries                                       | TVs, consoles, watches, CLIs, plugins                           |

## How the OAuth flow works

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

<Steps>
  <Step title="Send the user to Simkl" icon="arrow-up-right-from-square">
    Open `https://simkl.com/oauth/authorize?response_type=code&client_id=…&redirect_uri=…&app-name=my-app-name&app-version=1.0` in the user's **system browser** (or a Custom Tab / `ASWebAuthenticationSession` on mobile).

    <Warning>
      **Note the host.** This is `https://simkl.com/...`, not `https://api.simkl.com/...`. Only the *token exchange* in step 3 hits the API host.
    </Warning>

    Public clients should also send a [PKCE](/api-reference/oauth-pkce) `code_challenge` (and optional `code_challenge_method`, default `S256`) — this lets you skip `client_secret`.
  </Step>

  <Step title="User approves" icon="user-check">
    Simkl shows a consent screen. After approval, Simkl redirects to your `redirect_uri` with `?code=AUTHORIZATION_CODE` appended (and `&state=…` if you sent one).

    <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`. Your client should treat any flow where the redirect never lands as a denial / timeout: if your callback handler hasn't received a `code` within a sensible window (e.g. 5 minutes), surface a *"sign-in cancelled"* message and let the user retry.
    </Warning>
  </Step>

  <Step title="Exchange the code for a token" icon="key">
    `POST /oauth/token` with the `code`, your `client_id`, and either:

    * **`client_secret` + `redirect_uri`** (confidential clients), or
    * **`code_verifier`** (PKCE — public clients, no secret required).

    The response contains your `access_token`.

    <Warning>
      The `code` is short-lived. Exchange it immediately — don't queue it or pass it through a slow pipeline.
    </Warning>

    <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>
  </Step>

  <Step title="Store and reuse" icon="lock">
    Save the `access_token` securely. It's long-lived and only stops working when the user revokes your app from [Connected Apps settings](https://simkl.com/settings/connected-apps/). Send it as `Authorization: Bearer …` on every authenticated request.
  </Step>
</Steps>

### OAuth code samples

<CodeGroup>
  ```bash curl theme={"theme":{"light":"github-light","dark":"vesper"}}
  # Step 1 — send the user here in their browser:
  # https://simkl.com/oauth/authorize?response_type=code
  #     &client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URI
  #     &app-name=my-app-name&app-version=1.0

  # Step 2 — your redirect_uri receives ?code=AUTH_CODE.
  # Exchange it immediately:
  curl -X POST "https://api.simkl.com/oauth/token?client_id=$CLIENT_ID&app-name=my-app-name&app-version=1.0" \
    -H 'Content-Type: application/json' \
    -H 'User-Agent: my-app-name/1.0' \
    -d "{
      \"code\": \"$AUTH_CODE\",
      \"client_id\": \"$CLIENT_ID\",
      \"client_secret\": \"$CLIENT_SECRET\",
      \"redirect_uri\": \"$REDIRECT_URI\",
      \"grant_type\": \"authorization_code\"
    }"
  # → { "access_token": "...", "token_type": "bearer", "scope": "public", "expires_in": 157680000 }
  ```

  ```swift iOS (Swift) theme={"theme":{"light":"github-light","dark":"vesper"}}
  import AuthenticationServices

  let authURL = URL(string:
    "https://simkl.com/oauth/authorize?response_type=code" +
    "&client_id=\(clientId)&redirect_uri=\(redirectURI)" +
    "&app-name=my-app-name&app-version=1.0")!

  let session = ASWebAuthenticationSession(
    url: authURL,
    callbackURLScheme: "yourapp"   // matches the scheme in redirect_uri
  ) { callbackURL, error in
    guard let url = callbackURL,
          let code = URLComponents(url: url, resolvingAgainstBaseURL: false)?
            .queryItems?.first(where: { $0.name == "code" })?.value
    else { return }

    // POST code to https://api.simkl.com/oauth/token,
    // then store access_token in Keychain.
  }
  session.presentationContextProvider = self
  session.start()
  ```

  ```kotlin Android (Kotlin) theme={"theme":{"light":"github-light","dark":"vesper"}}
  // build.gradle:  implementation "androidx.browser:browser:1.7.0"
  import androidx.browser.customtabs.CustomTabsIntent

  val authUrl = Uri.parse("https://simkl.com/oauth/authorize")
    .buildUpon()
    .appendQueryParameter("response_type", "code")
    .appendQueryParameter("client_id", BuildConfig.SIMKL_CLIENT_ID)
    .appendQueryParameter("redirect_uri", "yourapp://oauth")
    .appendQueryParameter("app-name", "my-app-name")
    .appendQueryParameter("app-version", "1.0")
    .build()

  CustomTabsIntent.Builder().build().launchUrl(this, authUrl)

  // Handle the redirect via an intent-filter on yourapp://oauth,
  // then POST the ?code= to /oauth/token from your backend.
  ```

  ```js Node / TypeScript theme={"theme":{"light":"github-light","dark":"vesper"}}
  // In your /oauth/callback handler, after extracting `code` from the query:
  const params = new URLSearchParams({
    client_id:     process.env.SIMKL_CLIENT_ID,
    'app-name':    'my-app-name',
    'app-version': '1.0',
  });

  const r = await fetch(`https://api.simkl.com/oauth/token?${params}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'User-Agent':   'my-app-name/1.0',
    },
    body: JSON.stringify({
      code,
      client_id:     process.env.SIMKL_CLIENT_ID,
      client_secret: process.env.SIMKL_CLIENT_SECRET,
      redirect_uri:  process.env.SIMKL_REDIRECT_URI,
      grant_type:    'authorization_code',
    }),
  });
  const { access_token } = await r.json();
  // Set as httpOnly cookie or store server-side.
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vesper"}}
  import requests

  r = requests.post(
      'https://api.simkl.com/oauth/token',
      params={
          'client_id':   CLIENT_ID,
          'app-name':    'my-app-name',
          'app-version': '1.0',
      },
      headers={'User-Agent': 'my-app-name/1.0'},
      json={
          'code':          auth_code,
          'client_id':     CLIENT_ID,
          'client_secret': CLIENT_SECRET,
          'redirect_uri':  REDIRECT_URI,
          'grant_type':    'authorization_code',
      },
  )
  access_token = r.json()['access_token']
  ```
</CodeGroup>

### PKCE for public clients

If your app can't safely keep a `client_secret` — mobile, SPA, browser extension, desktop binary — use **PKCE** instead of the confidential flow above. The user experience is identical (browser-based OAuth); the difference is replacing the secret with a one-time `code_verifier` + `code_challenge` pair the client generates locally.

<Card title="Public PKCE — full walkthrough" icon="shield-keyhole" href="/api-reference/oauth-pkce" horizontal>
  Step-by-step PKCE flow ([RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)), per-platform recipes (iOS / Android / Web SPA / Desktop Python), common pitfalls (verifier mismatch, code expiry, redirect URI byte-for-byte rules, multi-flow verifier storage), and the "no registered redirect URI" mode Simkl supports for PKCE.
</Card>

## How the PIN flow works

<Steps>
  <Step title="Request a code" icon="qrcode">
    `GET /oauth/pin?client_id=…`. The response contains:

    ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
    {
      "result": "OK",
      "device_code": "DEVICE_CODE",
      "user_code": "ABCDE",
      "verification_uri": "https://simkl.com/pin",
      "verification_url": "https://simkl.com/pin",
      "expires_in": 900,
      "interval": 5
    }
    ```

    `user_code` is the 5-character code you show to the user. `expires_in` is 900 seconds (15 min). `interval` is 5 seconds — your polling cadence.

    <Note>
      The `device_code` field is returned as the literal string `"DEVICE_CODE"` (not a per-request device code value). It's a placeholder for compatibility with the OAuth 2.0 Device Authorization Grant response shape. Clients only need to remember `user_code` — that's what you poll on and what the user enters.
    </Note>

    <Note>
      The response also includes a `verification_url` key with the same value, kept as an alias. Read `verification_uri` — that's the RFC 8628 §3.2 spelling.
    </Note>
  </Step>

  <Step title="Show the code on screen" icon="display">
    Display `user_code` prominently. Tell the user: "Go to `simkl.com/pin` on your phone and enter `ABCDE`."
  </Step>

  <Step title="Poll while the user authorizes" icon="rotate">
    `GET /oauth/pin/{USER_CODE}?client_id=…` every `interval` seconds. There are two response shapes:

    ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
    // Still pending — keep polling
    { "result": "KO", "message": "Authorization pending" }

    // User approved — stop polling, store the token
    { "result": "OK", "access_token": "..." }
    ```

    <Warning>
      Respect the returned `interval` (5 seconds). Polling faster won't make the user enter their PIN faster. Once `expires_in` (900 seconds) elapses, the `user_code` is dead — request a fresh one and restart.
    </Warning>

    <Note>
      **Stop polling as soon as you receive the `access_token`.** After a successful authorization the server deletes the code, and any subsequent poll on the deleted (or any unknown) `user_code` falls through to the *create-a-new-code* branch — you'll get back the same shape as `GET /oauth/pin` with a brand-new `user_code`. Detect any response containing `device_code` as "the original code is gone" and stop.
    </Note>
  </Step>

  <Step title="Stop polling and save the token" icon="circle-check">
    Once you receive `access_token`, stop polling and store it. From here on, the device works exactly like an OAuth client.
  </Step>
</Steps>

### PIN code samples

<CodeGroup>
  ```bash curl theme={"theme":{"light":"github-light","dark":"vesper"}}
  PARAMS="client_id=$CLIENT_ID&app-name=my-app-name&app-version=1.0"
  UA="my-app-name/1.0"

  # Step 1 — request a code:
  curl "https://api.simkl.com/oauth/pin?$PARAMS" -H "User-Agent: $UA"
  # → { "user_code": "ABCDE", "verification_uri": "https://simkl.com/pin/",
  #     "verification_url": "https://simkl.com/pin/",
  #     "expires_in": 900, "interval": 5 }

  # Step 2 — show ABCDE to the user.

  # Step 3 — poll every 5 seconds:
  while true; do
    RESP=$(curl -s "https://api.simkl.com/oauth/pin/ABCDE?$PARAMS" -H "User-Agent: $UA")
    echo "$RESP" | grep -q access_token && { echo "$RESP"; break; }
    sleep 5
  done
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"vesper"}}
  import requests, time

  PARAMS = {
      'client_id':   CLIENT_ID,
      'app-name':    'my-app-name',
      'app-version': '1.0',
  }
  HEADERS = {'User-Agent': 'my-app-name/1.0'}

  # 1. Request a code
  pin = requests.get(
      'https://api.simkl.com/oauth/pin',
      params=PARAMS,
      headers=HEADERS,
  ).json()
  print(f"Visit {pin['verification_uri']} and enter: {pin['user_code']}")

  # 2. Poll
  deadline = time.time() + pin['expires_in']
  while time.time() < deadline:
      r = requests.get(
          f"https://api.simkl.com/oauth/pin/{pin['user_code']}",
          params=PARAMS,
          headers=HEADERS,
      ).json()
      if r.get('result') == 'OK' and 'access_token' in r:
          access_token = r['access_token']
          break
      time.sleep(pin['interval'])
  ```

  ```js Node / TypeScript theme={"theme":{"light":"github-light","dark":"vesper"}}
  const PARAMS  = `client_id=${CLIENT_ID}&app-name=my-app-name&app-version=1.0`;
  const HEADERS = { 'User-Agent': 'my-app-name/1.0' };

  // 1. Request a code
  const pin = await fetch(
    `https://api.simkl.com/oauth/pin?${PARAMS}`,
    { headers: HEADERS }
  ).then(r => r.json());

  console.log(`Visit ${pin.verification_uri} and enter: ${pin.user_code}`);

  // 2. Poll
  const deadline = Date.now() + pin.expires_in * 1000;
  while (Date.now() < deadline) {
    const r = await fetch(
      `https://api.simkl.com/oauth/pin/${pin.user_code}?${PARAMS}`,
      { headers: HEADERS }
    ).then(r => r.json());
    if (r.access_token) { /* done */ break; }
    await new Promise(res => setTimeout(res, pin.interval * 1000));
  }
  ```
</CodeGroup>

## After you have a token

<Info>
  Public endpoints (Search, Movies, TV, Anime, Ratings, Redirect) only need the [required URL parameters](/conventions/headers#required-url-parameters). Endpoints that touch user data also need `Authorization`.
</Info>

| Where                  | Value                                                           |
| ---------------------- | --------------------------------------------------------------- |
| URL params             | `client_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0` |
| `Authorization` header | `Bearer YOUR_ACCESS_TOKEN` (when token-required)                |
| `User-Agent` header    | `my-app-name/1.0`                                               |
| `Content-Type` header  | `application/json` (for POST)                                   |

```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
curl "https://api.simkl.com/sync/activities?client_id=$CLIENT_ID&app-name=my-app-name&app-version=1.0" \
  -H "User-Agent: my-app-name/1.0" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

## Token lifecycle

<AccordionGroup>
  <Accordion title="How long does a token last?" icon="clock">
    The token-mint response carries `expires_in: 157680000` — **5 years in seconds**. In practice tokens remain valid until the user revokes your app, so the lifetime advertised is more of a sentinel than a refresh hint (there's no refresh-token grant — once `expires_in` does run out, the user has to re-consent through `/oauth/authorize`). Store one and reuse it until the user revokes — see "What happens when a user revokes access?" below.
  </Accordion>

  <Accordion title="What happens when a user revokes access?" icon="ban">
    The user can revoke from [Connected Apps settings](https://simkl.com/settings/connected-apps/). After revocation, every authenticated call returns **`401 Unauthorized`**. Detect this and prompt the user to re-authorize.
  </Accordion>

  <Accordion title="Where should I store the token?" icon="lock">
    | Platform                    | Storage                                                          |
    | --------------------------- | ---------------------------------------------------------------- |
    | **iOS**                     | Keychain                                                         |
    | **Android**                 | EncryptedSharedPreferences or the Android Keystore               |
    | **macOS / Windows / Linux** | OS keychain (Keychain Access, Credential Manager, libsecret)     |
    | **Web**                     | `httpOnly` cookie set by your backend — **never** `localStorage` |
    | **CLI / server**            | A file with restrictive permissions, or a secrets manager        |
  </Accordion>

  <Accordion title="What scope do tokens have?" icon="shield">
    All tokens currently return `scope: "public"`. There's no granular permission system — every token grants every permission your app has been approved for.
  </Accordion>

  <Accordion title="What if the same user logs in twice?" icon="user-group">
    Simkl returns the **same `access_token`** both times for a given `(app, user)` pair — whether the user re-runs the standard flow, PKCE, or the PIN flow. The server tracks how often the token has been issued (an internal usage counter) but doesn't rotate the token itself. Storing the latest response is safe; you don't need to invalidate older ones because there aren't multiple ones. The token only stops working when the user revokes your app at [Connected Apps settings](https://simkl.com/settings/connected-apps/).
  </Accordion>
</AccordionGroup>

## Common pitfalls

<Danger>
  **Don't use an embedded `WebView` for OAuth on mobile.** Federated providers used by Simkl's login page — **Google sign-in**, **email auth**, and others — refuse to render inside embedded WebViews. Users see a blank screen or a "browser not supported" error and can't sign in. Use [`ASWebAuthenticationSession`](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) on iOS or [Chrome **Custom Tabs**](https://developer.chrome.com/docs/android/custom-tabs) on Android — both run in the user's real browser context and work with every provider.
</Danger>

<Warning>
  **Don't ship `client_secret` in a public binary.** Anything compiled into the user's app — mobile, desktop, browser extension, SPA — should be considered leaked. Use [**Public PKCE**](/api-reference/oauth-pkce) (`code_verifier` + `code_challenge`) instead, or use the PIN flow. Both work without a secret.
</Warning>

<Warning>
  **`redirect_uri` must match byte-for-byte.** Trailing slash, scheme (`http` vs `https`), port, casing — all of it. Mismatches return an error before the user even sees the consent screen.
</Warning>

<Warning>
  **The OAuth `code` is single-use.** Once you POST it to `/oauth/token`, the server deletes it — even if your exchange request fails (network error, validation mismatch, etc.) the code is consumed and won't work a second time. Don't log it. Don't queue it. Don't retry a failed exchange with the same code — restart the flow from `/oauth/authorize` instead.
</Warning>

<Tip>
  **Reuse the same token until it stops working.** Don't re-run the auth flow on every app launch — that's a guaranteed way to annoy users. Save it once, check on launch that it still works (any quick authenticated GET), and only re-prompt on `401` (the user revoked your app).
</Tip>

## Pick a flow

<Columns cols={3}>
  <Card title="OAuth 2.0" icon="lock" href="/api-reference/oauth">
    Confidential clients (server-side web). `client_id` + `client_secret` + `redirect_uri`. Parameter reference and example responses.
  </Card>

  <Card title="Public PKCE" icon="shield-keyhole" href="/api-reference/oauth-pkce">
    Public clients (mobile, SPA, extensions, desktop). `client_id` + `code_verifier` / `code_challenge`. Per-platform recipes.
  </Card>

  <Card title="PIN" icon="key" href="/api-reference/pin">
    Browser-less devices (TVs, consoles, watches, CLIs). Show a code, poll until the user enters it.
  </Card>
</Columns>
