Quickstart
Your server proves who the user is by signing a short-lived token with the app’s server secret. The browser only ever sees that token, never the secret.
Before you start
Section titled “Before you start”- A mondochat account. Staff create it on request: tell them your company name and the email of the person who will own it. That email gets a notice titled “You have access to your company on mondochat”, and the owner signs in at
https://dash.mondochat.iowith a link sent to the same email, whenever they like. See Tenants and apps. - A server that can sign an HS256 token for the user who is logged in, in Node, PHP, Python or any language with a JWT library.
- A page of your site served over https from an origin you can list in the dashboard.
New accounts start in test mode, with up to 50 users per app. Ask staff to switch yours to live when you are ready.
-
Get the app id and a server secret. Open Secrets in the dashboard and create one. It starts with
mcs_, is shown once and belongs on your server only. The app id is in the dashboard URL,/apps/<app id>/..., and on the same page. You send it asMondochat-App-Id. -
Allow your site’s origin. Open Origins and add every origin that shows the chat, for example
https://www.example.comorhttps://*.example.com. The chat loads only inside pages from listed origins. Every origin ishttps://host[:port]: anhttp://origin,http://localhostincluded, answers 422 when you save. The sample servers below listen on plain http (port 8081), so put TLS in front of them and list the https address you open. For a local try, make a self-signed certificate and a small proxy:Terminal window openssl req -x509 -newkey rsa:2048 -nodes -days 30 -keyout key.pem -out cert.pem -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"Save this as
tls-proxy.mjsnext to the two files and runnode tls-proxy.mjswhile the sample server runs:import { readFileSync } from 'node:fs'import { request } from 'node:http'import { createServer } from 'node:https'createServer({ key: readFileSync('key.pem'), cert: readFileSync('cert.pem') }, (req, res) => {const up = request({ port: 8081, path: req.url, method: req.method, headers: req.headers }, (r) => {res.writeHead(r.statusCode, r.headers)r.pipe(res)})req.pipe(up)}).listen(8443)List
https://localhost:8443as the origin, open that address, and accept the certificate warning once: the browser must trust the certificate before the page and the chat load. With mkcert installed,mkcert localhost 127.0.0.1makes a trusted pair instead (use itslocalhost+1.pemandlocalhost+1-key.pemfiles). -
Optional: set your own roles and policy. Open App in the dashboard. The defaults have one role,
member, and members may message each other but not call, which is enough for this quickstart. For fans and models, or any other roles, see Settings overview. Omitted keys keep their defaults. -
Add a token endpoint to your server. The chat calls
getTokenwhenever it needs to sign in, on load and again after a session ends. Your endpoint answers with a token for the logged-in user, or an empty body when nobody is logged in.The token is an HS256 JWT signed with the server secret:
Claim Required Value issyes your app id subyes the user’s id on your site, 1 to 128 chars of A-Z a-z 0-9 . _ : @ -; do not start it withguest:nameyes display name iat,expyes seconds; expat most 600 afteriat(30s clock skew allowed)roleno a role key from your settings; default default_roleavatarno https URL langno it,en,esorfrpermsno per-user overrides of policy flags and limits, for example {"call": false}peerno the external id this user was sent to talk to; counts as a vouch, see Embed jtino when present, the token is single use Each example below is a whole small site: the token endpoint, the page with the embed snippet, and a stand-in login (
/login?as=alice) that you replace with your own sessions. Any JWT library works too, as long as the algorithm is HS256.Save it as
server.mjs. It has no dependencies.import { createHmac, randomUUID } from 'node:crypto'import { createServer } from 'node:http'const APP_ID = process.env.MONDOCHAT_APP_IDconst SECRET = process.env.MONDOCHAT_SERVER_SECRETconst EMBED = process.env.MONDOCHAT_EMBED ?? 'https://app.mondochat.io/embed.js'const sessions = new Map()const b64url = (s) => Buffer.from(s).toString('base64url')function mondochatToken(user) {const iat = Math.floor(Date.now() / 1000)const claims = { iss: APP_ID, sub: user.id, name: user.name, iat, exp: iat + 300 }const body = `${b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))}.${b64url(JSON.stringify(claims))}`return `${body}.${createHmac('sha256', SECRET).update(body).digest('base64url')}`}const page = `<!doctype html><meta charset="utf-8"><title>My site</title><form onsubmit="MondoChat.open({ with: this.peer.value }); return false"><input name="peer" placeholder="user id"> <button>Chat</button></form><script>window.MondoChat = window.MondoChat || function () { (MondoChat.q = MondoChat.q || []).push(arguments) };MondoChat('init', { appId: '${APP_ID}', getToken: async () => (await fetch('/mondochat-token')).text() || null });</script><script async src="${EMBED}"></script>`createServer((req, res) => {const url = new URL(req.url, 'http://localhost')const user = sessions.get(/(?:^|; )sid=([\w-]+)/.exec(req.headers.cookie ?? '')?.[1])if (url.pathname === '/login') {const id = (url.searchParams.get('as') ?? '').toLowerCase().replace(/[^a-z0-9]/g, '')const sid = randomUUID()if (id) sessions.set(sid, { id, name: id[0].toUpperCase() + id.slice(1) })res.writeHead(303, { location: '/', 'set-cookie': `sid=${sid}; Path=/; HttpOnly; SameSite=Lax` }).end()} else if (url.pathname === '/mondochat-token') {res.writeHead(200, { 'content-type': 'text/plain', 'cache-control': 'no-store' })res.end(user ? mondochatToken(user) : '')} else {res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }).end(page)}}).listen(8081, '0.0.0.0')Save it as
index.php. Serve it with PHP’s built-in server as the router script, on port 8081:php -S 0.0.0.0:8081 index.php.<?php$appId = getenv('MONDOCHAT_APP_ID');$secret = getenv('MONDOCHAT_SERVER_SECRET');$embed = getenv('MONDOCHAT_EMBED') ?: 'https://app.mondochat.io/embed.js';session_start();function b64url(string $s): string{return rtrim(strtr(base64_encode($s), '+/', '-_'), '=');}function mondochat_token(array $user, string $appId, string $secret): string{$iat = time();$claims = ['iss' => $appId, 'sub' => $user['id'], 'name' => $user['name'], 'iat' => $iat, 'exp' => $iat + 300];$body = b64url(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])) . '.' . b64url(json_encode($claims));return $body . '.' . b64url(hash_hmac('sha256', $body, $secret, true));}$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);if ($path === '/login') {$id = preg_replace('/[^a-z0-9]/', '', strtolower($_GET['as'] ?? ''));$_SESSION['user'] = $id === '' ? null : ['id' => $id, 'name' => ucfirst($id)];header('Location: /', true, 303);exit;}if ($path === '/mondochat-token') {header('Content-Type: text/plain');header('Cache-Control: no-store');$user = $_SESSION['user'] ?? null;echo $user ? mondochat_token($user, $appId, $secret) : '';exit;}?><!doctype html><meta charset="utf-8"><title>My site</title><form onsubmit="MondoChat.open({ with: this.peer.value }); return false"><input name="peer" placeholder="user id"> <button>Chat</button></form><script>window.MondoChat = window.MondoChat || function () { (MondoChat.q = MondoChat.q || []).push(arguments) };MondoChat('init', { appId: <?= json_encode($appId) ?>, getToken: async () => (await fetch('/mondochat-token')).text() || null });</script><script async src="<?= htmlspecialchars($embed) ?>"></script>Save it as
server.py. It uses the standard library only.import base64import hashlibimport hmacimport jsonimport osimport reimport secretsimport timefrom http.cookies import SimpleCookiefrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServerfrom urllib.parse import parse_qs, urlsplitAPP_ID = os.environ["MONDOCHAT_APP_ID"]SECRET = os.environ["MONDOCHAT_SERVER_SECRET"].encode()EMBED = os.environ.get("MONDOCHAT_EMBED", "https://app.mondochat.io/embed.js")sessions: dict[str, dict] = {}def b64url(b: bytes) -> str:return base64.urlsafe_b64encode(b).rstrip(b"=").decode()def mondochat_token(user: dict) -> str:iat = int(time.time())claims = {"iss": APP_ID, "sub": user["id"], "name": user["name"], "iat": iat, "exp": iat + 300}body = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}).encode()) + "." + b64url(json.dumps(claims).encode())return body + "." + b64url(hmac.new(SECRET, body.encode(), hashlib.sha256).digest())PAGE = f"""<!doctype html><meta charset="utf-8"><title>My site</title><form onsubmit="MondoChat.open({{ with: this.peer.value }}); return false"><input name="peer" placeholder="user id"> <button>Chat</button></form><script>window.MondoChat = window.MondoChat || function () {{ (MondoChat.q = MondoChat.q || []).push(arguments) }};MondoChat('init', {{ appId: {json.dumps(APP_ID)}, getToken: async () => (await fetch('/mondochat-token')).text() || null }});</script><script async src="{EMBED}"></script>"""class Handler(BaseHTTPRequestHandler):def send(self, code: int, body: str = "", **headers: str) -> None:self.send_response(code)for k, v in headers.items():self.send_header(k.replace("_", "-"), v)self.end_headers()self.wfile.write(body.encode())def do_GET(self) -> None:url = urlsplit(self.path)sid = SimpleCookie(self.headers.get("Cookie")).get("sid")user = sessions.get(sid.value) if sid else Noneif url.path == "/login":uid = re.sub(r"[^a-z0-9]", "", parse_qs(url.query).get("as", [""])[0].lower())new = secrets.token_urlsafe()if uid:sessions[new] = {"id": uid, "name": uid.capitalize()}self.send(303, Location="/", Set_Cookie=f"sid={new}; Path=/; HttpOnly; SameSite=Lax")elif url.path == "/mondochat-token":token = mondochat_token(user) if user else ""self.send(200, token, Content_Type="text/plain", Cache_Control="no-store")else:self.send(200, PAGE, Content_Type="text/html; charset=utf-8")ThreadingHTTPServer(("0.0.0.0", 8081), Handler).serve_forever() -
Run your site. Set
MONDOCHAT_APP_IDandMONDOCHAT_SERVER_SECRETin its environment and serve it over https on an origin you listed in step 2. The examples read the script address fromMONDOCHAT_EMBEDand default tohttps://app.mondochat.io/embed.js. -
Add the embed snippet to every page that shows the chat. The examples above already hold it:
<script>window.MondoChat = window.MondoChat || function () { (MondoChat.q = MondoChat.q || []).push(arguments) };MondoChat('init', { appId: 'APP_ID', getToken: async () => (await fetch('/mondochat-token')).text() || null });</script><script async src="https://app.mondochat.io/embed.js"></script>A launcher bubble appears in the corner once
getTokenreturns a token. CallMondoChat('logout')when the user logs out of your site. Every option, method and event is on Embed. -
Open a chat between two users. Open
https://localhost:8443/login?as=alicein one browser profile andhttps://localhost:8443/login?as=bobin another, or in a private window. The sample session cookie is shared within a profile, so two normal windows of one profile stay the same user. As Alice, typeboband press Chat. The button callsMondoChat('open', { with: 'bob' }), wherewithis the other user’s id on your site.A user must exist before others can open a conversation with them. A user exists after their first session, or you create them from your server right away:
Terminal window curl -X PUT https://app.mondochat.io/v1/server/users/bob \-H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID" \-H 'content-type: application/json' -d '{"name": "Bob", "role": "member"}'MondoChat('open')withoutwithopens the inbox. The request is Upsert a user.
Check it works
Section titled “Check it works”-
Alice sends a message and Bob sees it arrive.
-
To check a token by hand, exchange it. A refused token answers 401 with the reason in
detail:Terminal window curl -X POST https://app.mondochat.io/v1/session -H 'content-type: application/json' \-d '{"app_id": "<app id>", "token": "<token>"}' -
The Test console in the dashboard opens the chat as any user and role without a token endpoint. Two browser windows with two users are the quickest way to try messages and calls.
When it fails
Section titled “When it fails”| Symptom | Cause |
|---|---|
Nothing appears, the console shows a frame-ancestors error |
The page is not on a listed origin. |
POST /v1/session answers 401 with detail |
The token was refused. detail says why: signature, exp must be at most 600s after iat, bad header and so on. Check that iss is the app id and that the secret is active. |
401 guests are disabled |
getToken returned nothing and guests.enabled is off: nobody is logged in. |
403 test_user_limit |
Tenants in test status accept 50 users per app. |
Opening a chat with bob fails, POST /v1/conversations answers 404 |
Bob has never signed in and was not created with PUT /v1/server/users/bob. |
Limits
Section titled “Limits”- A client token lives at most 600 seconds.
- An app has at most 2 active server secrets and 20 origins, and the origins together take at most 1024 bytes.
- A tenant in test status accepts at most 50 users per app.
- The session exchange is limited to 300 per IP per minute.