# Sessions and tokens

A user enters the chat with a session token. Your server vouches for the user with a client token, the chat exchanges it for a session token, and every user call after that carries the session token.

## Before you start

- An app with an active server secret (`mcs_...`) and your site's origin listed. See [Quickstart](/get-started/quickstart/).
- The claims a client token carries:

| Claim | Required | Value |
|---|---|---|
| `iss` | yes | your app id |
| `sub` | yes | the user's id on your site, 1 to 128 chars of `A-Z a-z 0-9 . _ : @ -`; do not start it with `guest:` |
| `name` | yes | display name |
| `iat`, `exp` | yes | seconds; `exp` at most 600 after `iat`, 30s clock skew allowed |
| `role` | no | a role key from your settings; default `default_role` |
| `avatar` | no | https URL |
| `lang` | no | `it`, `en`, `es` or `fr` |
| `tier` | no | a `workspace.tiers` key; left out keeps the stored one |
| `perms` | no | per-user overrides of policy flags and limits, for example `{"call": false}` |
| `peer` | no | the external id this user was sent to talk to; counts as a vouch |
| `jti` | no | when present, the token is single use |

The header is `alg: HS256` and `typ: JWT`, or no `typ`. Any other algorithm is refused, and so is a vouch token. The signature is checked against every active secret of the app. The Quickstart has a token endpoint in Node, PHP and Python.

## Steps

The embed runs this flow for you. Read it once to know what to build for your own client.

```text
Browser (embed.js)      Your server             mondochat API
       |                    |                         |
       | 1. GET /mondochat-token                      |
       |------------------->| 2. sign client token    |
       |<-------------------|    (server secret)      |
       | 3. POST /v1/session {app_id, token}          |
       |--------------------------------------------->|
       |                    |    4. verify, upsert user
       |<---------------------------------------------|
       |     session_token, expires_at, user, app, realtime
       | 5. user calls with Authorization: Bearer <session_token>
       |--------------------------------------------->|
       | 6. POST /v1/session/refresh before expiry    |
       |--------------------------------------------->|
```

1. The page asks your token endpoint for a token. `getToken` returns it, or `null` when nobody is logged in.
2. Your server signs a client token for the logged-in user with the server secret. Keep `exp` short and answer with `Cache-Control: no-store`.
3. The chat posts `{app_id, token}` to [`POST /v1/session`](/api/operations/exchange-session/). The route is public: the token is the proof.
4. The API checks the token and creates or updates the user by `(app, sub)` with the claim profile. It answers `{session_token, expires_at, user, app, realtime}`. `app` is the frame configuration for this viewer and `realtime` holds the socket host and channel.
5. The session token is an ES256 token that lasts 15 minutes. User calls send it as `Authorization: Bearer <session_token>`.
6. [`POST /v1/session/refresh`](/api/operations/refresh-session/) issues a new session token while the session is under 12 hours old and the user still exists and is not banned. After 12 hours, or when refresh fails, the session must start again with a new client token: the embed calls `getToken` and begins at step 3.

A refresh also re-checks bans and whether the tenant is suspended. A ban, a deletion or a guest merge also tells the open frame at once with the `session.revoked` event, and the frame signs out.

### Guests and merge

With `guests.enabled` on, a call to `POST /v1/session` with no `token` creates a guest and answers a `guest_token` as well. A guest token is an ES256 token that lasts 400 days. The embed stores it in `localStorage` under `mondochat:<appId>:guest` and sends it back to resume the same guest. A guest token of another app is refused with 401. A guest is named `<guests.name_prefix> <last 6 characters of its id>`.

With `guests.merge_on_login` on (the default), a client token that arrives together with a guest token of the same app for an unmerged guest active in the last 30 days merges the guest into the member:

- The guest's conversations and messages move to the member.
- Conversations with the same peer fold into the member's, and unread counts are recomputed.
- The guest receives `session.revoked` with reason `merged`.

A merge twice changes nothing. Your server also gets `user.merged`.

### Server sessions for headless clients

A native app or your own web client can skip the embed. Your server mints the session with the server secret and sends two headers on every server call:

```text
Authorization: Bearer <mcs_ server secret>
Mondochat-App-Id: <app id>
```

