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 from your backend. 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 CodeBackendInteractive user login (confidential clients)
PKCEFrontend & BackendInteractive user login (public clients)
ROPCBackend onlyUsername/password for trusted or internal apps
Bearer TokenFrontend & BackendYou already have a token from another source
Custom RefreshFrontend & BackendYou manage token refresh yourself
For complete examples of each flow, see the examples repository.

Client Credentials

Backend only. Never expose your client secret in client-side code. Client Credentials tokens are service-account tokens with access to all data within the API Client. For frontend scenarios, use Bearer Token or pass tokens from your backend.
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:
C# .NET
using Corti;

var client = new CortiClient(
    tenantName: "YOUR_TENANT_NAME",
    environment: "YOUR_ENVIRONMENT_ID",
    auth: CortiClientAuth.ClientCredentials(
        clientId: "YOUR_CLIENT_ID",
        clientSecret: "YOUR_CLIENT_SECRET")
);
To get a token directly without creating a client, use CustomAuthClient:
C# .NET
using Corti;

var auth = CustomAuthClient.Create(
    new CortiAuthClientOptions
    {
        TenantName = "YOUR_TENANT_NAME",
        Environment = "YOUR_ENVIRONMENT_ID",
    });

var tokenResponse = await auth.GetTokenAsync(
    new OAuthTokenRequest
    {
        ClientId = "YOUR_CLIENT_ID",
        ClientSecret = "YOUR_CLIENT_SECRET",
    });

Console.WriteLine(tokenResponse.AccessToken);
For more details on the Client Credentials flow, see the authentication overview.

Authorization Code Flow

The standard OAuth 2.0 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 CustomAuthClient instance and send the user to Corti’s authorization page:
C# .NET
using Corti;

var auth = CustomAuthClient.Create(
    new CortiAuthClientOptions
    {
        TenantName = "YOUR_TENANT_NAME",
        Environment = "YOUR_ENVIRONMENT_ID",
    });

var authorizeUrl = await auth.AuthorizeUrlAsync(
    clientId: "YOUR_CLIENT_ID",
    redirectUri: "https://your-app.com/callback");

// Redirect the user to authorizeUrl
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.
4

Exchange code for tokens

Pass code to CortiClient — it handles the exchange and refresh automatically:
C# .NET
var client = new CortiClient(
    tenantName: "YOUR_TENANT_NAME",
    environment: "YOUR_ENVIRONMENT_ID",
    auth: CortiClientAuth.AuthorizationCode(
        clientId: "YOUR_CLIENT_ID",
        clientSecret: "YOUR_CLIENT_SECRET",
        code: codeFromCallback,
        redirectUri: "https://your-app.com/callback")
);

// The SDK exchanges the code and manages refresh automatically
_ = await client.Interactions.ListAsync(new InteractionsListRequest());
Or exchange manually using the auth instance from step 1:
C# .NET
var tokenResponse = await auth.GetTokenAsync(
    new OAuthAuthCodeTokenRequest
    {
        ClientId = "YOUR_CLIENT_ID",
        ClientSecret = "YOUR_CLIENT_SECRET",
        RedirectUri = "https://your-app.com/callback",
        Code = codeFromCallback,
    });

Console.WriteLine(tokenResponse.AccessToken);
5

Use tokens

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

PKCE Flow

The Authorization Code flow with Proof Key for Code Exchange — recommended for public clients where a client secret cannot be stored securely (native apps, SPAs).
You can run PKCE (and refresh-token exchange) from a frontend, but only from an origin that is allowed by your OAuth client configuration. In practice, the page origin must match one of the origins implied by your configured redirectUri values.
1

Generate the authorization URL with code challenge

C# .NET
using Corti;

var auth = CustomAuthClient.Create(
    new CortiAuthClientOptions
    {
        TenantName = "YOUR_TENANT_NAME",
        Environment = "YOUR_ENVIRONMENT_ID",
    });

var codeVerifier = GenerateCodeVerifier();
var codeChallenge = GenerateCodeChallenge(codeVerifier);

var authorizeUrl = await auth.AuthorizeUrlAsync(
    clientId: "YOUR_CLIENT_ID",
    redirectUri: "https://your-app.com/callback",
    codeChallenge: codeChallenge);

// Redirect the user to authorizeUrl
2

User authenticates

The user logs in and grants access.
3

Exchange code with verifier

After the user is redirected back with a code parameter, pass it with the verifier to CortiClient:
C# .NET
var client = new CortiClient(
    tenantName: "YOUR_TENANT_NAME",
    environment: "YOUR_ENVIRONMENT_ID",
    auth: CortiClientAuth.Pkce(
        clientId: "YOUR_CLIENT_ID",
        code: codeFromCallback,
        redirectUri: "https://your-app.com/callback",
        codeVerifier: codeVerifier)
);

