Skip to main content
For most integrations, Client Credentials is the right choice. It’s designed for server-to-server communication where your backend talks to the Corti API directly. The SDK handles the full OAuth 2.0 token exchange, automatic refresh, and you never need to manage tokens yourself. Your backend should own all data-access logic and never expose the service-account token or client secret to the browser.
WebSocket connections in the frontend? If you need to open a Stream or Transcribe WebSocket from the browser, you can issue a scoped token by passing scopes: ["streams"] or scopes: ["transcribe"] when requesting a token. A scoped token can only access the specified endpoint — even if it’s intercepted, it cannot be used to read or modify any other data. See Scoped Tokens below for details.
New to Corti authentication? Read the general Authentication Overview first to understand environments, tenants, and how to create API clients.
If your use case requires end-user login — for example, when embedding the Corti Assistant in a user-facing app — the SDK also supports interactive OAuth flows. Pick the one that matches your client type:
MethodEnvironmentWhen to use
Client CredentialsBackend onlyRecommended. Server-to-server integrations
Authorization CodeFrontend & BackendInteractive user login (confidential clients)
PKCEFrontend & BackendInteractive user login (public clients, SPAs)
ROPCBackend onlyUsername/password for trusted or internal apps
Bearer TokenFrontend & BackendYou already have a token from another source
For complete examples of each flow, see the authentication examples in the examples repository.

Client Credentials

Backend only. Never expose your clientSecret in frontend code. Client Credentials tokens are service-account tokens with access to all data within the API Client. For frontend apps, use Bearer Token, PKCE, or a proxy.
This is the recommended method for server-to-server applications. The SDK handles the full OAuth 2.0 token exchange and automatic refresh. Create a CortiClient with your credentials — token acquisition and refresh are automatic:
JavaScript
import { CortiClient } from "@corti/sdk";

const client = new CortiClient({
    environment: "YOUR_ENVIRONMENT_ID",
    tenantName: "YOUR_TENANT_NAME",
    auth: {
        clientId: "YOUR_CLIENT_ID",
        clientSecret: "YOUR_CLIENT_SECRET",
    },
});
To get a token directly without creating a client, use CortiAuth:
JavaScript
import { CortiAuth } from "@corti/sdk";

const auth = new CortiAuth({
  environment: "YOUR_ENVIRONMENT_ID",
  tenantName: "YOUR_TENANT_NAME",
});

const token = await auth.getToken({
  clientId: "YOUR_CLIENT_ID",
  clientSecret: "YOUR_CLIENT_SECRET",
});

console.log("accessToken:", token.accessToken);
For more details on the Client Credentials flow, see the authentication overview.

Authorization Code Flow

The standard OAuth2 authorization code flow for interactive user authentication. Use this when your application has a server component that can keep the clientSecret confidential.
1

Redirect to login

Create a CortiAuth instance and send the user to Corti’s authorization page:
import { CortiAuth } from "@corti/sdk";

const auth = new CortiAuth({
    environment: "YOUR_ENVIRONMENT_ID",
    tenantName: "YOUR_TENANT_NAME",
});

// Redirects the user automatically
await auth.authorizeURL({
    clientId: "YOUR_CLIENT_ID",
    redirectUri: "https://your-app.com/callback",
});

// Or get the URL without redirecting
const url = await auth.authorizeURL(
    {
        clientId: "YOUR_CLIENT_ID",
        redirectUri: "https://your-app.com/callback",
    },
    { skipRedirect: true },
);
2

User authenticates

The user logs in and grants your application access.
3

Receive authorization code

The user is redirected back to your redirectUri with a code query parameter:
JavaScript
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
4

Exchange code for tokens

Pass code to CortiClient — it handles the exchange and refresh automatically:
JavaScript
import { CortiClient } from "@corti/sdk";

const client = new CortiClient({
    environment: "YOUR_ENVIRONMENT_ID",
    tenantName: "YOUR_TENANT_NAME",
    auth: {
        clientId: "YOUR_CLIENT_ID",
        clientSecret: "YOUR_CLIENT_SECRET",
        code,
        redirectUri: "https://your-app.com/callback",
    },
});
Or exchange manually using the auth instance from step 1:
JavaScript
const tokenResponse = await auth.getCodeFlowToken({
    clientId: "YOUR_CLIENT_ID",
    clientSecret: "YOUR_CLIENT_SECRET",
    redirectUri: "https://your-app.com/callback",
    code,
});

console.log(tokenResponse.accessToken);
5

Use tokens

The client from the CortiClient path is ready to use. For the CortiAuth path, create a CortiClient with the returned accessToken using the Bearer Token method.

Authorization Code Flow with PKCE

