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

# Embedding securely

> Your backend uses the API key, your frontend uses a session token — what the token carries, what it can't do, and how refresh works.

The rule, in one sentence: **your backend uses the organization API key; your
frontend uses a session token.** [Authentication](/docs/authentication) covers the
mechanics of each credential. This page is about the trust boundary between
them — what a browser can and cannot be trusted with, and how the design
makes the wrong thing unreachable rather than merely discouraged.

## Why

An organization API key can do everything: rewrite agents, read tool-provider
configuration, mint tokens, run experiments. Before sessions there were two
ways to put chat in a browser, and both were bad: ship the key to the page,
or proxy every streaming request through your own servers so the key never
left them.

A session token is the third option. Your backend vouches **once** — this is
user `maya`, she bills to group `acme`, these are her CRM credentials — and
gets back a short-lived token that carries all three and can change none of
them. The browser talks to Sidenet directly with that token, streaming
included.

## Minting

```bash theme={null}
curl -X POST https://api.sidenet.ai/v1/token \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id":  "maya",
    "group_id": "acme",
    "tools_auth": {
      "CRM_PROVIDER_ID": { "credentials": { "token": "her-crm-token" } }
    }
  }'
```

Only the organization API key can mint. A session token presenting itself to
`POST /v1/token` gets a `403`, because a session that could mint sessions
would have unbounded lifetime. Expose a small endpoint of your own that mints
for the signed-in user and returns the response to the page.

Three things are decided at this moment and fixed for the life of the
session:

| Claim                                            | Where it comes from                                            | Can the browser change it?                                             |
| ------------------------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Who the user is                                  | `user_id`                                                      | No — every call the token makes is that user.                          |
| Who pays                                         | `group_id` (or the user's current group, or the Default group) | No — the billing group comes from the session, never the request body. |
| Which upstream credentials the agent's tools use | `tools_auth`                                                   | No — anything credential-shaped the page sends is ignored.             |

Tool credentials are stored on the user, encrypted, not on the token. The
token is a reference; the secret never travels to the browser at all. See
[Authenticating tool providers](/docs/tool-auth#per-user-credentials-at-runtime).

## What a session cannot do

Session tokens are confined to the chat runtime by an allow-list of routes,
enforced before any handler runs:

* **Chat** — `POST /v1/chat`, the stream resume and cancel endpoints,
  threads, messages, votes, marking a thread read, running an agent, a
  network, a workflow or a tool.
* **The user's own automations** — listing their workflows, and creating,
  reading, editing, pausing, resuming and running their schedules. Every one
  of these is scoped to the session's user; anyone else's answers `404`.
* **Widget reads** — the network's configuration, the agent list, the tool
  list, and the credential-field descriptors from `GET /v1/tools-auth`.

Everything else returns `403` with a message saying the endpoint needs the
organization API key from your backend. That covers every write to
organization configuration — agents, workflows, prompt blocks, tool
providers — and also reads that would expose configuration, such as
`GET /v1/tool-providers`, which returns provider auth settings even in
redacted form. There is deliberately no token value that grants writes to
organization configuration.

Nor can a session claim another identity. `X-User-Id` headers, `userId`
fields and `runtimeAuth` maps in a request body are all ignored under a
session token; the API takes user, group and credentials from the session and
nothing else.

## Refresh

|                          | Lifetime                                              |
| ------------------------ | ----------------------------------------------------- |
| Access token (`snat_…`)  | 15 minutes                                            |
| Refresh token (`snrt_…`) | 30 days, sliding forward on each rotation             |
| Session                  | 90 days, a hard ceiling that refreshing never extends |

`POST /v1/token/refresh` takes only the refresh token, no API key and no
identity, so the browser can call it directly. Both tokens are replaced on
every call and the presented refresh token is spent.

The SDK does this for you. Give `initSidenet` the mint response as `auth` —
`access_token`, `refresh_token`, `expires_in` — and, while a refresh token is
present, it refreshes ahead of expiry, retries once if a request still comes
back `401`, and never interrupts an answer that is streaming: the new
credential applies to the next outgoing request. `auth` is replaced **whole**
when you update it, never merged, and swapping in a session for a different
user needs no teardown. See [SDK authentication](/docs/sdk/authentication) for
`onRefresh`, `onError` and persisting across reloads.

<Warning>
  Refresh tokens are single-use, and the server treats a second presentation
  of the same one as proof that two holders exist. Don't run your own refresh
  loop next to the SDK's, and keep the pair in `sessionStorage` rather than
  `localStorage` — two tabs sharing one refresh token will revoke the session
  for both.
</Warning>

## Revocation

* **Reuse revokes.** Presenting a spent refresh token, or losing the race
  when two refreshes arrive at once, revokes the entire session with a `401`.
  The client must bootstrap again through your backend; the SDK reports this
  as a fatal `session-revoked` error.
* **Revoking the API key kills every session it minted**, immediately and
  with no bookkeeping — session validity is checked against the key on every
  request.
* **Cached credentials are evicted on revocation**, so a revoked token stops
  working within the same request cycle rather than at the end of a cache
  window.

Stored tool credentials outlive sessions on purpose, so scheduled workflows
keep running between logins. Remove them explicitly with a `null` entry on
`PATCH /v1/users/{userId}` when a user disconnects an integration or
offboards.

## Migrating from the API-key embed

SDK versions before 2.0.124 took an organization credential and identity
fields in the browser. The session model replaces all of them:

| Before (in the browser)                      | Now                                                                        |
| -------------------------------------------- | -------------------------------------------------------------------------- |
| `orgId`, `userId` on `initSidenet`           | Carried by the session; pass `user_id` to `POST /v1/token` on your backend |
| `token` / `apiKey`                           | `auth: { access_token, refresh_token, expires_in }` from the mint response |
| `groupId`, `groupName`                       | `group_id` on `POST /v1/token`; names via `PATCH /v1/groups/{groupId}`     |
| `runtimeAuth: [...]`                         | `tools_auth` on `POST /v1/token`, keyed by provider id                     |
| `updateSidenetConfig({ token })`             | `updateSidenetConfig({ auth })`                                            |
| `destroySidenet()` to change user            | `updateSidenetConfig({ auth })` with a session for the new user            |
| `getAgents(copilotId, token, orgId, userId)` | `getAgents(copilotId)`                                                     |

The only new piece of infrastructure is the endpoint on your backend that
mints. Everything the page used to assert about identity, billing and
credentials moves behind it.