_ = await client.Interactions.ListAsync(new InteractionsListRequest());
Or exchange using the auth instance from step 1:
C# .NET
var tokenResponse = await auth.GetTokenAsync(
    new OAuthPkceTokenRequest
    {
        ClientId = "YOUR_CLIENT_ID",
        Code = codeFromCallback,
        RedirectUri = "https://your-app.com/callback",
        CodeVerifier = codeVerifier,
    });

Console.WriteLine(tokenResponse.AccessToken);
4

Use tokens

The client from the CortiClient path is ready to use. For the CustomAuthClient 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.
using Corti;

// CortiClient — exchanges credentials and manages refresh automatically
var client = new CortiClient(
    tenantName: "YOUR_TENANT_NAME",
    environment: "YOUR_ENVIRONMENT_ID",
    auth: CortiClientAuth.Ropc(
        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

C# .NET
using Corti;

var client = new CortiClient(
    auth: CortiClientAuth.Bearer(accessToken: "YOUR_ACCESS_TOKEN")
);
When using a bearer token, the SDK decodes the JWT to infer tenantName and environment automatically. You can override them if needed by using the (tenantName, environment, auth) constructor overload.

With automatic refresh

If you pass clientId and refreshToken, the SDK will call refreshToken automatically when the access token expires — no custom callback needed:
C# .NET
using Corti;

var client = new CortiClient(
    auth: CortiClientAuth.Bearer(
        accessToken: "YOUR_ACCESS_TOKEN",
        clientId: "YOUR_CLIENT_ID",
        refreshToken: "YOUR_REFRESH_TOKEN")
);
For custom refresh logic, see Custom Refresh below.

Custom Refresh

Provide your own refresh logic so the SDK can transparently refresh expired tokens:
C# .NET
using Corti;

var client = new CortiClient(
    auth: CortiClientAuth.BearerCustomRefresh(
        accessToken: "YOUR_ACCESS_TOKEN",
        refreshAccessToken: async (currentToken, cancellationToken) =>
        {
            var newToken = await YourAuthService.RefreshAsync(currentToken);

            return new CustomRefreshResult
            {
                AccessToken = newToken.AccessToken,
                ExpiresIn = newToken.ExpiresIn,
            };
        })
);
You can also start without an initial access token. The SDK will call your delegate immediately on the first request:
C# .NET
using Corti;

var client = new CortiClient(
    auth: CortiClientAuth.BearerCustomRefresh(
        refreshAccessToken: async (currentToken, cancellationToken) =>
        {
            var newToken = await YourAuthService.GetTokenAsync();

            return new CustomRefreshResult
            {
                AccessToken = newToken.AccessToken,
                ExpiresIn = newToken.ExpiresIn,
            };
        })
);
The CustomRefreshResult supports additional fields:
PropertyTypeDescription
AccessTokenstringRequired. The new access token
ExpiresInint?Token lifetime in seconds
RefreshTokenstring?Updated refresh token (if rotated)
RefreshExpiresInint?Refresh token lifetime in seconds
TokenTypestring?Token type (typically "Bearer")

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 CustomAuthClient:
C# .NET
using Corti;

var auth = CustomAuthClient.Create(
    new CortiAuthClientOptions
    {
        TenantName = "YOUR_TENANT_NAME",
        Environment = "YOUR_ENVIRONMENT_ID",
    });

// Token that can only access the transcribe endpoint
var transcribeToken = await auth.GetTokenAsync(
    new OAuthTokenRequestWithScopes
    {
        ClientId = "YOUR_CLIENT_ID",
        ClientSecret = "YOUR_CLIENT_SECRET",
        Scopes = ["transcribe"],
    });

// Token with both WebSocket scopes
var wsToken = await auth.GetTokenAsync(
    new OAuthTokenRequestWithScopes
    {
        ClientId = "YOUR_CLIENT_ID",
        ClientSecret = "YOUR_CLIENT_SECRET",
        Scopes = ["streams", "transcribe"],
    });
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, and a static token is used as-is until it expires. With Custom Refresh, your delegate controls the refresh behaviour entirely.

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.
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 delegate via CortiClientAuth.BearerCustomRefresh, the SDK calls it automatically when the access token is expired or missing. Your delegate receives the current access token (if any) and must return a CustomRefreshResult.If the initial accessToken is omitted, the SDK calls your delegate 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.

Proxy / passthrough mode

When your server acts as a proxy and handles authentication externally, create a client with only an environment:
C# .NET
using Corti;

var env = CortiEnvironments.FromBaseUrl("https://your-proxy.com/api");

var client = new CortiClient(environment: env);

Resources


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