Proof Key for Code Exchange (PKCE) is the recommended flow for public clients — single-page apps, native apps, and any environment where a client secret cannot be safely stored.
You can run PKCE (and refreshToken) directly from the frontend, but only from an origin that is allowed by your OAuth client configuration. In practice, this means the page origin must match one of the origins implied by your configured redirectUri values.
1

Generate the authorization URL

The SDK handles code verifier generation and localStorage storage automatically:
import { CortiAuth } from "@corti/sdk";

const auth = new CortiAuth({
    environment: "YOUR_ENVIRONMENT_ID",
    tenantName: "YOUR_TENANT_NAME",
});

// Redirects automatically and stores the code verifier
await auth.authorizePkceUrl({
    clientId: "YOUR_CLIENT_ID",
    redirectUri: "https://your-app.com/callback",
});

// Or get the URL without redirecting
const url = await auth.authorizePkceUrl(
    {
        clientId: "YOUR_CLIENT_ID",
        redirectUri: "https://your-app.com/callback",
    },
    { skipRedirect: true },
);
If you need manual control over the verifier:
JavaScript
const codeVerifier = "your-high-entropy-verifier";
const codeChallenge = "base64url-sha256-of-verifier";

const url = await auth.authorizeURL(
    {
        clientId: "YOUR_CLIENT_ID",
        redirectUri: "https://your-app.com/callback",
        codeChallenge,
    },
    { skipRedirect: true },
);
2

User authenticates

The user logs in and grants access.
3

Exchange code with verifier

After the user is redirected back, retrieve the code and verifier:
JavaScript
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
const codeVerifier = CortiAuth.getCodeVerifier(); // retrieves from localStorage
Pass them to CortiClient:
JavaScript
import { CortiClient } from "@corti/sdk";

const client = new CortiClient({
    environment: "YOUR_ENVIRONMENT_ID",
    tenantName: "YOUR_TENANT_NAME",
    auth: {
        clientId: "YOUR_CLIENT_ID",
        code,
        redirectUri: "https://your-app.com/callback",
        codeVerifier,
    },
});
Or exchange using the auth instance from step 1:
JavaScript
const tokenResponse = await auth.getPkceFlowToken({
    clientId: "YOUR_CLIENT_ID",
    redirectUri: "https://your-app.com/callback",
    code,
    codeVerifier,
});

console.log(tokenResponse.accessToken);
4

Use tokens

The client from the CortiClient path is ready to use. For the CortiAuth path, create a CortiClient with the returned accessToken using the Bearer Token method.

Resource Owner Password Credentials

The ROPC flow lets users authenticate with a username and password using the SDK.
import { CortiClient } from "@corti/sdk";

const client = new CortiClient({
    environment: "YOUR_ENVIRONMENT_ID",
    tenantName: "YOUR_TENANT_NAME",
    auth: {
        clientId: "YOUR_CLIENT_ID",
        username: "user@example.com",
        password: "your-password",
    },
});
For production web applications, prefer PKCE over ROPC.

Bearer Token

If you already have an access token — for example, from another OAuth flow, a token endpoint on your backend, or a third-party identity provider — you can pass it directly to CortiClient without using any of the built-in authentication flows.

Using a token

JavaScript
import { CortiClient } from "@corti/sdk";

const client = new CortiClient({
  auth: {
    accessToken: "<ACCESS_TOKEN_FROM_YOUR_BACKEND>",
  },
});

With automatic refresh

If you pass clientId and refreshToken, the SDK will call refreshToken automatically when the access token expires — no custom callback needed:
JavaScript
const client = new CortiClient({
    auth: {
        accessToken: "YOUR_ACCESS_TOKEN",
        refreshToken: "YOUR_REFRESH_TOKEN",
        clientId: "YOUR_CLIENT_ID",
    },
});
For custom refresh logic (e.g. calling your own backend), provide a refreshAccessToken callback instead:
const client = new CortiClient({
    auth: {
        accessToken: "YOUR_ACCESS_TOKEN",
        refreshToken: "YOUR_REFRESH_TOKEN",
        refreshAccessToken: async (refreshToken?: string) => {
            const response = await fetch("https://your-backend/refresh", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ refreshToken }),
            });
            return response.json();
        },
    },
});
You can also start without an initial access token. The SDK will call your refresh function immediately to obtain one:
JavaScript
const client = new CortiClient({
    auth: {
        refreshAccessToken: async (refreshToken?: string) => {
            const response = await fetch("https://your-backend/token", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ refreshToken }),
            });
            return response.json();
        },
    },
});

Scoped tokens

In some cases it’s acceptable to open a WebSocket connection directly from the frontend — for example, when streaming audio for real-time transcription. To do this securely, your backend should issue a scoped token instead of a full service-account token. A scoped token restricts access to a single endpoint. Even if the token is intercepted, it can only be used to send audio to the scoped WebSocket — it cannot read, modify, or delete any other data.

