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.

Step-up authentication

Link to this section

Kinde implements RFC 9470 step-up authentication. Your app or API can require a stronger authentication context for sensitive operations, and Kinde will collect only what is missing.

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>
&acr_values=mfa
&max_age=300

How a step-up works

Link to this section
  1. Your app sends the user to /oauth2/auth with acr_values, and optionally max_age.
  2. If the user’s session already satisfies the request, Kinde issues tokens straight away. To satisfy the request, the session must meet acr_values, and its last authentication must be more recent than max_age.
  3. If it does not, Kinde prompts for the additional factor only, then returns to your callback:
    • For mfa, the user verifies an MFA factor that they have already enrolled, for example an authenticator app or a one-time code. If the user has an enrolled factor, Kinde can also send a one-time code to the email address on their profile. Kinde does not ask the user to enrol a new factor to meet the request.
    • For phr, the user verifies with a passkey.
    • For max_age with acr_values, the user verifies only the factor that acr_values requires, as above. Kinde does not ask for the first factor again.
    • For max_age without acr_values, the user signs in again with their first factor. If your MFA policy requires a second factor, Kinde also asks for it.

Because the user keeps their session throughout, a step-up does not sign them out of your other applications.

After a step-up, use the new tokens from that callback. If you refresh a token that was issued before the step-up, the refreshed token keeps the acr, amr and auth_time of the earlier authentication.

Authentication claims

Link to this section

Tokens can carry three claims. Kinde adds them to tokens whether or not you request a context, so you can read them on each sign-in:

ClaimMeaning
acrThe context that was met: mfa or phr. Omitted when the user authenticated with a single factor.
amrThe methods that were used, as an array. Omitted when Kinde has no record of the methods, for example for a session that started before Kinde recorded them.
auth_timeWhen the user last actively authenticated, as seconds since the Unix epoch. Always present on the ID token; present on the access token when Kinde can establish it. Read it from the ID token. If step-up is not available (see Discovery), the ID token still has auth_time, but its value can be the time a token was issued rather than the time the user authenticated.

acr is deliberately absent for a single-factor login. A single factor is the baseline every authenticated session meets. Read amr if you need to know what was used. An absent acr means “neither mfa nor phr”:

  • For an operation that does not require mfa or phr, an absent acr is not an error. Accept the token.
  • For an operation that requires mfa or phr, an absent acr does not meet the requirement. Reject the request, or start a step-up. Do the same when acr is present but lower than the requirement. phr meets an mfa requirement.

Do this check on a token that you have verified. See Protecting an API with step-up.

amr values you may see:

ValueMethod
pwdPassword
passkeyPasskey, with the user verified by the device
popPasskey, where the device confirmed presence but did not verify the user. On its own, this does not meet mfa or phr.
email, smsA one-time code used as the first factor (passwordless sign-in)
otp:email, otp:smsA one-time code used as a second factor
totpAuthenticator app
step_up:email_otpA one-time code to the address on the user’s profile
mfaRecovery code
Provider name, for example googleA social or enterprise connection

A decoded ID token from the acr_values=mfa request above. amr lists each method that was used. In this case the user signed in with a password, then verified with an authenticator app:

{
"acr": "mfa",
"amr": ["pwd", "totp"],
"auth_time": 1716800000
}

When the context cannot be met

Link to this section

If the user cannot satisfy the request, Kinde redirects to your callback with an error rather than issuing a token that does not meet it. For example:

  • A phr request when passkeys are not enabled for the environment or organization, or the user has no passkey.
  • An mfa step-up from a user who has no enrolled MFA factor. A step-up only verifies a factor. It does not enrol one.
  • An mfa request on a new sign-in from a user who has no enrolled MFA factor, when your MFA policy does not ask the user to enrol one. If your policy asks the user to enrol, the user sees the usual enrolment screens and the request can be met. If the policy lets the user skip enrolment and the user skips, Kinde returns the error. The skip applies to this request only. Kinde does not record it as an opt-out.
  • An mfa request where the only factor left would prove the same channel again. For example, a one-time code to the email address or phone number that the user used for a passwordless sign-in does not count as a second factor.

