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

# Authentication

> How to authenticate requests and scope them to an organization.

There are two credentials, and one rule for choosing between them:

**Your backend uses the organization API key. Your frontend uses a session token.**

The API key can rewrite your agents and read your tool-provider configuration, so
it must never reach a browser. When you need to call Sidenet from client-side
code, exchange it for a [session token](#session-tokens-for-browsers) — a
short-lived credential scoped to one end user and to the chat runtime.

If you call Sidenet only from your own servers, the API key is all you need and
nothing below changes for you.

## Bearer token

All routes require a bearer token. Send it in the `Authorization` header:

```bash theme={null}
curl https://api.sidenet.ai/v1/agents \
  -H "Authorization: Bearer YOUR_API_KEY"
```

In the [API Reference](/docs/api-reference) "Try it" panel, paste your token into the
**Authorization** field and it is sent automatically.

## Organization context

Requests are scoped to the organization your API key belongs to. There is
nothing extra to send — the key already identifies the org.

## Which credential does each endpoint take?

Every reference page states what it accepts under **Authorization**. There are
three classes:

| Authorization shows          | Meaning                                                                                                                                                                           |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| API key                      | Organization configuration — agents, workflows, prompt blocks, tool providers, minting tokens. Call from your backend. No user identity is needed (or read) on these.             |
| API key **or** session token | Org-scoped reads and schedule management a chat widget may also call directly.                                                                                                    |
| Session token                | User-scoped endpoints — chat, threads, messages, votes, running agents and workflows as a user. The token already carries the end user, so nothing else is sent to identify them. |

## User identity

User identity travels in the [session token](#session-tokens-for-browsers):
your backend mints one for an end user, and every request made with it is that
user — from a browser or from a server. User-scoped resources resolve to
`{orgId}_{userId}`. Nothing else is sent to identify the user, and
organization-scoped endpoints don't need an identity at all.

## Session tokens (for browsers)

A session token lets client-side code call the chat endpoints without holding
your API key. Your backend vouches once for who the end user is, which group
they bill to, and which tool credentials they may use; the browser gets back a
token that carries all of that without being able to change any of it.

### 1. Mint a token on your server

```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":  "END_USER_ID",
    "group_id": "TEAM_ID",
    "tools_auth": {
      "TOOL_PROVIDER_ID": { "credentials": { "token": "their-upstream-token" } }
    }
  }'
```

```json theme={null}
{
  "access_token": "snat_…",
  "refresh_token": "snrt_…",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_expires_in": 2592000
}
```

Only `user_id` is required. An omitted `group_id` bills the session to the
user's current group — whatever a previous mint, chat call, or
[`PATCH /v1/users/{userId}`](#updating-a-user-without-re-minting) set — falling
back to your organization's Default group. The mint takes ids only: display
names for users and groups are set on the update routes. Send only
`access_token` to the browser if it has no need to refresh, or both if it
does.

<Warning>
  The group fallback is silent. If you bill per team, send
  `group_id` explicitly (or keep users current via `PATCH /v1/users/{userId}`)
  — a user whose group was never set bills to the Default group without an error.
</Warning>

### 2. Call the chat endpoints from the browser

```bash theme={null}
curl https://api.sidenet.ai/v1/chat \
  -H "Authorization: Bearer snat_…" \
  -d '{ "copilotId": "…", "messages": [ … ] }'
```

Note what is **not** in that request: no API key and no identity fields. The
user, the group and the tool credentials all come from the session.
Anything identity-shaped the browser sends is ignored, which is the point — a
client cannot claim to be a different user, bill a different group, or
substitute its own credentials.

### 3. Refresh when the access token expires

```bash theme={null}
curl -X POST https://api.sidenet.ai/v1/token/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refresh_token": "snrt_…" }'
```

Safe to call directly from the browser — it takes no API key and no identity
fields. Both tokens are replaced each time and the old refresh token is spent.

<Warning>
  Refresh tokens are single-use. Presenting one that has already been spent means
  two holders exist, so the entire session is revoked and the call returns `401`.
  When that happens, bootstrap again through your backend.
</Warning>

### What a session token can reach

Session tokens are scoped to the chat runtime:

`POST /v1/chat` · `GET|DELETE /v1/chat/stream` · `GET /v1/threads` ·
`POST /v1/threads/{id}/read` · `GET /v1/messages` · `POST /v1/vote` ·
`POST /v1/agents/{id}/run` · `POST /v1/copilots/{id}/run` ·
`POST /v1/workflows/{id}/run` · `POST /v1/tools/{toolId}/execute` ·
`GET /v1/copilots/{id}` · `GET /v1/agents` · `GET /v1/agents/{id}` ·
`GET /v1/tools` · the user's own workflow schedules and runs
(`/v1/workflow-schedules…`, `/v1/workflow-runs…`)

Everything else — creating or editing agents, workflows, prompt blocks and tool
providers — returns `403` and requires the API key from your backend. There is no
token that grants write access to your organization's configuration.

### Tool credentials (`tools_auth`)

`tools_auth` is a map keyed by tool provider id, each entry carrying the
provider's `credentials` (plus an optional `base_url` override):

```json theme={null}
{ "PROVIDER_ID": { "credentials": { "token": "…" }, "base_url": "https://…" } }
```

Credentials **persist on the user**, not on the session: everything the user
touches — their sessions, workflow runs, scheduled workflows — reads the same
stored credentials, and a later mint inherits them without re-sending. Each
push replaces only the providers it names; delete one by sending a `null`
entry to [`PATCH /v1/users/{userId}`](#updating-a-user-without-re-minting)
(e.g. when the user disconnects an integration).

`GET /v1/tools-auth` lists your organization's providers and the credential
fields each one expects — call it when building the map. Runtime auth is
organization-scoped, so the list is the same for every copilot (the
`GET /v1/copilots/{id}` read also bundles it as `runtimeAuthProviders`).

### Lifetimes and revocation

|               | Default    | Notes                                                             |
| ------------- | ---------- | ----------------------------------------------------------------- |
| Access token  | 15 minutes | Short by design; this bounds exposure of a token a browser holds. |
| Refresh token | 30 days    | Slides forward on each rotation.                                  |
| Session       | 90 days    | Hard ceiling. Refreshing never extends a session past it.         |

Revoking the API key that minted a session ends that session immediately, along
with every other session minted from that key. Stored tool credentials outlive
sessions — remove them with `null` entries on `PATCH /v1/users/{userId}` when a
user offboards or disconnects an integration.

<Note>
  Credentials in `tools_auth` are encrypted at rest and injected server-side when
  the agent calls a tool. They are never returned by any endpoint, never logged,
  and never used as part of a cache key — including by the endpoints that accept
  them.
</Note>

## Updating a user without re-minting

Everything the mint call vouches for can be changed later, from your backend,
while the browser keeps the session token it already holds:
[`PATCH /v1/users/{userId}`](/docs/api-reference) updates the user's display name,
their per-provider tool credentials, and/or their group.

Users can also be created ahead of use with `POST /v1/users` — same fields
plus the `id` — so their group, name and credentials are in place before
their first session. Like groups, creating explicitly is optional: a mint
with an unseen `user_id` provisions the user automatically.

```bash theme={null}
curl -X PATCH https://api.sidenet.ai/v1/users/END_USER_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ada Lovelace",
    "group_id": "TEAM_ID",
    "tools_auth": {
      "TOOL_PROVIDER_ID": { "credentials": { "token": "their-rotated-token" } }
    }
  }'
```

All fields are optional (send at least one). Credentials persist on the user,
and every surface — **live sessions**, workflow runs, **scheduled workflows** —
reads through the same store, so a push here reaches all of them within a
minute. Only the providers you send are touched; a `null` entry deletes that
provider's stored credentials.

This is the endpoint to reach for when an end user's upstream token is
short-lived, when they move teams, when they disconnect an integration, or
when their scheduled workflows should keep running with fresh credentials
between logins. Like the mint call, it takes the org API key and must be
called from your backend — a session token cannot call it.

## Groups

A group is the unit users belong to — today it is what billing and spend caps
attach to. Create one ahead of use with `POST /v1/groups`, so its cap is in
place before the first user references it:

```bash theme={null}
curl -X POST https://api.sidenet.ai/v1/groups \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "id": "TEAM_ID", "name": "Acme Corp", "monthly_limit": 250 }'
```

Creating explicitly is optional: minting a token or updating a user with an
unseen group id provisions the group automatically, with a default cap.
Either way, `PATCH /v1/groups/{groupId}` renames a group or changes its cap
later:

```bash theme={null}
curl -X PATCH https://api.sidenet.ai/v1/groups/TEAM_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "monthly_limit": 500 }'
```

The cap is enforced per calendar month; `null` removes it.

## Errors

| Status | Meaning                                                                                                                                                 |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing or invalid bearer token; or a refresh token that is unknown, expired, revoked, or already spent.                                                |
| `403`  | Token is valid but not authorized for the requested organization or resource — including a session token used on an endpoint that requires the API key. |
| `429`  | Usage limit reached for the organization.                                                                                                               |