Available scopes

ScopeGrants access to
"streams"Stream WebSocket endpoint (/streams) only
"transcribe"Transcribe WebSocket endpoint (/transcribe) only

Requesting scoped tokens

Issue scoped tokens from your backend using CortiAuth or client.auth:
import { CortiAuth } from "@corti/sdk";

const auth = new CortiAuth({
    environment: "YOUR_ENVIRONMENT_ID",
    tenantName: "YOUR_TENANT_NAME",
});

// Token that can only access the transcribe endpoint
const transcribeToken = await auth.getToken({
    clientId: "YOUR_CLIENT_ID",
    clientSecret: "YOUR_CLIENT_SECRET",
    scopes: ["transcribe"],
});

// Token with both WebSocket scopes
const wsToken = await auth.getToken({
    clientId: "YOUR_CLIENT_ID",
    clientSecret: "YOUR_CLIENT_SECRET",
    scopes: ["streams", "transcribe"],
});

Using scoped tokens in the frontend

Pass the scoped token to the frontend and use it as a bearer token:
JavaScript
const client = new CortiClient({
    environment: "YOUR_ENVIRONMENT_ID",
    tenantName: "YOUR_TENANT_NAME",
    auth: {
        accessToken: transcribeToken.accessToken,
    },
});

// Works -- transcribe is within scope
const socket = await client.transcribe.connect({ configuration: { primaryLanguage: "en" } });

// Rejected by the API -- REST endpoints are outside scope
// await client.interactions.list(); // Error
For even tighter security, consider routing WebSocket traffic through a proxy instead of exposing tokens to the frontend at all.

How token refresh works

When using Client Credentials, Authorization Code, PKCE, or ROPC, the SDK manages the full token lifecycle automatically — you don’t need to handle expiry or renewal. With Bearer Token, refresh depends on what you provide: a clientId + refreshToken pair enables automatic refresh, a refreshAccessToken callback gives you full control, and a static token is used as-is until it expires.

Proactive refresh

Tokens are refreshed before each API call, not in response to a 401 error. Every time you call an SDK method, the client checks whether the current access token is still valid. If it has expired (or is about to), the SDK refreshes it transparently before sending the request. The SDK subtracts a 2-minute buffer from the token’s expiresIn value to ensure refresh happens before the token actually expires on the server.
Token lifetime:  |------ expiresIn (e.g. 300s) ------|
Effective:       |---- usable (300s - 120s) ----|refresh|

Refresh behaviour per flow

The SDK acquires a token on the first API call using your clientId and clientSecret. When the token approaches expiry:
  1. If the server returned a refresh token and it hasn’t expired, the SDK uses it to get a new access token.
  2. Otherwise, the SDK performs a fresh client credentials exchange.
This is fully automatic — no configuration needed beyond clientId and clientSecret.
A static accessToken without refreshToken or refreshAccessToken is used as-is. The SDK does not attempt to refresh it. When the token expires, API calls will fail with an authentication error.If you provide expiresIn, the SDK tracks the expiry locally. Without it, the SDK attempts to extract expiry from the JWT.
When you provide clientId and refreshToken alongside the accessToken, the SDK calls refreshToken automatically when the access token approaches expiry. No custom callback is needed.
When you provide a refreshAccessToken callback, the SDK calls it automatically when the access token is expired or missing. Your callback receives the current refreshToken (if any) and must return a new token response:
JavaScript
{
    accessToken: string;
    expiresIn?: number;
    refreshToken?: string;      // updated refresh token (if rotated)
    refreshExpiresIn?: number;
    tokenType?: string;
}
If you omit the initial accessToken entirely, the SDK calls refreshAccessToken immediately on the first API request to obtain one.
These flows all follow the same refresh pattern:
  1. The initial token is acquired via the respective grant (code exchange, PKCE exchange, or password grant).
  2. When the access token approaches expiry, the SDK uses the refresh token to obtain a new one.
  3. If the refresh token has also expired, the SDK falls back to the original grant (re-exchanging credentials or code). For Authorization Code and PKCE, this requires a new authorization code from a fresh user login — the SDK cannot redo the interactive flow automatically.
In practice, the refresh token typically has a much longer lifetime than the access token, so step 3 is rare.

What the SDK does NOT do

  • No 401-based retry. If a token is rejected by the server while the SDK considers it valid (e.g. clock skew or server-side revocation), the request fails immediately. HTTP retries only apply to 408, 429, and 5xx status codes.
  • No persistent storage. Tokens live only in memory for the lifetime of the client instance.
  • No configurable buffer. The 2-minute refresh buffer is fixed and cannot be overridden.

Resources


For support or questions, reach out through help.corti.app