Skip to content

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.

  • An app with an active server secret (mcs_...) and your site’s origin listed. See 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.

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

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

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.

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:

Authorization: Bearer <mcs_ server secret>
Mondochat-App-Id: <app id>
Terminal window
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"}'

POST /v1/server/sessions takes the same fields as PUT /v1/server/users/{external_id} plus external_id: name is required, and avatar, role, lang, perms and tier are optional. It answers {session_token, expires_at, user}. 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.

POST /v1/server/guests 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.

The 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:

    Terminal window
    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.

  • 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).
  • A refused token answers 401 with the reason in detail, for example signature or exp must be at most 600s after iat.
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.
  • 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.