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
Section titled “Before you start”- 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 | |--------------------------------------------->|- The page asks your token endpoint for a token.
getTokenreturns it, ornullwhen nobody is logged in. - Your server signs a client token for the logged-in user with the server secret. Keep
expshort and answer withCache-Control: no-store. - The chat posts
{app_id, token}toPOST /v1/session. The route is public: the token is the proof. - 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}.appis the frame configuration for this viewer andrealtimeholds the socket host and channel. - The session token is an ES256 token that lasts 15 minutes. User calls send it as
Authorization: Bearer <session_token>. POST /v1/session/refreshissues 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 callsgetTokenand 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
Section titled “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.revokedwith reasonmerged.
A merge twice changes nothing. Your server also gets user.merged.
Server sessions for headless clients
Section titled “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:
Authorization: Bearer <mcs_ server secret>Mondochat-App-Id: <app id>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"}'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 } = await res.json()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.
A session token for the playground
Section titled “A session token for the playground”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:
-
Mint one from your backend with the call above and copy
session_token. -
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.
Check it works
Section titled “Check it works”- The exchange answers 200 with a
session_token, anduser.external_idis thesubyou signed. GET /v1/mewithAuthorization: Bearer <session_token>answers the user (Get the signed-in user).- A refused token answers 401 with the reason in
detail, for examplesignatureorexp must be at most 600s after iat.
When it fails
Section titled “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
Section titled “Limits”- A client token lives at most 600 seconds, and one with a
jtiworks 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.