Skip to main content
AUTH V1 is the original Simkl authentication system, and it is being retired. It works today and these pages stay accurate for as long as it does — but every V1 integration needs to move to AUTH V2.
AUTH V1 is being retired. Plan to migrate.V1 apps are expected to stop working around April 2027 — about six months after the AUTH V2 release. The exact date is not fixed yet, and we will announce it here and in the Discord #api channel with notice — but do not wait for that announcement to start. Everything you need is in Migrating from V1 to V2, and for most apps the work is confined to your auth code.That window should be comfortable for almost everyone. If it is not comfortable for you, tell us rather than running out of time. We can help with the transition, raise your limits while you run both, or extend your timeline if your situation genuinely needs it. Reach us in the Discord #api channel or through support.If something about your integration makes migrating look impossible, that is exactly the case we want to hear about. The goal is for this to be painless for you and invisible to your users.
Do not start a new integration on V1. V2 is standards-compliant, so stock OAuth libraries work against it without Simkl-specific handling, and it adds scopes, refresh tokens and a proper device flow. These pages exist to keep V1 documented while you migrate off it.
Migrating means registering a new app. A V1 client_id cannot be upgraded — V2 requires a separate registration with a new client_id, and your V1 credentials are rejected by the /oauth2/* endpoints. Your V1 app keeps working while you move, so the two run side by side. Details in Migrating from V1 to V2.

The three V1 flows

OAuth (V1)

Browser-based, for server-side apps that can keep a client_secret.

Public PKCE (V1)

The same browser flow for mobile, SPA, extensions and desktop — no secret required.

PIN flow (V1)

For TVs, consoles, watches, CLIs and media-server plugins. A 5-character code.
Each card links to that flow’s endpoint reference. The walkthroughs, platform matrix and code samples for all three are further down this page.

How V1 differs from V2

Useful if you are reading both sets of pages, or deciding whether to migrate. The migration path is on Migrating from V1 to V2.
V1 tokens are long-lived, not permanent. expires_in is five years, but a user can revoke your app at any time from their Connected Apps settings. Handle a 401 by restarting the flow rather than assuming a token you hold is still good.

Find your platform (AUTH V1)

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 / iPadOSASWebAuthenticationSession. Mandatory — the App Store rejects the older SFAuthenticationSession. On iOS 17.4+, prefer the newer initializer that handles universal links cleanly.
  • AndroidChrome Custom Tabs (androidx.browser:browser). For Chrome-only apps you can opt into the newer purpose-built Auth Tab — Custom Tabs remains the broadest-compatibility default.

Comparing the three V1 flows

How the V1 OAuth flow works

Two domains, two roles. OAuth uses two different hosts — easy to mix up, and the most common cause of “404 Not Found” during integration:If your authorize URL points at api.simkl.com you’ll get a 404 — it has to be simkl.com.

Send the user to Simkl

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).
Note the host. This is https://simkl.com/..., not https://api.simkl.com/.... Only the token exchange in step 3 hits the API host.
Public clients should also send a PKCE code_challenge (and optional code_challenge_method, default S256) — this lets you skip client_secret.

User approves

Simkl shows a consent screen. After approval, Simkl redirects to your redirect_uri with ?code=AUTHORIZATION_CODE appended (and &state=… if you sent one).
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.

Exchange the code for a token

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.
The code is short-lived. Exchange it immediately — don’t queue it or pass it through a slow pipeline.
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:
For library-specific examples (Python, Node, Java, Go, PHP), see OAuth client libraries — most are zero-config.

Store and reuse

Save the access_token securely. It’s long-lived and only stops working when the user revokes your app from Connected Apps settings. Send it as Authorization: Bearer … on every authenticated request.

OAuth code samples

PKCE for public V1 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.

Public PKCE — full walkthrough

Step-by-step PKCE flow (RFC 7636), 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.

How the V1 PIN flow works

Request a code

GET /oauth/pin?client_id=…. The response contains:
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.
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.
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.

Show the code on screen

Display user_code prominently. Tell the user: “Go to simkl.com/pin on your phone and enter ABCDE.”

Poll while the user authorizes

GET /oauth/pin/{USER_CODE}?client_id=… every interval seconds. There are two response shapes:
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.
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.

Stop polling and save the token

Once you receive access_token, stop polling and store it. From here on, the device works exactly like an OAuth client.

PIN code samples

After you have a token (AUTH V1)

Public endpoints (Search, Movies, TV, Anime, Ratings, Redirect) only need the required URL parameters. Endpoints that touch user data also need Authorization.

V1 token lifecycle

The token-mint response carries expires_in: 1576800005 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.
The user can revoke from Connected Apps settings. After revocation, every authenticated call returns 401 Unauthorized. Detect this and prompt the user to re-authorize.
All tokens currently return scope: "public". There’s no granular permission system — every token grants every permission your app has been approved for.
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.

Common V1 pitfalls

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 on iOS or Chrome Custom Tabs on Android — both run in the user’s real browser context and work with every provider.
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 (code_verifier + code_challenge) instead, or use the PIN flow. Both work without a secret.
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.
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.
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).

See also

Set up authentication

Where V1 apps are heading: client type, secret, flow and scope on AUTH V2.

OAuth flow

The V1 browser flow, step by step.

Public PKCE

V1 without a client_secret, for mobile, SPA and desktop.

PIN flow

The V1 flow for TVs, consoles and CLIs.

Client libraries

Live-tested configuration for every popular OAuth library.

AUTH V2

The recommended system for new apps.