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

# Set up authentication

> Register an app, pick a flow, choose a scope — everything you decide once, before you write any auth code.

Every API call that touches user data needs a user `access_token`. This page covers the four things you decide **before** writing any code, in the order you hit them.

Two of those decisions are easy to confuse, because the numbers differ:

| Decision | How many | Decided by |
| - | -: | - |
| **Client type** — how you register the app | 3 | Where your code runs, and whether it can keep a secret |
| **Flow** — how the user signs in | 2 | What the device can do |

[Developer settings](https://simkl.com/settings/developer/) asks for the **type**. The **flow** is a choice you make in code. They are not the same list.

<a id="client-types" />

## 1. Pick a client type

**The type is not a filing category — it is the one answer that configures the registration.** Picking it decides three things at once:

* **Whether a `client_secret` is minted at all.** One type gets one; for the other two no secret is ever created, so there is nothing to leak.
* **Whether redirect URIs are stored.** Two types require at least one; the third registers none.
* **Which flows are therefore reachable.** A registration with no redirect URI has nowhere for Simkl to send the user back to, so it cannot run a browser redirect.

That is also why the choice is **fixed at registration and cannot be changed afterwards** — correcting it means registering another app. These are the labels you will see on the form, in this order:

| In developer settings, choose | `client_secret` | Redirect URI | Register this when |
| - | - | - | - |
| **Mobile, desktop & browser apps** | No | Required | Your code ships to users — mobile apps, SPAs, desktop binaries, browser extensions. Anything embedded in it must be assumed readable. |
| **TV, devices & command line** | No | Not registered | The app has no browser to redirect back into — TVs, consoles, set-top boxes, CLIs. |
| **Server apps & services** | **Yes** | Required | The code runs on a server you control, so a secret can stay private. |

<Note>
  **AUTH V1 and AUTH V2 registrations are counted separately**, so [migrating](/guides/migrating-v1-to-v2) never costs you a slot — your existing V1 apps keep working and do not use up your V2 allowance.

  The allowance for unverified apps is **3 on Free, 5 on PRO, 10 on VIP** — per version, not combined. Verified apps do not count against it at all, and **verification is free on every plan**: request it from [developer settings](https://simkl.com/settings/developer/) once your app is live.
</Note>

<a id="client-secret" />

## 2. Decide whether you need a `client_secret`

**Only Server apps & services has one, and only because a backend can keep it.** It is not a capability upgrade and unlocks nothing — the other two types are fully functional without one.

### What the secret actually does

It answers one question at the token endpoint: *is this request really coming from your backend?* Your `client_id` is public, so on its own it proves nothing. The secret is the proof, and it is why a server app can be held to a stricter standard than an app running on a stranger's phone.

It is deliberately **not** a key to anything by itself. There is no app-only grant — `client_credentials` returns `400 unsupported_grant_type` — so a secret with no authorization code and no refresh token alongside it reaches nothing user-specific.

### When you send it

Once you are registered as **Server apps & services**, sending the secret is **not optional** — it is required on every request that authenticates your app:

| Request | Secret |
| - | - |
| `GET /oauth2/authorize` | **No.** This is a browser redirect; a secret must never travel through a user agent. |
| `POST /oauth2/device` | **No.** Starting a device flow takes only `client_id`. |
| `POST /oauth2/token` | **Yes** — on all three grants: authorization code, refresh, and device code. |
| `POST /oauth2/revoke` | **Yes.** |

Two transports are accepted, and they are equivalent — pick whichever your HTTP library makes easy:

```bash theme={"theme":{"light":"github-light","dark":"vesper"}}
# Either: HTTP Basic (RFC 6749 §2.3.1)
curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" https://api.simkl.com/oauth2/token ...

# Or: in the request body
curl -d client_id=YOUR_CLIENT_ID -d client_secret=YOUR_CLIENT_SECRET https://api.simkl.com/oauth2/token ...
```

<Note>
  **Client authentication is checked before anything else in the request.** A missing or wrong secret is `401 invalid_client`, and you get it regardless of what else might be wrong — so during setup, fix `invalid_client` first and re-read the rest of the error only after it clears.

  The upside: a `401 invalid_client` **does not consume your authorization code.** The code is never reached, so you can fix the secret and retry the same one.
</Note>

<Tip>
  **If the secret leaks, rotate it — do not register a new app.** Developer settings regenerates it; the new one is shown once, the old one dies immediately, and **existing user grants survive**, so nobody has to re-authorize. Rotating is what stops a leaked secret being used to refresh.
</Tip>

<Warning>
  **Never ship a `client_secret` in anything a user can install.** Mobile binaries, JavaScript bundles and extension packages can all be unpacked. If your code runs on someone else's device, register **Mobile, desktop & browser apps** and rely on PKCE — that is exactly what it is designed for.
</Warning>

Your `client_id` is **public and safe to commit** — in source, in a JS bundle, in a public repo. Under V2 it reaches nothing user-specific on its own. Only the secret and user tokens need protecting, and those are [revoked automatically if they reach GitHub](/api-reference/auth-v2#what-you-get-back), private repositories included.

<a id="choose-a-flow" />

## 3. Pick a flow

<Columns cols={2}>
  <Card title="OAuth flow" icon="lock" href="/api-reference/oauth2-authorization-code" horizontal>
    Web, mobile, SPA, desktop, extensions — anything that can open a browser.
  </Card>

  <Card title="Device / PIN flow" icon="tv" href="/api-reference/oauth2-device" horizontal>
    TVs, consoles, watches, CLIs — anything that can't. 8-character code.
  </Card>
</Columns>

Both run on [AUTH V2](/api-reference/auth-v2) and both require PKCE.

| Client type | OAuth flow | Device / PIN flow |
| - | - | - |
| **Mobile, desktop & browser apps** | Yes | Yes |
| **TV, devices & command line** | **No** — no redirect URI is registered, so there is nowhere to send the user back to | Yes |
| **Server apps & services** | Yes, sending the secret at the token endpoint | Yes |

**Any type can use the Device / PIN flow.** Choose **TV, devices & command line** only if that is the *only* flow your app will ever use — if you also ship a web or mobile companion, register **Mobile, desktop & browser apps** and use both flows from it.

<Note>
  **A `client_secret` is not a third flow.** A Server apps & services app runs the OAuth flow like everyone else — same authorize redirect, same PKCE, same code. The only difference is that the secret travels alongside the `code` at the token endpoint. Every access token belongs to a user who authorized it, whichever type you registered.
</Note>

## 4. Choose a scope

| Scope | Grants |
| - | - |
| `media:read` | Read the user's watchlist, history, ratings and lists |
| `media:read media:write` | The above, plus adding, rating, scrobbling and check-ins |

<Warning>
  **Omitting `scope` gives you read-only.** V2 defaults to `media:read`, so a write request on a token minted without an explicit scope fails with `403 insufficient_scope`. Ask for `media:read media:write` up front if your app writes — widening later means sending the user through consent again.
</Warning>

Full detail, including how to handle an insufficient scope at runtime, is on [Scopes](/api-reference/oauth2-scopes).

## Point your app at the endpoints

Four URLs, on two different hosts. **Setting them directly is a perfectly normal way to configure an app** — most of our examples do exactly that:

| Purpose | URL | Metadata field |
| - | - | - |
| Consent page (browser) | `https://simkl.com/oauth2/authorize` | `authorization_endpoint` |
| Token exchange and refresh | `https://api.simkl.com/oauth2/token` | `token_endpoint` |
| Start a device flow | `https://api.simkl.com/oauth2/device` | `device_authorization_endpoint` |
| Revoke a token | `https://api.simkl.com/oauth2/revoke` | `revocation_endpoint` |

<Warning>
  **Only the consent page is on `simkl.com`.** The other three are on `api.simkl.com`, and pointing the authorize URL at the API host is the single most common wiring mistake — it returns a 404.
</Warning>

### Or let the library fetch them

Simkl publishes an [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) discovery document at [`/.well-known/oauth-authorization-server`](https://simkl.com/.well-known/oauth-authorization-server). It carries all four URLs under the metadata names above, plus the supported auth methods, scopes and PKCE methods.

**Neither approach is more correct.** Hardcoding is fine and is what the rest of these docs show. Discovery is worth it when your library already speaks it — you configure one setting instead of four, and you pick up any future endpoint change without a release:

```js theme={"theme":{"light":"github-light","dark":"vesper"}}
import * as client from "openid-client";

// Fetches https://simkl.com/.well-known/oauth-authorization-server and returns a
// config with the four URLs above already filled in.
const config = await client.discovery(
  new URL("https://simkl.com"), // the issuer — the only URL you supply
  "YOUR_CLIENT_ID",
  undefined,                    // no client_secret (Mobile, desktop & browser apps)
  client.None(),                // ...so authenticate with client_id alone
  { algorithm: "oauth2" },      // read the OAuth 2.0 document, not OpenID Connect
);
```

<Warning>
  **If your app has no `client_secret`, pass `client.None()`.** Left out, `openid-client` defaults to `client_secret_post` and tries to send a secret you do not have. A **Server apps & services** app does the opposite — pass the secret as the third argument and leave the fourth one out:

  ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
  const config = await client.discovery(
    new URL("https://simkl.com"),
    "YOUR_CLIENT_ID",
    "YOUR_CLIENT_SECRET",         // third argument is a client_secret shorthand
    undefined,                    // defaults to client_secret_post, which is correct here
    { algorithm: "oauth2" },
  );
  ```
</Warning>

<Note>
  **If your library asks for `/.well-known/openid-configuration`, point it at OAuth 2.0 instead.** Simkl is an OAuth 2.0 server, not an OpenID Connect provider, so that path returns `404` and the document lives at `/.well-known/oauth-authorization-server`. Libraries that walk both paths in turn — Spring Security, for one — sort this out themselves. A `404` during setup means yours does not, and needs telling.
</Note>

Either way you still choose the client type, the secret, the flow and the scope yourself — discovery configures endpoints, not decisions. [Setup snippets for Node, Python and Java](/api-reference/auth-v2#discovery) are on the V2 overview.

## What you will be working with

| Credential | Lifetime | Notes |
| - | - | - |
| Access token | 7 days | Short by design. Refresh rather than re-authorizing. |
| Refresh token | 180 days, **sliding** | The clock resets on every use, so an app in regular use never needs re-consent. |
| Authorization code | 10 minutes | Single-use, and consumed even by a failed exchange. |
| Device code | 15 minutes | Poll every 5 seconds. |

See [Tokens and refresh](/api-reference/oauth2-tokens) for the refresh grant, revocation, and the two refresh behaviours that surprise people.

<Note>
  **Check one thing before you build: V2 apps must send a user token on everything except plain catalog lookups.** If your app has a catalog or browsing mode that works with no signed-in user, read the [V2 overview](/api-reference/auth-v2) first — search in particular needs a token.
</Note>

<Warning>
  **AUTH V1 is being retired.** Apps on it are expected to stop working around April 2027, so starting a new integration there means doing this work twice.

  If you already ship a V1 app it keeps working for now, and [Migrating from V1 to V2](/guides/migrating-v1-to-v2) is the path off. That window is comfortable for most apps; if it is not comfortable for yours, [talk to us](https://discord.gg/MJsWNE4) rather than running out of time.
</Warning>

## Next

<CardGroup cols={2}>
  <Card title="OAuth flow" icon="lock" href="/api-reference/oauth2-authorization-code">
    The browser flow end to end, with PKCE.
  </Card>

  <Card title="Device / PIN flow" icon="tv" href="/api-reference/oauth2-device">
    Show a code, poll until the user approves.
  </Card>

  <Card title="Client libraries" icon="puzzle-piece" href="/api-reference/oauth2-libraries">
    What to configure in a stock OAuth library, and the three whose defaults break.
  </Card>

  <Card title="AUTH V2 overview" icon="shield-check" href="/api-reference/auth-v2">
    Discovery, anonymous limits, and the endpoint reference.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/conventions/errors">
    Every error code, including the two different 401s.
  </Card>

  <Card title="Scopes" icon="list-check" href="/api-reference/oauth2-scopes">
    The full `media:read` / `media:write` reference.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.