Authentication
Authenticate the SDK with a personal access token or a service account.
Kortix accepts one bearer token per request. Pass it through getToken in
createKortix. The SDK sends it as Authorization: Bearer <token>.
import { createKortix } from '@kortix/sdk';
const kortix = createKortix({
backendUrl: 'https://api.kortix.com/v1',
getToken: async () => process.env.KORTIX_API_KEY!,
});backendUrl and getToken are required. The SDK caches nothing: it calls
getToken on every request, so your app owns token storage and refresh.
The audit log records the credential your getToken returns (a browser
session, a personal access token, a connected app), not a label your host
reports. clientSource is deprecated and does nothing.
Set clientVersion to your app's name and release version, for example
my-app/1.4.0. The SDK sends it as X-Kortix-Client-Version on every request.
Machine writes it to request logs only, to see which client versions still call
an API route before that route is retired. It never reaches the audit log.
Personal access tokens
A personal access token (PAT) is the credential for the SDK, the CLI, and CI.
Create one in your own settings, at Settings → API keys
(/settings/tokens). The key starts with kortix_pat_ and shows only once, at
creation. Store it as a secret.
A PAT acts as the user who created it and holds exactly that user's role assignments. It adds no access of its own. Its scope only narrows the reach:
| Scope | Reach |
|---|---|
| Account (default) | Every project in the account |
| Project | One project only; every other project returns 403 |
Choose the project scope for CI and other narrow-purpose credentials.
kortix login mints a PAT and stores it locally — it is the same credential
type, not a separate token kind.
Service accounts
A service account is a separate credential family for non-human callers,
prefixed kortix_sa_. Create one at Account → Tokens
(/accounts/<account-id>?tab=tokens), the account-level surface for
credentials that are not a person's.
A service account is its own principal (service_account), not a person's
credential. It has no membership, so it holds only the roles assigned to it
directly. An agent's identity is a service account, which is how you assign a
role to an agent. See
Accounts & access.
:::warning
A new service account has no assignments and therefore no project access. If
you point getToken at one before you assign it a role, every call returns
403 "You do not have access to this project". Assign it a project role
first, or use a personal access token for the SDK, the CLI, and demos
instead.
:::
OAuth access tokens (Sign in with Kortix)
A third-party app that signs users in through Kortix receives a kortix_oat_
token per user. With the kortix scope it acts as that user on the whole API,
exactly like a personal access token, but it expires after an hour and rotates
through a refresh token. createKortixAuth in @kortix/sdk/server owns the
whole lifecycle — see Sign in with Kortix.
Supabase JWT
If your app uses Kortix's own sign-in, return the live session token instead of a PAT:
getToken: async () =>
(await supabase.auth.getSession()).data.session?.access_token ?? null,The SDK calls getToken on every request, so a refreshed token takes effect
automatically.
Recover from a rejected token
When the API answers 401, the SDK replays the request once with a fresh
token. A getToken that caches must tell the SDK what to drop. Attach an
invalidate(rejectedToken) function to it. The SDK calls it before the replay,
and the replay then reads a new token.
const getToken = Object.assign(
async () => cachedSession.accessToken,
{ invalidate: (rejected: string) => cachedSession.refreshIfCurrent(rejected) },
);Rules:
- Parallel
401s on one token callinvalidateonce and share one refresh. - The SDK replays a request once. A second
401returns to the caller. - Without
invalidate, the replay re-readsgetToken(). A cached getter then returns the dead token, and the replay does not happen. invalidateTokenCache()callsinvalidatewith the last tokengetTokenreturned. The live event stream uses it after an auth error.isAuthFailure(error)istruefor a401, a403or anAuthError. It readsstatus, never the message text.
Headless sign-in (email, password, magic link, social)
Every ordinary sign-in flow is available through the Kortix API, so a CLI, a native app, a script, or your own backend signs users up and in without a Supabase URL or key — on kortix.com and on a self-host alike.
import { createKortix } from '@kortix/sdk';
const session = createKortix({ backendUrl, getToken: async () => null }).auth.session({
storage: { // optional: any get/set/remove
get: () => localStorage.getItem('kortix'),
set: (v) => localStorage.setItem('kortix', v),
remove: () => localStorage.removeItem('kortix'),
},
});
const kortix = createKortix({ backendUrl, getToken: session.getToken }); // refreshes itself
const { session: s, user } = await kortix.auth.signInWithPassword({ email, password });
await session.set(s, user);
await kortix.projects.list(); // as that user| Call | Route | Notes |
|---|---|---|
auth.signUp({ email, password, redirect_to? }) | POST /v1/auth/signup | requires_email_confirmation: true → no session until the emailed link/code is used. |
auth.signInWithPassword({ email, password }) | POST /v1/auth/sign-in/password | |
auth.sendMagicLink({ email, redirect_to? }) → auth.verifyOtp({ email, token, type: 'magiclink' }) | /sign-in/magic-link, /verify-otp | The email carries a link and a 6-digit code. |
auth.signInWithProvider({ provider, redirect_to }) → auth.exchangeCode({ code, code_verifier }) | /sign-in/oauth, /oauth/exchange | PKCE: keep code_verifier until the provider redirects back with ?code=. redirect_to must be on the instance's redirect allow-list. |
auth.refresh({ refresh_token }) | POST /v1/auth/refresh | createKortixSession calls it for you. |
auth.resetPassword({ email, redirect_to? }) → auth.verifyOtp({ type: 'recovery' }) → auth.updatePassword({ password }, token) | /password/reset, /verify-otp, /password/update | |
auth.user(token) / auth.signOut(token) | GET /v1/auth/user, POST /v1/auth/sign-out | Sign-out revokes at Supabase and in the Kortix session gate. |
Errors throw HeadlessAuthError with code, message and the upstream status
(invalid_credentials, over_request_rate_limit, …). Each route is limited to
30 attempts per minute per IP. Multi-factor enrolment/challenge is not on the
API yet — it stays on the Kortix web app.
Choose a credential
| You are building | Use |
|---|---|
| A backend, script, or CI job | A personal access token, account-wide or project-scoped |
| The Kortix CLI | kortix login (mints a personal access token) |
| A web app, CLI, or native app signing users in itself | kortix.auth.* + createKortixSession (headless sign-in, above) |
| An automated caller with its own project assignment | A service account |
| Your own app, signed in by its users with their Kortix account | Sign in with Kortix — an OAuth access token the SDK manages for you |
