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

# C# .NET SDK - Authentication

> All authentication methods for the Corti C# .NET 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 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](#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) | Backend            | Interactive user login (confidential clients)  |
| [PKCE](#pkce-flow)                             | Frontend & Backend | Interactive user login (public clients)        |
| [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   |
| [Custom Refresh](#custom-refresh)              | Frontend & Backend | You manage token refresh yourself              |

<Info>For complete examples of each flow, see the [examples repository](https://github.com/corticph/corti-examples).</Info>

***

## Client Credentials

<Warning>**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](#bearer-token) or pass tokens from your backend.</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:

```csharp title="C# .NET" theme={null}
using Corti;

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

var client = new CortiClient(
    tenantName: TENANT,
    environment: ENVIRONMENT,
    auth: CortiClientAuth.ClientCredentials(
        clientId: CLIENT_ID,
        clientSecret: CLIENT_SECRET)
);
```

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

```csharp title="C# .NET" theme={null}
using Corti;

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

var auth = CustomAuthClient.Create(
    new CortiAuthClientOptions
    {
        TenantName = TENANT,
        Environment = ENVIRONMENT,
    });

var tokenResponse = await auth.GetTokenAsync(
    new OAuthTokenRequest
    {
        ClientId = CLIENT_ID,
        ClientSecret = CLIENT_SECRET,
    });

Console.WriteLine(tokenResponse.AccessToken);
```

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

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

    ```csharp title="C# .NET" theme={null}
    using Corti;

    var auth = CustomAuthClient.Create(
        new CortiAuthClientOptions
        {
            TenantName = "<your-tenant-name>",
            Environment = "<eu-or-us>",
        });

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

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

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

    ```csharp title="C# .NET" theme={null}
    var client = new CortiClient(
        tenantName: "<your-tenant-name>",
        environment: "<eu-or-us>",
        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:

    ```csharp title="C# .NET" theme={null}
    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);
    ```
  </Step>

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

***

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

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

<Steps>
  <Step title="Generate the authorization URL with code challenge">
    ```csharp title="C# .NET" theme={null}
    using Corti;

    var auth = CustomAuthClient.Create(
        new CortiAuthClientOptions
        {
            TenantName = "<your-tenant-name>",
            Environment = "<eu-or-us>",
        });

    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
    ```
  </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 with a `code` parameter, pass it with the verifier to `CortiClient`:

    ```csharp title="C# .NET" theme={null}
    var client = new CortiClient(
        tenantName: "<your-tenant-name>",
        environment: "<eu-or-us>",
        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:

    ```csharp title="C# .NET" theme={null}
    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);
    ```
  </Step>

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

***

## Resource Owner Password Credentials

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

<CodeGroup>
  ```csharp title="C# .NET" theme={null}
  using Corti;

  var client = new CortiClient(
      tenantName: "<your-tenant-name>",
      environment: "<eu-or-us>",
      auth: CortiClientAuth.Ropc(
          clientId: "<your-client-id>",
          username: "<your-username>",
          password: "<your-password>")
  );
  ```

  ```csharp title="C# .NET" theme={null}
  var tokenResponse = await auth.GetTokenAsync(
      new OAuthRopcTokenRequest
      {
          ClientId = "<your-client-id>",
          Username = "<your-username>",
          Password = "<your-password>",
      });

  Console.WriteLine(tokenResponse.AccessToken);
  ```
</CodeGroup>

<Tip>For production web applications, prefer [PKCE](#pkce-flow) 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

```csharp title="C# .NET" theme={null}
using Corti;

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

var client = new CortiClient(
    auth: CortiClientAuth.Bearer(accessToken: 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:

```csharp title="C# .NET" theme={null}
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](#custom-refresh) below.

***

## Custom Refresh

Provide your own refresh logic so the SDK can transparently refresh expired tokens:

```csharp title="C# .NET" theme={null}
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:

```csharp title="C# .NET" theme={null}
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:

| Property           | Type      | Description                        |
| :----------------- | :-------- | :--------------------------------- |
| `AccessToken`      | `string`  | **Required.** The new access token |
| `ExpiresIn`        | `int?`    | Token lifetime in seconds          |
| `RefreshToken`     | `string?` | Updated refresh token (if rotated) |
| `RefreshExpiresIn` | `int?`    | Refresh token lifetime in seconds  |
| `TokenType`        | `string?` | 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

| 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 `CustomAuthClient`:

```csharp title="C# .NET" expandable theme={null}
using Corti;

var auth = CustomAuthClient.Create(
    new CortiAuthClientOptions
    {
        TenantName = "<your-tenant-name>",
        Environment = "<eu-or-us>",
    });

// 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"],
    });
```

<Tip>For even tighter security, consider routing WebSocket traffic through a 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, and a static token is used as-is until it expires. With [Custom Refresh](#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

<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.
  </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="Custom Refresh (BearerCustomRefresh)">
    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.
  </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.

***

## Proxy / passthrough mode

When your server acts as a proxy and handles authentication externally, create a client with only an environment:

```csharp title="C# .NET" theme={null}
using Corti;

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

var client = new CortiClient(environment: env);
```

***

## Resources

* **[Authentication overview](/authentication/overview)** -- OAuth 2.0 concepts and Corti-specific details
* **[Security best practices](/authentication/security_best_practices)** -- credential management guidance
* **[Examples repository](https://github.com/corticph/corti-examples)** -- working code for authentication, streaming, text generation, and more

***

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