> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corti.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript SDK - Authentication

> All authentication methods for the Corti JavaScript SDK

For most integrations, **[Client Credentials](#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](#how-token-refresh-works), 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.

<Tip>**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](#scoped-tokens) below for details.</Tip>

<Info>New to Corti authentication? Read the general [Authentication Overview](/authentication/overview) first to understand environments, tenants, and how to create API clients.</Info>

If your use case requires **end-user login** -- for example, when embedding the [Corti Assistant](/assistant/authentication) in a user-facing app -- the SDK also supports interactive OAuth flows. Pick the one that matches your client type:

| Method                                         | Environment        | When to use                                    |
| :--------------------------------------------- | :----------------- | :--------------------------------------------- |
| [Client Credentials](#client-credentials)      | Backend only       | **Recommended.** Server-to-server integrations |
| [Authorization Code](#authorization-code-flow) | Frontend & Backend | Interactive user login (confidential clients)  |
| [PKCE](#authorization-code-flow-with-pkce)     | Frontend & Backend | Interactive user login (public clients, SPAs)  |
| [ROPC](#resource-owner-password-credentials)   | Backend only       | Username/password for trusted or internal apps |
| [Bearer Token](#bearer-token)                  | Frontend & Backend | You already have a token from another source   |

<Info>For complete examples of each flow, see the [authentication examples](https://github.com/corticph/corti-examples/tree/main/sdk/typescript/next-auth-examples) in the examples repository.</Info>

***

## Client Credentials

<Warning>**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](#bearer-token), [PKCE](#authorization-code-flow-with-pkce), or a [proxy](/sdk/js/proxy).</Warning>

This is the recommended method for server-to-server applications. The SDK handles the full OAuth 2.0 token exchange and [automatic refresh](#how-token-refresh-works).

Create a `CortiClient` with your credentials — token acquisition and refresh are automatic:

```ts title="JavaScript" theme={null}
import { CortiClient } from "@corti/sdk";

// Replace these with your values
const CLIENT_ID = "<your-client-id>";
const CLIENT_SECRET = "<your-client-secret>";
const ENVIRONMENT = "<eu-or-us>";
const TENANT = "<your-tenant-name>";

const client = new CortiClient({
    environment: ENVIRONMENT,
    tenantName: TENANT,
    auth: {
        clientId: CLIENT_ID,
        clientSecret: CLIENT_SECRET,
    },
});
```

To get a token directly without creating a client, use `CortiAuth`:

```ts title="JavaScript" theme={null}
import { CortiAuth } from "@corti/sdk";

// Replace these with your values
const CLIENT_ID = "<your-client-id>";
const CLIENT_SECRET = "<your-client-secret>";
const ENVIRONMENT = "<eu-or-us>";
const TENANT = "<your-tenant-name>";

const auth = new CortiAuth({
  environment: ENVIRONMENT,
  tenantName: TENANT,
});

const token = await auth.getToken({
  clientId: CLIENT_ID,
  clientSecret: CLIENT_SECRET,
});

console.log("accessToken:", token.accessToken);
```

For more details on the Client Credentials flow, see the [authentication overview](/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.

<Steps>
  <Step title="Redirect to login">
    Create a `CortiAuth` instance and send the user to Corti's authorization page:

    ```ts title="JavaScript" expandable theme={null}
    import { CortiAuth } from "@corti/sdk";

    const auth = new CortiAuth({
        environment: "<eu-or-us>",
        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 },
    );
    ```
  </Step>

  <Step title="User authenticates">
    The user logs in and grants your application access.
  </Step>

  <Step title="Receive authorization code">
    The user is redirected back to your `redirectUri` with a `code` query parameter:

    ```ts title="JavaScript" theme={null}
    const urlParams = new URLSearchParams(window.location.search);
    const code = urlParams.get("code");
    ```
  </Step>

  <Step title="Exchange code for tokens">
    Pass `code` to `CortiClient` — it handles the exchange and refresh automatically:

    ```ts title="JavaScript" theme={null}
    import { CortiClient } from "@corti/sdk";

    const client = new CortiClient({
        environment: "<eu-or-us>",
        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:

    ```ts title="JavaScript" theme={null}
    const tokenResponse = await auth.getCodeFlowToken({
        clientId: "<your-client-id>",
        clientSecret: "<your-client-secret>",
        redirectUri: "https://your-app.com/callback",
        code,
    });

    console.log(tokenResponse.accessToken);
    ```
  </Step>

  <Step title="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](#bearer-token) method.
  </Step>
</Steps>

***

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

<Tip>
  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.
</Tip>

<Steps>
  <Step title="Generate the authorization URL">
    The SDK handles code verifier generation and `localStorage` storage automatically:

    ```ts title="JavaScript" expandable theme={null}
    import { CortiAuth } from "@corti/sdk";

    const auth = new CortiAuth({
        environment: "<eu-or-us>",
        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:

    ```ts title="JavaScript" theme={null}
    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 },
    );
    ```
  </Step>

  <Step title="User authenticates">
    The user logs in and grants access.
  </Step>

  <Step title="Exchange code with verifier">
    After the user is redirected back, retrieve the code and verifier:

    ```ts title="JavaScript" theme={null}
    const urlParams = new URLSearchParams(window.location.search);
    const code = urlParams.get("code");
    const codeVerifier = CortiAuth.getCodeVerifier(); // retrieves from localStorage
    ```

    Pass them to `CortiClient`:

    ```ts title="JavaScript" theme={null}
    import { CortiClient } from "@corti/sdk";

    const client = new CortiClient({
        environment: "<eu-or-us>",
        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:

    ```ts title="JavaScript" theme={null}
    const tokenResponse = await auth.getPkceFlowToken({
        clientId: "<your-client-id>",
        redirectUri: "https://your-app.com/callback",
        code,
        codeVerifier,
    });

    console.log(tokenResponse.accessToken);
    ```
  </Step>

  <Step title="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](#bearer-token) method.
  </Step>
</Steps>

***

## Resource Owner Password Credentials

The ROPC flow lets users authenticate with a username and password using the SDK.

<CodeGroup>
  ```ts title="CortiClient (JavaScript)" expandable theme={null}
  import { CortiClient } from "@corti/sdk";

  const client = new CortiClient({
      environment: "<eu-or-us>",
      tenantName: "<your-tenant-name>",
      auth: {
          clientId: "<your-client-id>",
          username: "<your-username>",
          password: "<your-password>",
      },
  });
  ```

  ```ts title="CortiAuth (JavaScript)" expandable theme={null}
  import { CortiAuth } from "@corti/sdk";

  const auth = new CortiAuth({
      environment: "<eu-or-us>",
      tenantName: "<your-tenant-name>",
  });

  const tokenResponse = await auth.getRopcFlowToken({
      clientId: "<your-client-id>",
      username: "<your-username>",
      password: "<your-password>",
  });

  console.log(tokenResponse.accessToken);
  ```
</CodeGroup>

<Tip>For production web applications, prefer [PKCE](#authorization-code-flow-with-pkce) over ROPC.</Tip>

***

## 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

```ts title="JavaScript" theme={null}
import { CortiClient } from "@corti/sdk";

// Replace these with your values
const ACCESS_TOKEN = "<your-access-token>";

const client = new CortiClient({
  auth: {
    accessToken: ACCESS_TOKEN,
  },
});
```

### With automatic refresh

If you pass `clientId` and `refreshToken`, the SDK will call `refreshToken` automatically when the access token expires -- no custom callback needed:

```ts title="JavaScript" theme={null}
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:

```ts title="JavaScript" expandable theme={null}
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:

```ts title="JavaScript" theme={null}
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

| Scope          | Grants 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`:

```ts title="JavaScript" expandable theme={null}
import { CortiAuth } from "@corti/sdk";

const auth = new CortiAuth({
    environment: "<eu-or-us>",
    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:

```ts title="JavaScript" theme={null}
const client = new CortiClient({
    environment: "<eu-or-us>",
    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
```

<Tip>For even tighter security, consider routing WebSocket traffic through a [proxy](/sdk/js/proxy) instead of exposing tokens to the frontend at all.</Tip>

***

## 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](#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

<AccordionGroup>
  <Accordion title="Client Credentials">
    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`.
  </Accordion>

  <Accordion title="Bearer Token (static)">
    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.
  </Accordion>

  <Accordion title="Bearer Token with clientId + refreshToken">
    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.
  </Accordion>

  <Accordion title="Bearer Token with refreshAccessToken">
    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:

    ```ts title="JavaScript" theme={null}
    {
        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.
  </Accordion>

  <Accordion title="Authorization Code / PKCE / ROPC">
    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.
  </Accordion>
</AccordionGroup>

### 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

* **[Authentication overview](/authentication/overview)** -- OAuth2 concepts and Corti-specific details
* **[Security best practices](/authentication/security_best_practices)** -- guidance on credential management
* **[Auth examples](https://github.com/corticph/corti-examples/tree/main/sdk/typescript/next-auth-examples)** -- interactive Next.js demo of all four OAuth flows
* **[Proxy Guide](/sdk/js/proxy)** -- secure frontend patterns when using client credentials

***

<Note>For support or questions, reach out through [help.corti.app](https://help.corti.app)</Note>