The error is:

302 https://YOUR_CALLBACK_URL?
error=unmet_authentication_requirements&
error_description=The+requested+authentication+context+could+not+be+met&
state=YOUR_STATE

Handle this as a definite answer, not a transient failure. Retrying the same request will produce the same error.

This error is for a requirement that the user cannot meet. If a step-up that has started fails for a different reason, Kinde shows its own error page. It does not redirect to your callback, so your app does not receive an OAuth error in that case.

With prompt=none, Kinde cannot show a screen to collect the extra factor, so it does not start a step-up:

  • If the user has a session that does not meet acr_values, Kinde returns unmet_authentication_requirements.
  • If the user’s session is older than max_age, or the user has no session, Kinde returns login_required. Kinde keeps the session.

In each case, send the user through an interactive request (without prompt=none) to complete the step-up.

Protecting an API with step-up

Link to this section

An API that requires a stronger context for an operation must check the acr claim of the verified access token on each request. It must not trust the client to request the context.

If acr is absent, or is lower than the operation requires, return the RFC 9470 challenge:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="insufficient_user_authentication",
error_description="A different authentication level is required",
acr_values="mfa"

To require recent authentication, send max_age in the challenge and compare it with the auth_time claim. When your app gets this challenge, it sends the user to /oauth2/auth with the acr_values (and max_age) from the challenge, then calls the API again with the new access token.

The contexts Kinde can return are advertised at https://<your_kinde_subdomain>.kinde.com/.well-known/openid-configuration:

"acr_values_supported": ["mfa", "phr"],
"claims_supported": ["aud", "exp", "iat", "iss", "sub", "acr", "amr", "auth_time"]

Check for acr_values_supported before you rely on step-up. If the document does not list it, Kinde ignores acr_values and max_age, and does not add the acr and amr claims. The ID token still has auth_time, but its value can be the time a token was issued rather than the time the user authenticated. Do not use it to check recency in this case. To require a fresh sign-in, send prompt=login.

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

Request a minimum authentication context. Kinde recognises two values:

  • mfa - the user must complete a second factor. A step-up only verifies a factor that the user has already enrolled.
  • phr - the user must complete a phishing-resistant factor: a passkey, with the user verified by the device (PIN or biometrics)

If the user’s session already meets the requested context, Kinde issues tokens without prompting. If it does not, Kinde steps the user up - it asks only for the additional factor rather than making them sign in again from scratch.

Accepts a space-separated list; the highest context requested is applied. Values are case-insensitive. Unrecognised values are ignored.

Kinde also accepts some values that other providers use, so a client that you move to Kinde can keep its request: http://schemas.openid.net/pape/policies/2007/06/multi-factor and urn:okta:loa:2fa:any are read as mfa. Kinde does not return these values. When a token has an acr claim, its value is always mfa or phr. urn:okta:loa:2fa:any:ifpossible is not accepted, because Kinde cannot make an optional requirement. Kinde ignores this value.

If the requested context cannot be met - for example phr for a user with no usable passkey - Kinde returns an error to your callback rather than issuing a token that does not satisfy the request. See Step-up authentication.

Type: string

Required: No

The maximum age, in seconds, of the user’s last authentication. If the user authenticated longer ago than this, Kinde asks them to authenticate again before issuing tokens.

If the user has a session, Kinde keeps the session. The user stays signed in to your other applications. What Kinde asks for depends on acr_values:

  • max_age without acr_values: the user signs in again with their first factor (for example, their password). max_age does not add a second factor. If your MFA policy requires a second factor, Kinde asks for it, as it does on every sign-in.
  • max_age with acr_values=mfa or acr_values=phr: the user verifies only the factor that acr_values requires. Kinde does not ask for the first factor again. Kinde asks for this verification whenever the session is older than max_age, even if it already meets acr_values. The verification sets a new auth_time.

Use max_age=0 to require re-authentication on every request.

Kinde ignores an empty value and the values undefined, null and NaN. Some SDKs send these values when no max_age is set. Kinde treats a different value that is not a non-negative whole number as requiring re-authentication.

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/..."
}