Kinde SDKs
SDKs and APIs
Kinde is designed to help founders and developers build SaaS products by providing software infrastructure like authentication, feature flags, user management, and more.
We support connecting to Kinde through our SDKs, but everything we build follows the OAuth 2.0 standard, so you can integrate Kinde into any language or framework without an SDK.
Sign in to your Kinde dashboard and select Add application
Enter a name for the application (e.g., “My App”)
Select an application type:
Select Other
Select Save.
In your app settings page, select Details
Copy your app keys:
You will need these keys to connect your codebase with Kinde.
In your app’s Details page, scroll down to the Callback URLs section
Add the following URLs:
http://localhost:3000/auth/callback)http://localhost:3000)You can add more than one callback URL per line.
Authorization servers require the callback URL to be registered ahead of time to prevent abuse. Whatever path you use for your callback URL, it must be registered with Kinde. The logout redirect URL is where Kinde redirects users after they sign out — typically your app’s homepage.
Select Save
These are the OpenID endpoints for Kinde, found at:
<YOUR_DOMAIN>/.well-known/openid-configuration
Replace <YOUR_DOMAIN> with either your custom domain (e.g., https://auth.yourbusiness.com) or your Kinde domain (e.g., https://your_business.kinde.com)
<YOUR_DOMAIN>/oauth2/auth<YOUR_DOMAIN>/oauth2/token<YOUR_DOMAIN>/oauth2/v2/user_profile<YOUR_DOMAIN>/logoutOther endpoints:
<YOUR_DOMAIN>/.well-known/jwks<YOUR_DOMAIN>/oauth2/revoke<YOUR_DOMAIN>/oauth2/introspectIn your app, generate a random state value and a nonce:
import crypto from 'node:crypto';
const state = crypto.randomBytes(32).toString('hex');const nonce = crypto.randomBytes(32).toString('hex');Store state and nonce in your server-side session — you will need them when the user returns to the callback URL.
Send the user to Kinde’s authorization endpoint with the state and nonce parameters included in the URL:
GET <YOUR_DOMAIN>/oauth2/auth?response_type=code&client_id=<your_kinde_client_id>&redirect_uri=<your_app_redirect_url>&scope=openid%20profile%20email&state=<your_random_state_value_min_8_characters>&nonce=<your_random_nonce_value>In your app, generate a random state value, a nonce, a code_verifier, and derive a code_challenge from it:
function base64UrlEncode(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (const byte of bytes) { binary += String.fromCharCode(byte); } return btoa(binary) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, '');}
const stateBytes = new Uint8Array(32);crypto.getRandomValues(stateBytes);const state = Array.from(stateBytes, (b) => b.toString(16).padStart(2, '0')).join('');
const nonceBytes = new Uint8Array(32);crypto.getRandomValues(nonceBytes);const nonce = Array.from(nonceBytes, (b) => b.toString(16).padStart(2, '0')).join('');
// 64 random bytes, base64url-encoded = 86-char code_verifierconst verifierBytes = new Uint8Array(64);crypto.getRandomValues(verifierBytes);const codeVerifier = base64UrlEncode(verifierBytes);
// SHA-256 hash of the verifier, base64url-encoded = code_challengeconst digest = await crypto.subtle.digest( 'SHA-256', new TextEncoder().encode(codeVerifier));const codeChallenge = base64UrlEncode(digest);Store state, nonce, and code_verifier in sessionStorage (for SPAs) and secure device storage (Keychain/Keystore) for mobile apps— you will need them when the user returns to the callback URL.
Send the user to Kinde’s authorization endpoint with the state, nonce, and PKCE parameters included in the URL:
GET <YOUR_DOMAIN>/oauth2/auth?response_type=code&client_id=<your_kinde_client_id>&redirect_uri=<your_app_redirect_url>&scope=openid%20profile%20email&state=<your_random_state_value_min_8_characters>&nonce=<your_random_nonce_value>&code_challenge=<code_challenge>&code_challenge_method=S256The redirect_uri is your app’s callback URL and must be registered with Kinde.
Kinde redirects the user back to your redirect_uri with an authorization code and the original state value as query parameters:
https://your-app.com/auth/callback?code=<AUTHORIZATION_CODE>&state=<your_random_state_value_min_8_characters>Validate that the returned state matches the one you generated in the previous step to prevent CSRF attacks.
Exchange the authorization code for tokens by making a POST request to the Kinde token endpoint:
POST <YOUR_DOMAIN>/oauth2/tokenContent-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=<AUTHORIZATION_CODE>&redirect_uri=<your_app_redirect_url>&client_id=<your_kinde_client_id>&client_secret=<your_kinde_client_secret>Exchange the authorization code for tokens by making a POST request to the Kinde token endpoint with the code_verifier — no client_secret needed:
POST <YOUR_DOMAIN>/oauth2/tokenContent-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=<AUTHORIZATION_CODE>&redirect_uri=<your_app_redirect_url>&client_id=<your_kinde_client_id>&code_verifier=<code_verifier>Kinde returns the tokens in the response body (same for both flows):
{ "access_token": "...", "id_token": "...", "refresh_token": "..." // only if the "offline" scope was requested}Redirect the user to a protected page in your app
Decode the id_token with any standard JWT library, or if you are using JavaScript, use the Kinde JavaScript library. Validate that the nonce claim matches the value you stored earlier to prevent replay attacks.
Use the decoded claims to display in your app:
{ "sub": "kp_7a4b2c...", "given_name": "Jane", "family_name": "Doe", "email": "jane@example.com", "picture": "https://gravatar.com/...", "nonce": "<your_random_nonce_value>", "iat": 1716800000, "exp": 1716886400}Alternatively, you can call the userinfo endpoint, passing the access_token as a Bearer token. This always returns an up-to-date version of the user’s profile. See When should I use the userinfo endpoint instead of decoding the id_token? for more details.
Complete the registration flow in your app and verify that you can sign in to a protected page (e.g., a dashboard).
The following link redirects the user to the Kinde sign-in page by default:
GET <YOUR_DOMAIN>/oauth2/auth?response_type=code&client_id=<your_kinde_client_id>&redirect_uri=<your_app_redirect_url>&scope=openid%20profile%20email&state=<your_random_state_value_min_8_characters>&nonce=<your_random_nonce_value>To send the user to the sign-up page instead, add prompt=create to the URL:
GET <YOUR_DOMAIN>/oauth2/auth?response_type=code&client_id=<your_kinde_client_id>&redirect_uri=<your_app_redirect_url>&scope=openid%20profile%20email&state=<your_random_state_value_min_8_characters>&nonce=<your_random_nonce_value>&prompt=createIf you offer mobile and desktop apps alongside a browser-based version, you’ll need to handle the post-authentication browser state. Rather than leaving a hanging screen, you can show users a success page. To do this, add the has_success_page parameter to the authorization URL.
Route protection works at two layers. Both are required — the frontend layer is UX, the backend layer is the real security gate.
Layer 1: Frontend — redirect unauthenticated users
Before rendering a protected page or screen, check that you have a valid, non-expired access token. If not, redirect to the Kinde authorization endpoint:
function isTokenExpired(token) { // JWT payloads are base64url-encoded; convert to standard base64 before decoding const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const { exp } = JSON.parse(atob(base64)); return Date.now() >= exp * 1000;}
// Run this check on every protected page loadif (!accessToken || isTokenExpired(accessToken)) { window.location.href = '<YOUR_DOMAIN>/oauth2/auth?response_type=code&client_id=<CLIENT_ID>&...';}Never rely on the frontend check alone. A user can bypass client-side redirects and call your API directly. The backend must always verify the token independently.
Layer 2: Backend — verify the token on every request
Your server must validate the access token on every call to a protected endpoint. Send the token in the Authorization header:
GET /api/protected-resourceAuthorization: Bearer <access_token>On the server, verify the token before returning any data:
// Pseudocode — pattern is the same in Node, Python, Go, etc.function protectedRoute(req, res, next) { const token = req.headers.authorization?.replace('Bearer ', ''); if (!token) return res.status(401).json({ error: 'Unauthorized' });
try { const payload = verifyJWT(token, { jwksUri: '<YOUR_DOMAIN>/.well-known/jwks', issuer: '<YOUR_DOMAIN>', }); req.user = payload; // access sub, org_code, scp, permissions next(); } catch { return res.status(401).json({ error: 'Invalid or expired token' }); }}See Validate the access token below for the full list of claims to check.
Using claims for authorization
Once the token is verified, use the claims inside it to make access decisions:
| Claim | Use for |
|---|---|
sub | Identify the user — link to your own database records |
scp | Check the user has the required OAuth scopes |
permissions | Fine-grained access control (e.g. read:reports) |
org_code | Multi-tenant apps — confirm the user belongs to the right organization |
Handling token expiry
By default, access tokens expire after 24 hours. If you requested the offline scope, use the refresh token to get a new access token silently — without sending the user through the login flow again:
POST <YOUR_DOMAIN>/oauth2/tokenContent-Type: application/x-www-form-urlencoded
grant_type=refresh_token&client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>&refresh_token=<REFRESH_TOKEN>POST <YOUR_DOMAIN>/oauth2/tokenContent-Type: application/x-www-form-urlencoded
grant_type=refresh_token&client_id=<CLIENT_ID>&refresh_token=<REFRESH_TOKEN>If no refresh token is available (i.e. offline scope was not requested), redirect the user to sign in again.
Always verify the access_token signature on your server before trusting any request. Kinde signs tokens with RS256 and publishes its public keys at the JWKS endpoint:
<YOUR_DOMAIN>/.well-known/jwksMost JWT libraries accept a JWKS URL directly — point your library at this URL and it will fetch and cache the correct public key automatically.
As part of verification, validate the following claims:
| Claim | Expected value |
|---|---|
iss | <YOUR_DOMAIN> |
aud | Your API audience if configured |
exp | Must be in the future |
iat | Must be in the past |
Reject any token that fails signature verification or has an unexpected claim value.
Call the Kinde Account API with the logged-in user’s access token to generate a self-serve portal link for the user:
const response = await fetch("<YOUR_DOMAIN>/account_api/v1/portal_link", { headers: { Authorization: `Bearer ${userAccessToken}` }});const data = await response.json();window.location = data.url;Optional parameters:
subnav – specify the portal section to open (e.g., organization_details, profile).return_url – where to redirect the user after exiting the portal.See: Self-serve portal for users.
To sign a user out:
Clear any session or token storage in your app
Redirect them to the Kinde logout endpoint with your logout URL:
<YOUR_DOMAIN>/logout?redirect=<your_logout_url>This ends their Kinde session. They will need to authenticate again to receive new tokens.
To register a logout URL, go to Settings > Applications > [Your app] > View details, then add it to the Allowed logout redirect URLs field.
The Authorization Code Flow is the standard way to sign users in when your app runs on a server you control. Instead of returning tokens directly in the browser, Kinde redirects the user back to your app with a short-lived authorization code. Your server then exchanges that code for tokens by making a secure back-channel request to Kinde’s token endpoint, including your client_secret. This two-step design keeps tokens off the URL and out of the browser — the authorization code is useless on its own without your secret. Use this flow for server-rendered web applications, traditional back-end web apps, and any client that can store a client_secret securely on the server.
The Authorization Code Flow with PKCE (Proof Key for Code Exchange) works the same way, but replaces the client_secret with a one-time cryptographic proof. Before redirecting the user to Kinde, your app generates a random code_verifier and sends a hashed version (code_challenge) in the authorization request. When exchanging the authorization code for tokens, your app sends the original code_verifier instead of a secret. Kinde verifies they match, proving that the same app that started the login is completing it. This matters because single-page apps (SPAs) and mobile apps are “public clients” — their code runs on the user’s device, so a client_secret could be extracted and abused. PKCE gives you the security of the authorization code flow without needing a secret. Use PKCE for SPAs, native mobile apps, desktop apps, and any client that cannot keep a client_secret confidential.
The Implicit Flow was originally created for browser-based apps that couldn’t keep a secret — it returned the access_token directly in the URL fragment after login. That convenience came at a serious cost: tokens in URL fragments can leak through browser history, referrer headers, and server logs. The OAuth 2.0 Security Best Current Practice (RFC 9700) now explicitly recommends against using the Implicit Flow for any new application.
What to use instead: The Authorization Code Flow with PKCE is the correct choice for all public clients — including single-page apps and mobile apps — regardless of whether they can store a client secret. PKCE (Proof Key for Code Exchange) solves the same problem the Implicit Flow was trying to address, without the token-in-URL security exposure.
If you are migrating an existing app from the Implicit Flow, replace the flow type token with code and add the code_challenge and code_challenge_method parameters to your authorization request. No client secret is required.
The following scopes can be requested from Kinde.
openidRequests an id_token that contains information about the authenticated user.
emailRequests the email and email_verified claims in the id_token.
profileRequests profile details as part of the id_token, including given name, family name, and picture.
offlineRequests a refresh_token that can be used to silently refresh the access_token when it expires.
Kinde uses offline to request a refresh token. The OIDC specification defines this scope as offline_access, but Kinde does not support offline_access — using it will not return a refresh token and will not produce an error. Make sure to use offline exactly.
phoneRequests the phone_number and phone_number_verified claims in the id_token.
addressRequests the address claim in the id_token.
There are a few useful additional parameters that Kinde supports in the authorization URL.
response_typeShould always be code. Kinde does not support the Implicit Flow — see Does Kinde support the Implicit Flow? for details.
Type: string
Required: Yes
client_idThe unique ID of your application in Kinde.
Type: string
Required: Yes
redirect_uriThe URL that the user will be returned to after authentication.
Type: string
Required: Yes
scopeThe scopes to be requested from Kinde. Scopes include openid, profile, email, offline.
Type: string
Required: No
Recommended: openid profile email
Kinde uses offline to request a refresh token in place of the OIDC specification’s offline_access scope.
stateKinde will return this to your app so you can validate it came from us and prevent CSRF attacks.
Type: string
Required: Yes (min 8 characters)
You can also use the state parameter to store additional information about the user or the authentication flow.
nonceA single-use value included in the signed id_token to prevent replay attacks.
Type: string
Required: No (but recommended)
code_challengeA base64url-encoded SHA-256 hash of the code verifier.
Type: string
Required: for public clients (e.g., SPAs and mobile apps)
code_challenge_methodShould always be S256. This tells Kinde that the code challenge was generated using SHA-256.
Type: string
Required: For PKCE
promptAccepts login, create, consent, select_account, or none to control which page users land on. By default, users are directed to the sign-in page.
If you use none, Kinde will not show authentication screens to the user.
If the user has an active SSO session, this will redirect them to the callback URL where the token exchange can occur.
If the user does NOT have an active SSO session, this will redirect them to the callback URL with the following error:
302 <YOUR_CALLBACK_URL>?error=login_required&error_description=User+authentication+is+required&state=YOUR_STATEType: string
Required: No
login_hintWhen your project knows which user it is trying to authenticate, it can provide their email in this parameter as a hint to Kinde. Passing this hint pre-fills the email box on the sign-up and sign-in screens.
Type: string
Required: No
org_codeFor multi-tenant or platform apps, tell Kinde which organization a user is trying to sign in or sign up to.
Type: string
Required: No
is_create_orgSet to true to create a new organization for the user during sign-up.
Type: boolean
Required: No
org_nameIf is_create_org is true, you can optionally include the name of the organization to create.
Type: string
Required: No
audienceThe audience claim for the JWT. This can be used to protect your APIs and resource servers.
Type: string
Required: No
has_success_pageShows a success page at the end of the authentication flow. Useful when the callback URL opens a native app rather than a web page.
Type: boolean
Required: No
Sets the UI language for the authentication screens.
Type: string
Required: No
connection_idThe connection ID for the authentication method, used when implementing custom sign-up and sign-in pages.
Type: string
Required: When using custom sign-up and sign-in pages
supports_reauthWhen set to true, the user’s state is encrypted and returned to the application when they access an expired link.
Type: boolean
Required: No
reauth_stateWhen supports_reauth is true and authentication fails, the user’s state is returned alongside the error. Pass this value back to the authorization flow to resume where the user left off.
Type: string
Required: No
deploy_idThe ID of a workflow deployment to test. When set, a password will be requested on login.
Type: string
Required: No
plan_interestIndicates the plan the user has expressed interest in.
Type: string
Required: No
pricing_table_keyDefines which pricing table to display in the billing flow.
Type: string
Required: No
start_page (deprecated)Accepts login or registration. Use prompt instead.
Type: string
Required: No
id_token?Decoding the id_token is usually the fastest approach — it requires no extra network request. However, there are three situations where calling the userinfo endpoint is the right choice:
id_token is a snapshot frozen at login time. If the user updates their name, email, or avatar in Kinde after they’ve authenticated, the id_token stays stale until they get a new one. The userinfo endpoint queries Kinde live and always returns current data.access_token. The id_token is typically held by the frontend. If your server needs to look up the authenticated user’s profile, it can call the userinfo endpoint using the access_token from the request header — without needing the frontend to forward the id_token separately.openid scope. Without openid, no id_token is issued. The userinfo endpoint still works as long as you hold a valid access_token.In all other cases, prefer decoding the id_token — it’s faster and requires no round trip to Kinde.
To call the userinfo endpoint, pass the access_token as a Bearer token:
GET https://<YOUR_DOMAIN>/oauth2/v2/user_profileAuthorization: Bearer <access_token>The response returns the user’s current profile fields:
{ "sub": "kp_7a4b2c...", "given_name": "Jane", "family_name": "Doe", "email": "jane@example.com", "picture": "https://gravatar.com/..."}The fields returned depend on the scopes that were granted. email and email_verified require the email scope. given_name, family_name, and picture require the profile scope.