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

# Migrating from AUTH V1 to V2

> Register a V2 app, update authentication, and reconnect existing users before AUTH V1 retires around April 2027.

AUTH V1 has supported Simkl integrations for more than ten years. This guide takes you through the changes, the implementation steps, and a staged rollout for existing users.

<Warning>
  **AUTH V1 is expected to stop working around April 2027.** The exact date is not fixed. We will announce it in advance here and in [Discord](https://discord.gg/MJsWNE4) `#api`.

  **Need more time or help with a blocker?** Contact [support](/support) or Discord. We can help with the transition, raise limits while you run both versions, or extend your timeline where needed.
</Warning>

## Before you start

* **Register a new app.** Your V1 `client_id` cannot be upgraded. You need a new `client_id` and a [fixed client type](#register-your-v2-app).
* **Plan for user action.** Every existing user must [authorise your V2 app once](#migrating-existing-users). V1 tokens cannot be exchanged for V2 tokens.
* **Check browsing before sign-in.** Public catalogue lookups still work anonymously, but [search and user-scoped requests need a token](#deal-with-anonymous-access-first).

Start with [the migration steps](#the-work-in-order), then [plan how to reconnect existing users](#migrating-existing-users).

## What you are gaining

| Area                    | AUTH V1                                                   | AUTH V2                                                                                                            |
| ----------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Daily quota             | One allowance shared by the app                           | An allowance for each user, based on their plan                                                                    |
| Public client ID        | Can be used to spend the app's allowance                  | Public catalogue access only; user data requires a token                                                           |
| Access token lifetime   | 5 years                                                   | 7 days                                                                                                             |
| Revocation              | No token-revocation endpoint                              | `POST /oauth2/revoke` invalidates one connection's tokens                                                          |
| Permissions (scopes)    | Full read and write access                                | `media:read` = read-only; `media:write` = read and write                                                           |
| OAuth libraries         | Manual endpoint configuration and Simkl-specific handling | Compatible libraries load endpoints from [discovery metadata](/api-reference/auth-v2#discovery)                    |
| TV and console sign-in  | Bespoke PIN flow                                          | Standard device sign-in (RFC 8628), with a QR-friendly link and a typo-tolerant code                               |
| Same app on two devices | Same account gets the same access token on both           | Each device that signs in gets its own access and refresh tokens; revoking one connection leaves the other working |
| New APIs                | Existing API surface                                      | [Custom Lists](/guides/custom-lists) and future APIs                                                               |

[Custom Lists](/guides/custom-lists) is the first API that requires AUTH V2. New APIs will also require V2.

<AccordionGroup>
  <Accordion title="How per-user quotas change capacity">
    **The quota change is the one that matters as you grow.** A V1 app has a fixed ceiling however many users it has, so one heavy user or a runaway sync loop eats the budget everyone else needed. In V2 ten thousand users bring ten thousand allowances — capacity grows with your user base rather than being divided between it, and a noisy user can no longer affect anyone else. See [Rate limits](/resources/rate-limits).
  </Accordion>

  <Accordion title="Client IDs, secrets and token security">
    **That is also why V2 client IDs are safe to publish.** Every user-scoped V2 endpoint refuses a bare `client_id`, so there is no library to read, nothing to write, and no per-user quota to burn — ship yours in your source code, your JavaScript bundle or a public repository, as intended for every client type. All a stranger gets is public catalog data that is free and uncounted anyway.

    <Warning>
      **Your `client_secret` and user tokens are a different matter.** Never commit those. They are found and revoked automatically if they reach GitHub, **including private repositories**.
    </Warning>

    **The security story is better in both directions.** A V2 access token that leaks is useless after seven days, where a V1 token would have stayed valid for five years. PKCE makes a stolen authorization code unusable without the matching verifier. And if your `client_secret` ever leaks you can rotate it from developer settings without disturbing a single user — existing grants keep working, because the secret only authenticates your app at the token endpoint.
  </Accordion>

  <Accordion title="Configure an OAuth library with discovery">
    **The library setup is also simpler.** Simkl publishes a small JSON file listing its own endpoints and capabilities — the standard "discovery document" at [`/.well-known/oauth-authorization-server`](https://simkl.com/.well-known/oauth-authorization-server). Most OAuth libraries know how to read it.

    So instead of setting the authorize URL, the token URL and the supported auth methods yourself, you give the library **one** setting — `https://simkl.com` — and it fetches the rest. Libraries usually call that setting the **issuer**:

    ```js theme={"theme":{"light":"github-light","dark":"vesper"}}
    // Instead of configuring three or four endpoints by hand (app with no secret):
    const config = await client.discovery(
      new URL("https://simkl.com"),
      "YOUR_CLIENT_ID",
      undefined,
      client.None(),
      { algorithm: "oauth2" },
    );
    ```

    **[Setup snippets for Node, Python and Java](/api-reference/auth-v2#discovery)** are on the V2 overview, along with the one flag some libraries need to look for the OAuth 2.0 document rather than the OpenID Connect one.

    If your library does not support discovery, nothing is lost — set the two endpoints directly as you do today.
  </Accordion>
</AccordionGroup>

## The one thing that might be hard

**Decide how browsing before sign-in will work.** V2 limits anonymous requests to public catalogue data.

| Without a user token                        | Requires a user token                    |
| ------------------------------------------- | ---------------------------------------- |
| Movie, show and anime summaries by Simkl ID | Search, including title searches         |
| Episode lists                               | User-scoped data and all other endpoints |
| Trending and Calendar data files            |                                          |

<Warning>
  **Search is not an anonymous catalogue lookup.** A browse mode that accepts a title search needs sign-in, even though fetching a known title by Simkl ID still works.
</Warning>

Review [anonymous access](#deal-with-anonymous-access-first) before planning the implementation. If this makes migration difficult, [tell us](https://discord.gg/MJsWNE4); temporary limit increases or more time may help.

## What changes

| Change                    | V1                                          | V2                                                                               |
| ------------------------- | ------------------------------------------- | -------------------------------------------------------------------------------- |
| Access token lifetime     | 5 years                                     | **7 days**                                                                       |
| Refresh                   | None                                        | `refresh_token` grant; [180 days, renewed on use](/api-reference/oauth2-tokens)  |
| PKCE                      | Optional, `S256` or `plain`                 | **Mandatory**, `S256` only                                                       |
| Scopes                    | None, every token is full-access            | `media:read` / `media:write`, **defaults to read**                               |
| Anonymous requests        | Allowed on every endpoint, with a daily cap | **Catalog lookups only** — everything else needs a user token                    |
| Repeat authorisation      | Same token for the same app and user        | New access and refresh tokens on every sign-in                                   |
| `token_type`              | `bearer`                                    | `Bearer` — [check response comparisons](#traps-specific-to-migrating)            |
| Input-constrained devices | PIN flow, 5-character code                  | Device flow (RFC 8628), 8-character code                                         |
| Client type               | Implicit                                    | Fixed at registration — [three to choose from](/api-reference/auth#client-types) |
| Endpoints                 | `/oauth/*`                                  | `/oauth2/*`                                                                      |

## What does not change

Everything that is not authentication. The API itself is identical — same endpoints, same request and response shapes, same conventions. A V2 token goes in the same `Authorization: Bearer` header a V1 token did, and every call you already make keeps working unchanged once it carries one.

So the migration is genuinely scoped to your auth code. You are not rewriting your sync logic.

The one adjacent thing that *does* change is **how your quota is counted** — per user rather than per app. That needs no code from you, and it is a change in your favour, but it is worth knowing about if you have capacity monitoring built around the old model. See [Rate limits](/resources/rate-limits).

## The work, in order

<Steps titleSize="h3">
  <Step title="Register your V2 app" id="register-your-v2-app">
    [Register a new app](https://simkl.com/settings/developer/) to get a **new `client_id`**, plus a new `client_secret` if you register it as a server app. Your V1 `client_id` cannot be upgraded.

    <Note>
      V1 credentials are rejected by `/oauth2/*` endpoints with `401 invalid_client`: "This client\_id is not enabled for OAuth 2.0".
    </Note>

    This is a second app, not an edit to the first. **Your V1 app keeps working the whole time**, which is what lets you run both side by side while users move across.

    <Note>
      **Registering the V2 app does not use up a V1 slot, or the reverse.** The unverified-app allowance is counted separately for each version — 3 on Free, 5 on PRO, 10 on VIP, per version — so you never have to delete a working V1 app to make room for the V2 one.

      Verified apps do not count at all, and verification is free. If you are running several V1 apps and need headroom on either side while you migrate, [ask us](https://discord.gg/MJsWNE4).
    </Note>

    Client type is fixed at registration and cannot be changed either, so pick deliberately:

    | Label in developer settings        | Choose it for                                                                                  |
    | ---------------------------------- | ---------------------------------------------------------------------------------------------- |
    | **Mobile, desktop & browser apps** | Mobile, SPA, desktop and extensions. No secret.                                                |
    | **TV, devices & command line**     | TVs, consoles and CLIs using only the device flow. No redirect URI or authorisation code flow. |
    | **Server apps & services**         | Server-side apps. The secret never ships to a user.                                            |

    Note that **any** V2 client type can use the device flow — **TV, devices & command line** is not a permission to use it, it is the type for an app that never performs a browser redirect. If you have a companion app that signs users in through a browser, register **Mobile, desktop & browser apps** and use both flows from it.

    <Note>
      **Ship the new `client_id` to your users before you need it.** Because it is a different credential, a client that only learns the V2 `client_id` in a later release cannot migrate anyone still running an older build. If your app is distributed rather than server-side, getting the new ID into the field early is the thing most likely to determine whether you finish on time.
    </Note>
  </Step>

  <Step title="Decide what happens to anonymous access" id="deal-with-anonymous-access-first">
    Settle this before you write anything. It is the one item that can change your product rather than your code, and the answer may affect everything below it.

    V1 lets an app make `client_id`-only requests with no signed-in user on **any** endpoint, within a daily cap. V2 narrows that to public catalog data only — summaries by Simkl ID, episode lists, and the Trending and Calendar files. A user-scoped request without a token returns:

    ```json theme={"theme":{"light":"github-light","dark":"vesper"}}
    {
      "error": "user_token_required",
      "code": 401,
      "message": "This client_id requires a user access token on every request"
    }
    ```

    **Search is the one that catches people out.** It is not in the exempt set, so a browse mode where the user types a title stops working before sign-in, even though fetching a known title by ID still succeeds. If your pre-sign-in experience needs more than known-ID lookups, you either require sign-in for it, or keep that path on your V1 app while the authenticated paths move to V2. Both are legitimate.
  </Step>

  <Step title="Add PKCE" id="add-pkce">
    Mandatory for every client type in V2, including server apps. If you used V1's optional PKCE with `S256` you already have this and it carries over unchanged. If you used `plain`, or skipped PKCE, this is new code — see [PKCE in AUTH V2](/api-reference/oauth2-pkce).
  </Step>

  <Step title="Ask for the scope you need" id="request-scopes">
    V1 tokens do everything, so there was nothing to request. In V2, **omitting `scope` gives you read-only**, and so does misspelling it. If your app writes, pass `scope=media:read media:write` and then check the `scope` in the token response.

    A migration that appears to succeed and then fails on the first write is usually this.
  </Step>

  <Step title="Add refresh handling" id="add-refresh-handling">
    A 7-day token needs refreshing; a 5-year one did not. Refresh on a `401` or shortly before expiry.

    **Refreshing replaces the access token for that connection only.** Two devices that sign in separately can refresh independently, even when they use the same app and Simkl account.

    If you copy the same tokens between devices or processes, they share one connection. Independent refreshes can then invalidate each other's access tokens. Have each device sign in separately, or coordinate refreshes through one owner. See [Tokens and refresh](/api-reference/oauth2-tokens).
  </Step>

  <Step title="Swap the endpoints" id="swap-the-endpoints">
    The hosts stay the same: authorisation uses `https://simkl.com`; token, device and revocation requests use `https://api.simkl.com`.

    | V1 method and path                 | V2 method and path                  |
    | ---------------------------------- | ----------------------------------- |
    | `/oauth/authorize` *(browser URL)* | `/oauth2/authorize` *(browser URL)* |
    | `POST /oauth/token`                | `POST /oauth2/token`                |
    | `GET /oauth/pin`                   | `POST /oauth2/device`               |
    | —                                  | `POST /oauth2/revoke`               |
  </Step>
</Steps>

## Migrating existing users

There is no token exchange — a V1 token cannot be swapped for a V2 one. **Every existing user has to authorize your V2 app once**, which makes this the part of the migration that depends on your users rather than on you.

**Two ways to get there**, and they are both supported. Pick whichever suits your app — nothing in Simkl requires you to carry two authentication paths.

| Approach                                        | What a user experiences                       | What it means for your code                           |
| ----------------------------------------------- | --------------------------------------------- | ----------------------------------------------------- |
| **[Run both client IDs](#run-both-client-ids)** | Nothing, until they choose to reconnect       | Both token formats live side by side until V1 retires |
| **[Cut over to V2 only](#cut-over-to-v2-only)** | Signed out once, on the release that ships it | One authentication path from day one                  |

<a id="run-both-client-ids" />

### Run both client IDs side by side

**Ship the V2 `client_id` alongside the V1 one and keep both live.** Nothing in Simkl objects to this: the two registrations are entirely separate apps, each with its own grants, so a user can hold a V1 token from one and a V2 token from the other at the same time with no conflict. Revoking one leaves the other untouched.

That gives you the shape that makes this painless:

| User group                             | Behaviour                                                                            |
| -------------------------------------- | ------------------------------------------------------------------------------------ |
| **Users who have already reconnected** | Their V2 token is used. Nothing to do.                                               |
| **Users who have not**                 | Their existing V1 token keeps working exactly as before, right up to the retirement. |
| **New sign-ins**                       | Go straight to V2. Never mint another V1 token.                                      |

Your app runs on whichever credential it holds for that user, and the prompt to reconnect becomes an invitation rather than a wall. If you take this route, **get both credentials working before you write any of the prompt UI** — once they do, the deadline stops being a cliff.

<Note>
  **You can tell the two tokens apart locally, with no API call.** A V2 access token starts with `simkl_at_` and is 43 characters. A V1 token is 64 lowercase hex characters with no prefix. So `token.startsWith("simkl_at_")` is enough to decide which code path a given user is on, which credential to refresh, and whether to show the prompt.

  Send tokens exactly as issued — token matching is case-sensitive, so upper- or lower-casing a stored token will silently fail to authenticate.
</Note>

<Warning>
  **Watch for response-shape differences between your two registrations.** Some response fields are keyed to how old the registered app is, not to the auth version — most visibly, older apps receive `notinteresting` everywhere newer apps receive `dropped`. See [List statuses](/conventions/list-statuses).

  Your V1 app may be old enough to sit on one side of that line while your brand-new V2 registration sits on the other. **The same endpoint can then return a different shape depending on which credential made the call.** Parse both forms while you run the two side by side, rather than assuming the response matches what your V1 app has always seen.
</Warning>

### When to prompt

Start quiet and escalate. A reconnect prompt gets roughly one chance at being taken seriously, so spending it early — while everything still works and the user has no reason to act — is how it ends up ignored.

| When                                               | Prompt behaviour                                                                              |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| **On release: silent**                             | New sign-ins use V2. Existing users carry on without a prompt.                                |
| **About 6 months before retirement: discoverable** | Add a settings status row and one dismissible card.                                           |
| **About 3 months before: recurring**               | Show a dismissible banner no more than every couple of weeks.                                 |
| **About 1 month before: persistent**               | Keep the banner visible. Notify users of unattended integrations through an existing channel. |
| **At retirement**                                  | Degrade affected features and keep the reconnect button easy to reach.                        |

**Never interrupt a task.** Prompt on launch, on a settings screen, or after something completes — not in front of the thing the user came to do.

### Where the prompt goes, by app type

<Tabs>
  <Tab title="Mobile / desktop">
    Put a permanent row in your existing account or settings screen — *"Simkl: connected (legacy) — Reconnect"*. That row is the canonical destination, and it matters even when nobody clicks it: a suspicious user who distrusts a banner goes looking for it there, and needs to find it.

    The banner is the express lane; its button opens the authorization flow directly rather than dumping the user on a settings page to hunt for a second button.

    Use the platform's secure web-auth session — `ASWebAuthenticationSession` on iOS, Custom Tabs on Android — never an embedded WebView. On iOS, do not suppress the system's *"…Wants to Use simkl.com to Sign In"* prompt: it is chrome your app cannot spoof, which is exactly what makes it reassuring.
  </Tab>

  <Tab title="Web">
    An inline banner above the main content, plus the settings row. Avoid a modal until the last few weeks — modals used for non-urgent things train people to dismiss them without reading, which costs you the one that matters.

    Keep the reconnect on the same origin the user is already on, and send them to `simkl.com` in a normal top-level navigation.
  </Tab>

  <Tab title="TV / device">
    Use the [device flow](/api-reference/oauth2-device) for the reconnect exactly as you would for a first sign-in. Show the short code and `verification_uri`, and render `verification_uri_complete` as a **QR code** as well — typing a URL on a remote is the main reason people abandon halfway.

    Show a countdown against `expires_in` and restart automatically when the code expires rather than leaving a dead code on screen. Put the reconnect entry point in your settings area too, so a user who dismissed the prompt can come back to it.
  </Tab>

  <Tab title="Headless">
    Media-server plugins, scrobblers, bots and cron jobs have no one watching at the moment the token matters, so an in-app banner reaches nobody.

    Surface it wherever the user already administers the thing — the plugin's settings page in Plex/Jellyfin/Kodi, a log line, a startup warning — and use whatever out-of-band channel you already have (the bot's own DM, an email, a notification) as the deadline approaches. Then **stop retrying on a loop**: a dead token does not recover by being asked repeatedly.
  </Tab>
</Tabs>

### Make the prompt trustworthy

This is the part most apps get wrong, and it costs them conversions. *"Please sign in again"* is also what credential phishing looks like, and users have been trained to distrust it. Being genuine is not enough on its own — you have to look genuine.

1. **Explain the reason.** Say *"Simkl is retiring the login method this app uses"* rather than the vague *"your session has expired"*.
2. **Name the destination.** Explain that sign-in opens **simkl.com** in the browser and that your app never sees the password.
3. **Explain what stays.** Reassure users that their watch history stays where it is.
4. **Give a clear timeframe.** Use the announced retirement date when available. Explain what the user needs to do without alarmist wording.

**Do not claim a cause you have not verified.** A `401` can mean the user revoked you from [Connected Apps](https://simkl.com/settings/connected-apps/), or that something transient went wrong. If you show *"Simkl retired this login"* to someone who simply disconnected your app, you are both wrong and alarming. Branch on what you actually know, and keep a neutral message for the rest.

<Note>
  **Your app will appear twice in the user's Connected Apps list** while both registrations hold a grant — the two client IDs are separate apps as far as Simkl is concerned. That is expected and harmless, but it surprises people, so it is worth a line in your own help text. Once a user has reconnected, [revoke](/api-reference/oauth2-tokens#revoking) the V1 token you are no longer using and the duplicate disappears.
</Note>

### After the deadline

Degrade by capability, not by locking the app.

* **Keep local data.** Cached lists, history and posters stay visible, marked stale. Losing a token is not losing the account — do not log the user out of your own app, and do not clear the cache.
* **Queue writes instead of failing them.** *"12 changes waiting to sync — reconnect to send them"* is a reason to act, and converts far better than a warning. Never discard the queue.
* **Suspend background sync** rather than retry-storming a token that cannot come back.
* **Check the account matches on reconnect.** If the user authorizes a *different* Simkl account, do not silently merge — the queued writes belong to the old one. Ask them.
* **Keep the reconnect path forever.** People come back months later.

<Note>
  **Watch your own numbers rather than guessing.** You can see how much of your user base has moved by how many V2 grants your app holds. If the curve looks like it will not finish in time, [tell us](https://discord.gg/MJsWNE4) — we would much rather extend your timeline than break your users.
</Note>

<a id="cut-over-to-v2-only" />

### Cut over to V2 only

**Ship a release that only knows V2, and have everyone reconnect at once.** No dual credential handling, no prompt schedule, no window where the same endpoint answers two shapes — the V1 path goes away with the migration itself.

**What you get:** one authentication path from day one. Carrying two token formats, two refresh behaviours and two response shapes through a codebase is real work, and it is work with a known expiry date — especially if you are rewriting the auth layer anyway.

**What it costs:** every user hits a signed-out state at the same time. A reconnect screen nobody explained looks identical to a bug, and users who quietly stop opening the app do not appear in any error log. Anything running unattended — a media-server plugin, a scrobbler, a bot, a cron job — stops at the moment of the update and stays stopped until someone notices.

Four things make it go smoothly:

1. **Explain it before they see it.** Release notes, and a line on the reconnect screen itself: *"Simkl has upgraded its sign-in. Reconnect once and you are done."* Without a reason, the screen reads as a failure.
2. **Do not treat a missing token as a missing account.** Keep cached lists, history and queued writes exactly as [After the deadline](#after-the-deadline) describes. Reconnecting should restore a working app, not an empty one.
3. **Leave the V1 registration alone.** Delete it later, and only once the old builds are genuinely gone. Every user still on the previous version depends on it, and users update slowly — a mobile or desktop app typically has a long tail running releases many months old.
4. **Expect the support spike in one place.** It arrives on release day rather than spread over months, which is easier to staff for but harder to ignore. Have the answer written before you ship.

**You can also change your mind halfway.** Shipping the V2-only release to new installs while older builds keep running on V1 is the side-by-side model arriving by a different route — the V1 registration stays live, and the two populations simply differ by app version rather than by stored credential.

## Traps specific to migrating

### Keep discovery tied to V2

<Warning>
  **Do not point your V1 app at the discovery document.** `https://simkl.com/.well-known/oauth-authorization-server` describes **V2**. A V1 app that auto-configures from it will end up calling V2 endpoints, which reject V1 client IDs with `invalid_client` and a message about OAuth 2.0 not being enabled — confusing, since you are already using OAuth.

  Use it for your V2 app, where it is accurate and genuinely useful. Configure V1 endpoints explicitly.
</Warning>

### Compare token types without case sensitivity

<Warning>
  **`token_type` changed case — in the response only.** V1 returns `"bearer"`, V2 returns `"Bearer"`. If you kept code that compares this string exactly, and you now support both, compare case-insensitively: [RFC 6749 section 5.1](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1) defines the value as case-insensitive, so exact-matching it was always a latent bug.

  **Nothing changes about what you send.** The `Authorization` scheme name is case-insensitive per [RFC 7235](https://datatracker.ietf.org/doc/html/rfc7235#section-2.1), and Simkl accepts `Bearer`, `bearer` or any other casing on every request, V1 and V2 alike. You do not need to touch your request code — only a comparison against the response field, if you have one.
</Warning>

### Handle repeat authorisation as a new grant

<Note>
  **Each V2 sign-in creates a separate connection, called a grant.** If a user signs into the same app on their phone and tablet, each device gets its own access and refresh tokens. Refreshing or revoking the phone's grant leaves the tablet's grant working.

  This depends on each device signing in separately. Copying the same tokens between devices shares one grant instead. [Revoke a grant](/api-reference/simkl/oauth2-revoke) when that connection is no longer needed.
</Note>

## See also

<CardGroup cols={2}>
  <Card title="AUTH V2 overview" icon="shield-halved" href="/api-reference/auth-v2">
    Client types, lifetimes, and which flow to use.
  </Card>

  <Card title="OAuth flow" icon="lock" href="/api-reference/oauth2-authorization-code">
    The V2 replacement for the V1 OAuth flow.
  </Card>

  <Card title="Device / PIN flow" icon="tv" href="/api-reference/oauth2-device">
    The V2 replacement for the PIN flow.
  </Card>

  <Card title="AUTH V1" icon="clock-rotate-left" href="/api-reference/auth-v1">
    What you are migrating from. Being retired around April 2027.
  </Card>
</CardGroup>
