Skip to main content

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.

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.

OAuth 2.0

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.

Public PKCE

For mobile, SPA, browser extensions, desktop binaries — any client where you can’t safely embed client_secret. Same browser-based UX, no secret required.

PIN

For TVs, consoles, watches, CLIs, and media-server plugins. Show a 5-character code; the user enters it on their phone.

Find your platform

PlatformUseRecommended UI
iOS / iPadOSOAuth 2.0ASWebAuthenticationSession (iOS 12+)
AndroidOAuth 2.0Chrome Custom Tabs (androidx.browser)
React NativeOAuth 2.0expo-web-browser or react-native-app-auth
FlutterOAuth 2.0flutter_web_auth_2
Capacitor / IonicOAuth 2.0@capacitor/browser
watchOS / Wear OSPINThe watch shows the code; the user enters it on their phone
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.

At-a-glance comparison

OAuth 2.0Public PKCEPIN
User experienceTap login → browser → approve → back to appSame as OAuth 2.0App shows code → user types it at simkl.com/pin → app continues
HTTP calls1 redirect + 1 token POST1 redirect + 1 token POST1 code request + N polls
Time to token~5 seconds~5 seconds30 seconds – 2 minutes
Needs client_secretYesNo — uses code_verifier + code_challengeNo
Needs redirect_uriYes (pre-registered, byte-for-byte)Yes, OR omit entirely if no redirect URI is registered (consent completes on simkl.com)No
Code TTLShort-lived — exchange immediatelyShort-lived — exchange immediately15 minutes (expires_in: 900)
Best forServer-side web appsMobile, SPA, browser extensions, desktop binariesTVs, consoles, watches, CLIs, plugins

How the 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:
EndpointHostWhat it does
GET /oauth/authorizesimkl.comBrowser-facing consent page. The user lands here, signs in, and approves your app.
POST /oauth/tokenapi.simkl.comServer-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.

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

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

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.

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 PIN flow works

Request a code

GET /oauth/pin?client_id=…. The response contains:
{
  "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.
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:
// Still pending — keep polling
{ "result": "KO", "message": "Authorization pending" }

// User approved — stop polling, store the token
{ "result": "OK", "access_token": "..." }
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

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

After you have a token

Public endpoints (Search, Movies, TV, Anime, Ratings, Redirect) only need the required URL parameters. Endpoints that touch user data also need Authorization.
WhereValue
URL paramsclient_id=YOUR_CLIENT_ID&app-name=my-app-name&app-version=1.0
Authorization headerBearer YOUR_ACCESS_TOKEN (when token-required)
User-Agent headermy-app-name/1.0
Content-Type headerapplication/json (for POST)
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

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.
PlatformStorage
iOSKeychain
AndroidEncryptedSharedPreferences or the Android Keystore
macOS / Windows / LinuxOS keychain (Keychain Access, Credential Manager, libsecret)
WebhttpOnly cookie set by your backend — never localStorage
CLI / serverA file with restrictive permissions, or a secrets manager
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 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).

Pick a flow

OAuth 2.0

Confidential clients (server-side web). client_id + client_secret + redirect_uri. Parameter reference and example responses.

Public PKCE

Public clients (mobile, SPA, extensions, desktop). client_id + code_verifier / code_challenge. Per-platform recipes.

PIN

Browser-less devices (TVs, consoles, watches, CLIs). Show a code, poll until the user enters it.