```sh
    curl -X POST https://app.mondochat.io/v1/server/sessions \
      -H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID" \
      -H 'content-type: application/json' \
      -d '{"external_id": "alice", "name": "Alice"}'
    ```
  ```js
    const res = await fetch('https://app.mondochat.io/v1/server/sessions', {
      method: 'POST',
      headers: {
        authorization: `Bearer ${process.env.MONDOCHAT_SERVER_SECRET}`,
        'mondochat-app-id': process.env.MONDOCHAT_APP_ID,
        'content-type': 'application/json',
      },
      body: JSON.stringify({ external_id: 'alice', name: 'Alice' }),
    })
    const { session_token, expires_at, user, realtime } = await res.json()
    ```
  [`POST /v1/server/sessions`](/api/operations/mint-server-session/) takes the same fields as [`PUT /v1/server/users/{external_id}`](/api/operations/upsert-user/) plus `external_id`: `name` is required, and `avatar`, `role`, `lang`, `perms` and `tier` are optional. It answers `{session_token, expires_at, user, realtime}`. The token lasts 15 minutes and is the Bearer for the user routes. It refreshes with `POST /v1/session/refresh` like any session, for 12 hours after it was minted.

Hand `session_token` and `realtime` to your client. `realtime` is the same block `POST /v1/session` answers: the client opens `ws_url`, authorizes with `http_host` and the session token, and subscribes to `channel`, its user's own `/users/{user_id}`. After each refresh it reconnects with the new token. [Connection](/realtime/connection/) has the handshake. The token opens that one channel and no other, exactly like a session from the embed.

[`POST /v1/server/guests`](/api/operations/mint-server-guest/) mints a guest and answers `{guest_token, user}`. It works only with `guests.enabled` on and answers 403 `forbidden` with reason `guests_disabled` otherwise.

The Bearer session routes answer CORS for any origin without credentials, so a page on your own origin can call them with the session token. Server routes, dashboard routes and the health route answer no CORS: call them from your backend.

### A session token for the playground

The [playground](/playground/) calls the session routes with a session token you paste under Authentication. It has no server routes, because those answer no CORS. Get a token one of two ways:

1. Mint one from your backend with the call above and copy `session_token`.
2. Sign a client token and exchange it:

   ```sh
   curl -X POST https://app.mondochat.io/v1/session -H 'content-type: application/json' \
     -d '{"app_id": "<app id>", "token": "<client token>"}'
   ```

The playground sends requests to `https://app.mondochat.io` from your browser, as a headless client would.

## Check it works

- The exchange answers 200 with a `session_token`, and `user.external_id` is the `sub` you signed.
- `GET /v1/me` with `Authorization: Bearer <session_token>` answers the user ([Get the signed-in user](/api/operations/get-me/)).
- A refused token answers 401 with the reason in `detail`, for example `signature` or `exp must be at most 600s after iat`.

## When it fails

| Answer | Cause |
|---|---|
| 401 `unauthorized` | The token is expired, its lifetime is over 600 seconds, `iss` is another app, the algorithm is not HS256, the secret is revoked, a guest token belongs to another app, or a `jti` was already used. |
| 401 `unauthorized`, `guests are disabled` | No token was sent and `guests.enabled` is off. |
| 403 `banned` | The user or the IP is banned, at exchange or at refresh. `reason` is `temporary` when the ban expires. |
| 403 `forbidden` | A test app already has 50 users, or the tenant is suspended. |
| 429 `rate_limited` | More than 300 exchanges from one IP in a minute. Wait `Retry-After` seconds. |

## Limits

- A client token lives at most 600 seconds, and one with a `jti` works once.
- A session token lasts 15 minutes and is refreshable until 12 hours after sign-in.
- A guest token lasts 400 days.
- The exchange is limited to 300 per IP per minute, because mobile carriers put many users behind one IP.
- A test tenant accepts at most 50 users per app.

## Related topics

- [Exchange a token for a session](/api/operations/exchange-session/)
- [Refresh a session](/api/operations/refresh-session/)
- [Create a session from your server](/api/operations/mint-server-session/)
- [Errors](/errors/)
- [Security](/guides/security/)
- [Users](/guides/users/)