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

# Authentication

> Mint a session token on your backend, then hand it to the SDK.

The SDK authenticates with a **session token** that your backend mints. Your organization API key stays on your server and never reaches a browser.

<Steps>
  <Step title="Your backend mints a session">
    Call [`POST /v1/token`](/api-reference/authentication/mint-session-token) with your organization API key. The session it returns already carries the organization, the user, and the billing group.
  </Step>

  <Step title="Your page fetches it">
    Expose a small endpoint of your own that returns that response to the signed-in user.
  </Step>

  <Step title="The SDK takes it from there">
    Pass the response to `initSidenet({ auth })`. The SDK refreshes it on its own for as long as the session lives.
  </Step>
</Steps>

## Minting the session

<CodeGroup>
  ```javascript Your backend theme={null}
  // The organization API key lives here and only here.
  const tokens = await fetch('https://api.sidenet.ai/v1/token', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SIDENET_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      user: { id: 'user-789', name: 'Maya' },          // your stable id for the signed-in user
      billing_group: { id: 'acme-corp', name: 'Acme' }, // what this session's spend bills to
    }),
  }).then((r) => r.json());

  // → { access_token: 'snat_…', refresh_token: 'snrt_…',
  //     token_type: 'Bearer', expires_in: 900, refresh_expires_in: … }
  return tokens;
  ```

  ```typescript Your page theme={null}
  import { initSidenet } from 'sidenetai-sdk';

  const tokens = await fetch('/my-backend/sidenet-token').then((r) => r.json());

  await initSidenet({
    copilotId: 'copilot-456',
    auth: { ...tokens },
  });
  ```
</CodeGroup>

Because the session carries identity, the browser sends nothing but an `Authorization` header — no org id, no user id.

<Note>
  `user` and `billing_group` are both required, and both are objects with a required `id` and an optional `name`. A changed `name` renames the user or group in Studio; omitting it never blanks one you sent before. Full schema in the [API reference](/api-reference/authentication/mint-session-token).
</Note>

## The `auth` object

| Field           | Type               | Description                                                          |
| --------------- | ------------------ | -------------------------------------------------------------------- |
| `access_token`  | `string`           | The `snat_…` token. Lasts 15 minutes.                                |
| `refresh_token` | `string`           | The `snrt_…` token. Its presence is what turns automatic refresh on. |
| `expires_in`    | `number`           | Lifetime in seconds, as returned by the API.                         |
| `onRefresh`     | `(tokens) => void` | Optional. Fires with the rotated pair each time the SDK refreshes.   |
| `onError`       | `(error) => void`  | Optional. Fires when refreshing fails — check `error.fatal`.         |

## Automatic refresh

While `refresh_token` is present the SDK keeps the session alive by itself: it refreshes ahead of expiry, and retries once if a request still comes back `401`. The open conversation is never interrupted.

<Warning>
  **Don't run your own refresh loop alongside `onRefresh`.** Refresh tokens are single-use, so two refreshers race and one will present a token the other already spent — which revokes the session.
</Warning>

## Persisting across reloads

`onRefresh` hands you an object that is itself a valid `auth`, so restoring it is just a spread:

```javascript theme={null}
initSidenet({
  copilotId: 'copilot-456',
  auth: {
    ...JSON.parse(sessionStorage.getItem('sidenet-auth')),
    onRefresh: (t) => sessionStorage.setItem('sidenet-auth', JSON.stringify(t)),
    onError: (e) => { if (e.fatal) remintAndUpdate(); },
  },
});
```

<Warning>
  **Use `sessionStorage`, not `localStorage`.** Refresh tokens are single-use, so two tabs sharing one pair means one eventually presents a token the other already spent — and the API revokes the session for **both**.
</Warning>

<Note>
  Keep the `expiresAt` that `onRefresh` includes. Re-deriving it from `expires_in` would treat a stale token as fresh, and the first request after a reload would `401`.
</Note>

## When it fails

`onError` fires once with a `reason`. What you do depends on `fatal`:

| `fatal` | Reasons                                                  | What it means                                                                                   |
| ------- | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `true`  | `session-revoked`, `refresh-rejected`, `refresh-missing` | The SDK has stopped using the credential and will not retry. Mint a new session and swap it in. |
| `false` | `refresh-failed`                                         | Transient — a network blip or a 5xx. Already being retried; do nothing.                         |

Swapping in a fresh session needs no teardown — it replaces the credential in place, as a unit, and the conversation on screen survives:

```javascript theme={null}
updateSidenetConfig({ auth: { ...newTokens, onRefresh, onError } });
```

This is also how you **change who the user is**: identity lives in the session, so mint a session for the new user and pass it here. Only [`copilotId` is init-only](/sdk/api/configuration-updates#copilotid-is-init-only).

## Helpers

| Function                | Description                                                                           |
| ----------------------- | ------------------------------------------------------------------------------------- |
| `refreshSidenetToken()` | Force a refresh now. Single-flight. Rarely needed.                                    |
| `getSidenetAuthState()` | `{ hasToken, hasRefreshToken, expiresAt, refreshExpiresAt, dead }` — for diagnostics. |

<Accordion title="Upgrading from v2.0.123 or earlier">
  Session tokens replaced the old browser-side credential in **v2.0.124**. The identity fields are gone from `initSidenet()`:

  | Before                                       | Now                                                                           |
  | -------------------------------------------- | ----------------------------------------------------------------------------- |
  | `orgId`, `userId`                            | Carried by the session — pass `user` to `POST /v1/token` instead.             |
  | `token` / `apiKey`                           | `auth: { access_token, refresh_token, expires_in }`                           |
  | `groupName`, `groupId`                       | Still accepted, but prefer `billing_group` on `POST /v1/token`.               |
  | `runtimeAuth: [...]`                         | Removed from the browser — see [Tool Credentials](/sdk/api/tool-credentials). |
  | `updateSidenetConfig({ token })`             | `updateSidenetConfig({ auth })`                                               |
  | `destroySidenet()` to change user            | `updateSidenetConfig({ auth })` — no teardown needed.                         |
  | `getAgents(copilotId, token, orgId, userId)` | `getAgents(copilotId)`                                                        |

  ```diff theme={null}
  - initSidenet({
  -   orgId: 'org-123',
  -   userId: 'user-789',
  -   copilotId: 'copilot-456',
  -   token: 'sk_...',
  - });
  + initSidenet({
  +   copilotId: 'copilot-456',
  +   auth: { ...tokens },   // from POST /v1/token, on your backend
  + });
  ```
</Accordion>
