Register and manage APIs
Manage your APIs
This guide shows how to implement a permissions model where your React frontend handles authentication and page-level access control, while your backend enforces permissions on API routes using Kinde’s built-in permissions.
You do not need the Management API for this flow. Permissions are included in the access token at sign-in.
When a user signs in through the Kinde React SDK, they receive a JWT access token. That token includes a permissions array based on the roles and permissions you configured in Kinde.
Both your frontend and backend read from the same token:
permissions array.The access token is a standard JWT. Any backend (Python, Node.js, .NET, Ruby, and others) can validate and decode it, then read the permissions claim.
Before writing code, set up permissions in the Kinde dashboard:
Create todos)create:todos)Example permissions:
| Key | Description |
|---|---|
create:todos | Can create new todo items |
read:todos | Can view todo items |
update:todos | Can edit existing todo items |
Group permissions into roles:
After creating permissions, go to Settings > User Management > Roles and create roles that bundle permissions together.
For example:
read:todos, read:reportsread:todos, create:todos, update:todosdelete:todos, manage:usersAssign roles to users in the Users section. Role-based permissions are then automatically included in each user’s access token at sign-in.
The React SDK gives you three common ways to enforce permissions in the frontend.
has()import { has } from "@kinde-oss/kinde-auth-react/utils";
export default function TodoHeader() { const canCreateTodos = has({ permissions: ["create:todos"] }); const canManageUsers = has({ roles: ["admin"], permissions: ["manage:users"], });
return ( <header> <h1>Todos</h1> {canCreateTodos && <button>Create todo</button>} {canManageUsers && <a href="/admin/users">Manage users</a>} </header> );}Import has from @kinde-oss/kinde-auth-react/utils, then call it with an object containing permissions (and optionally roles).
It returns true only when all requested conditions pass.
Examples:
has({ permissions: ["create:todos"] })has({ roles: ["admin"], permissions: ["manage:users"] })Use this for conditional UI rendering, like showing a New todo button only when create:todos exists.
ProtectedRoute (react-router-dom)import { Routes, Route } from "react-router-dom";import { ProtectedRoute } from "@kinde-oss/kinde-auth-react/react-router";
function AdminPage() { return <h2>Admin</h2>;}
function UnauthorizedPage() { return <h2>You are not authorized</h2>;}
export default function AppRoutes() { return ( <Routes> <Route path="/unauthorized" element={<UnauthorizedPage />} /> <Route path="/admin" element={ <ProtectedRoute has={{ permissions: ["manage:users"] }} fallbackPath="/unauthorized" > <AdminPage /> </ProtectedRoute> } /> </Routes> );}Import ProtectedRoute from @kinde-oss/kinde-auth-react/react-router and wrap route elements.
Required props:
has - access requirements (permissions, roles, featureFlags, or billingEntitlements)fallbackPath - where to redirect unauthorized usersUse this for route-level guards such as restricting /admin to users with manage:users.
useKindeAuth() hookimport { useEffect, useState } from "react";import { useKindeAuth } from "@kinde-oss/kinde-auth-react";
export default function PermissionsPanel() { const { getPermission, getPermissions } = useKindeAuth(); const [canCreateTodo, setCanCreateTodo] = useState(false); const [allPermissions, setAllPermissions] = useState<string[]>([]);
useEffect(() => { const loadPermissions = async () => { const createTodoPermission = await getPermission("create:todos"); const permissionSet = await getPermissions();
setCanCreateTodo(Boolean(createTodoPermission?.isGranted)); setAllPermissions(permissionSet?.permissions ?? []); };
loadPermissions(); }, [getPermission, getPermissions]);
return ( <section> {canCreateTodo && <button>Create todo</button>} <pre>{JSON.stringify(allPermissions, null, 2)}</pre> </section> );}For inline checks and full permission objects (including org context), use:
await getPermission("create:todos")await getPermissions()Typical return shapes:
{ "permissionKey": "create:todos", "orgCode": "org_1234", "isGranted": true}{ "orgCode": "org_1234", "permissions": ["create:todos", "update:todos", "read:todos"]}import { useKindeAuth } from "@kinde-oss/kinde-auth-react";
type TodoPayload = { title: string;};
export default function CreateTodoButton() { const { getAccessToken } = useKindeAuth();
const createTodo = async () => { const accessToken = await getAccessToken();
const payload: TodoPayload = { title: "Ship docs update" };
const response = await fetch("https://api.example.com/todos", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify(payload), });
if (!response.ok) { throw new Error("Request failed"); }
const data = await response.json(); console.log("Created todo:", data); };
return <button onClick={createTodo}>Create todo</button>;}Retrieve the token with getAccessToken() from useKindeAuth(), then send it as Authorization: Bearer <token> using fetch (or any HTTP client).
Use Kinde’s JWT validator and decoder packages to validate the token and read claims on the backend.
npm install @kinde/jwt-validator @kinde/jwt-decoderNext.js example:
import { NextResponse } from "next/server";import { validateToken } from "@kinde/jwt-validator";import { jwtDecoder } from "@kinde/jwt-decoder";
export async function POST(req: Request) { const authHeader = req.headers.get("authorization"); const token = authHeader?.replace("Bearer ", "");
if (!token) { return NextResponse.json({ error: "Missing bearer token" }, { status: 401 }); }
try { const validationResult = await validateToken({ token, domain: process.env.KINDE_ISSUER_URL, });
if (!validationResult.valid) { return NextResponse.json({ error: "Invalid token" }, { status: 401 }); }
// Decode claims after successful validation const decodedToken = jwtDecoder(token); const { sub, aud, permissions = [] } = decodedToken; const expectedAudience = process.env.KINDE_API_AUDIENCE; const audiences = Array.isArray(aud) ? aud : [aud].filter(Boolean);
if (!expectedAudience || !audiences.includes(expectedAudience)) { return NextResponse.json({ error: "Invalid audience" }, { status: 401 }); }
if (!permissions.includes("create:todos")) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); }
return NextResponse.json({ ok: true, claims: { sub, aud, permissions }, }); } catch (error) { return NextResponse.json({ error: "Token validation failed" }, { status: 401 }); }}The KINDE_ISSUER_URL environment variable is created when you set up your Kinde application. Setup details vary by framework.
permissions claim.If you use a Kinde backend SDK, helper methods can simplify permission checks. Otherwise, any JWT validation library works if you validate tokens against your Kinde issuer URL.
If you want an access token minted for your registered API, set the audience prop on KindeProvider to that API’s identifier.
Register your API in Kinde first, then use that identifier as the audience value.
import { StrictMode } from "react";import { createRoot } from "react-dom/client";import { KindeProvider } from "@kinde-oss/kinde-auth-react";import App from "./App";
createRoot(document.getElementById("root")!).render( <StrictMode> <KindeProvider clientId={"your_kinde_client_id"} domain={"your_kinde_domain"} logoutUri={"your_logout_uri"} redirectUri={"your_redirect_uri"} audience="api.example.com/v1" > <App /> </KindeProvider> </StrictMode>);