Skip to content
  • SDKs and APIs
  • Overview

Use Kinde without an SDK

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.

  • A Kinde account with Admin or Engineer access (Sign up for free)
  • Knowledge of programming languages and OAuth 2.0

1. Create a Kinde application

Link to this section
  1. Sign in to your Kinde dashboard and select Add application

  2. Enter a name for the application (e.g., “My App”)

  3. Select an application type:

    • Back-end web: for server-rendered or full-stack apps
    • Front-end and mobile: for JavaScript-based single-page applications (SPAs) or mobile apps
  4. Select Other

  5. Select Save.

2. Get your app keys

Link to this section
  1. In your app settings page, select Details

  2. Copy your app keys:

    • Custom domain: if you have configured a custom domain
    • Domain: your Kinde domain (use it if you don’t have a custom domain)
    • Client ID: a unique identifier for your app
    • Client secret: (only for back-end apps)

    You will need these keys to connect your codebase with Kinde.

3. Add a callback URL

Link to this section
  1. In your app’s Details page, scroll down to the Callback URLs section

  2. Add the following URLs:

    • Allowed callback URLs (e.g., http://localhost:3000/auth/callback)
    • Allowed logout redirect URLs (e.g., http://localhost:3000)

    You can add more than one callback URL per line.

  3. Select Save

4. Set authentication methods

Link to this section
  1. In your app’s settings page, go to Authentication
  2. Enable the authentication methods you want your users to sign in with (e.g., Google, Apple, SSO, etc.)
  3. Select Save

5. Get the OpenID endpoints

Link to this section

These are the OpenID endpoints for Kinde, found at:

<YOUR_DOMAIN>/.well-known/openid-configuration

  • Authorization endpoint: <YOUR_DOMAIN>/oauth2/auth
  • Token endpoint: <YOUR_DOMAIN>/oauth2/token
  • Userinfo endpoint: <YOUR_DOMAIN>/oauth2/v2/user_profile
  • Logout endpoint: <YOUR_DOMAIN>/logout

Other endpoints:

  • JWKS URI: <YOUR_DOMAIN>/.well-known/jwks
  • Revocation endpoint: <YOUR_DOMAIN>/oauth2/revoke
  • Introspection endpoint: <YOUR_DOMAIN>/oauth2/introspect

6. Create the authorization request

Link to this section
  1. Your user clicks a button to sign in or sign up in your app.
  1. In your app, generate a random state value and a nonce:

    Node.js example
    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.

  2. 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>
  1. The user is redirected to the Kinde sign in page and authenticates with their credentials (email/password, social login, SSO, etc.).

7. Handle the callback

Link to this section
  1. 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>
  2. Validate that the returned state matches the one you generated in the previous step to prevent CSRF attacks.

  1. Exchange the authorization code for tokens by making a POST request to the Kinde token endpoint:

    POST <YOUR_DOMAIN>/oauth2/token
    Content-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>
  1. 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
    }

8. Display user information

Link to this section
  1. Redirect the user to a protected page in your app

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

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

9. Test user registration

Link to this section

Complete the registration flow in your app and verify that you can sign in to a protected page (e.g., a dashboard).

  1. Go to your Kinde dashboard and select Users
  2. You should see the new user’s details in the list.

Authentication

Link to this section
Link to this section

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=create

Handle successful auth for desktop and mobile apps

Link to this section

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

Protect a route with auth

Link to this section

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 load
if (!accessToken || isTokenExpired(accessToken)) {
window.location.href =
'<YOUR_DOMAIN>/oauth2/auth?response_type=code&client_id=<CLIENT_ID>&...';
}

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-resource
Authorization: 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:

ClaimUse for
subIdentify the user — link to your own database records
scpCheck the user has the required OAuth scopes
permissionsFine-grained access control (e.g. read:reports)
org_codeMulti-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/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&client_id=<CLIENT_ID>
&client_secret=<CLIENT_SECRET>
&refresh_token=<REFRESH_TOKEN>

If no refresh token is available (i.e. offline scope was not requested), redirect the user to sign in again.

Validate the access token

Link to this section

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/jwks

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

ClaimExpected value
iss<YOUR_DOMAIN>
audYour API audience if configured
expMust be in the future
iatMust be in the past

Reject any token that fails signature verification or has an unexpected claim value.

Link to this section

Call the Kinde Account API with the logged-in user’s access token to generate a self-serve portal link for the user:

JavaScript example
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.

Sign out your users

Link to this section

To sign a user out:

  1. Clear any session or token storage in your app

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

Supported grant types in Kinde

Link to this section

Authorization Code Flow

Link to this section

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.

PKCE extension

Link to this section

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.

Link to this section

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.

OAuth 2.0 scopes for Kinde

Link to this section

The following scopes can be requested from Kinde.

Requests an id_token that contains information about the authenticated user.

Requests the email and email_verified claims in the id_token.

Requests profile details as part of the id_token, including given name, family name, and picture.

Requests a refresh_token that can be used to silently refresh the access_token when it expires.

Requests the phone_number and phone_number_verified claims in the id_token.

Requests the address claim in the id_token.

Request parameters

Link to this section

There are a few useful additional parameters that Kinde supports in the authorization URL.

Should always be code. Kinde does not support the Implicit Flow — see Does Kinde support the Implicit Flow? for details.

Type: string

Required: Yes

The unique ID of your application in Kinde.

Type: string

Required: Yes

The URL that the user will be returned to after authentication.

Type: string

Required: Yes

The scopes to be requested from Kinde. Scopes include openid, profile, email, offline.

Type: string

Required: No

Recommended: openid profile email

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

A single-use value included in the signed id_token to prevent replay attacks.

Type: string

Required: No (but recommended)

code_challenge

Link to this section

A base64url-encoded SHA-256 hash of the code verifier.

Type: string

Required: for public clients (e.g., SPAs and mobile apps)

code_challenge_method

Link to this section

Should always be S256. This tells Kinde that the code challenge was generated using SHA-256.

Type: string

Required: For PKCE

Accepts 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_STATE

Type: string

Required: No

When 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

For multi-tenant or platform apps, tell Kinde which organization a user is trying to sign in or sign up to.

Type: string

Required: No

Set to true to create a new organization for the user during sign-up.

Type: boolean

Required: No

If is_create_org is true, you can optionally include the name of the organization to create.

Type: string

Required: No

The audience claim for the JWT. This can be used to protect your APIs and resource servers.

Type: string

Required: No

has_success_page

Link to this section

Shows 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

The 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_reauth

Link to this section

When set to true, the user’s state is encrypted and returned to the application when they access an expired link.

Type: boolean

Required: No

When 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

The ID of a workflow deployment to test. When set, a password will be requested on login.

Type: string

Required: No

Indicates the plan the user has expressed interest in.

Type: string

Required: No

pricing_table_key

Link to this section

Defines which pricing table to display in the billing flow.

Type: string

Required: No

start_page (deprecated)

Link to this section

Accepts login or registration. Use prompt instead.

Type: string

Required: No

When should I use the userinfo endpoint instead of decoding the id_token?

Link to this section

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:

  • You need up-to-date profile data. The 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.
  • Your backend only has the 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.
  • You didn’t request the 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_profile
Authorization: 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/..."
}