# Authentication

> API-key signing, email/password login, 2FA, token refresh, and background session keep-alive.

Source: https://docs.stxapp.io/sdks/csharp/authentication/

The SDK supports two ways to authenticate, and you pick one when you register it.

| | When to use it |
|---|---|
| **API key** (recommended) | Programmatic access: bots, services, anything unattended |
| **Email and password** | Apps acting on behalf of a person signing in, including 2FA flows |

API keys are the better fit for automated callers: there is no login call, no token to expire, and no refresh cycle, so a restart or an API deployment cannot leave a client without a session.

## API-key authentication

Create a key under **Account → API Keys**, which gives you a key ID and an Ed25519 private key. Register the SDK with both:

```csharp
using STX.Sdk;
using STX.Sdk.Auth;
using STX.Sdk.Settings;

services.ConfigureSTXServices(
    STXEnvironment.OntarioProduction,
    STXApiKeyCredentials.FromPemFile(keyId, "~/.stx/ontario.pem"));
```

`FromPemFile` reads the key from disk so it never has to pass through an environment variable, shell history, or a process listing. If you already hold the PEM in memory, the constructor takes it directly:

```csharp
new STXApiKeyCredentials(keyId, pemContents);
```

There is nothing else to call. Every request is signed automatically with three headers:

| Header | |
|---|---|
| `X-STX-ACCESS-KEY` | Your key ID |
| `X-STX-ACCESS-TIMESTAMP` | Unix time in milliseconds |
| `X-STX-ACCESS-SIGNATURE` | Base64 Ed25519 signature over the timestamp, method, and path |

:::caution
The server rejects any request whose timestamp is more than 30 seconds from its own clock. Keep the host clock on NTP.
:::

### Knowing who you are

There is no login response under API-key auth, so the user id that WebSocket channels need has to be fetched once:

```csharp
var identity = await serviceProvider.GetRequiredService<STXIdentityService>().GetMeAsync();

Console.WriteLine($"{identity.UserId} / account {identity.AccountId} / scope {identity.Scope}");
```

Do this at startup, before subscribing to any user-scoped channel.

### Scopes

A key is issued as either `read_only` or `read_write`. A `read_only` key can query markets, orders, trades, and settlements, but placing or cancelling an order needs `read_write`. `STXIdentity.Scope` reports which one you hold.

### Bringing your own cryptography

`STXApiKeyCredentials` also accepts a signing delegate, for callers who keep private keys in an HSM or a KMS rather than on disk:

```csharp
new STXApiKeyCredentials(keyId, message => myHsm.SignEd25519(message));
```

## Existing integrations: email and password

:::note
Email and password authentication is supported for integrations that already use it. It is **not recommended for new work**, and it is not documented here. Use an API key instead.
:::

The path still exists and keeps working: `STXLoginService.LoginAsync` acquires a JWT, `STXSessionBackgroundService` refreshes it, and everything else in the SDK behaves the same once authenticated. Nothing about it changed in 1.6.0.

It is the weaker option for a programmatic caller. A token expires, so a restart or an API deployment can leave a client without a session, and the credentials have to sit in memory for the background service to re-login. An API key has none of those properties.

### Moving to an API key

1. Create a key under **Account → API Keys** and choose a scope. `read_only` covers market, order, trade and settlement queries; placing or cancelling orders needs `read_write`.
2. Swap the registration to the API-key overload shown above. Nothing else in your code changes.
3. Replace the user id you read from the login response with one `GetMeAsync()` call at startup, since there is no login response to read.
4. Drop the `keepSessionAlive` handling. There is no token to keep alive.

## Accept terms & conditions

Until the current terms are accepted, authenticated calls are rejected.

:::caution
**`CheckTermsAndConditionsAsync` does not only check.** If the current terms are already in effect, it **accepts them on the user's behalf** and returns the result of that acceptance. Do not call it as a read-only test before asking a user for consent: by the time it returns, consent has been recorded.
:::

To show the terms and accept only after the user has agreed, use the two explicit calls and do not call `CheckTermsAndConditionsAsync` at all:

```csharp
var tnc = serviceProvider.GetRequiredService<STXTermsAndConditionsService>();

// Read-only: fetches the current terms without accepting anything.
var current = await tnc.GetTermsAndConditionsAsync();

// Display current.Version to the user. Only once they have agreed in your UI:
await tnc.AcceptTermsAndConditionsAsync(current.Version);
```

`AcceptTermsAndConditionsAsync` also takes no argument, in which case the server accepts the version currently in effect. Pass the version you actually displayed where you can, so what was accepted is what the user saw.

## Logout

There is no server-side logout call.

With an API key there is nothing to end: requests are signed individually and no session is held. To revoke access, delete the key under **Account → API Keys**. Signatures made with it stop being accepted immediately.

On the legacy email/password path the session lives in `STXUserStorage`, which is a singleton. Stop the host to end it, and stop any channels you started with `StopAsync`.

## Errors

| Exception | Cause |
|---|---|
| `STXRequestFailedException` | Signature rejected. Check the host clock: the server allows 30 seconds of skew |
| `STXWrongCredentialsException` | Bad email/password, on the legacy path |
| `STXSessionExpiredException` | Token and refresh token both expired, on the legacy path |
| `STXGeoComplyException` | Geo-compliance check failed; user is outside a permitted jurisdiction |

See [Errors & retries](/sdks/csharp/errors-and-retries/) for the full list and recovery patterns.
