Signing visitors in
A visitor who is signed in on your site should not have to sign up again in the messenger. Your backend vouches for them with a user token: a short JWT, signed with your private key, that says who the user is (contracts/user-token.md). The platform checks it with the public key you uploaded and starts a session for that user on your site.
The same kind of token signs a profile owner in to the inbox on your owner dashboard. Its audience differs, and so does the server that signs it: the dashboard's own (guide 5).
How it fits together
- The messenger on your page asks who is signed in.
- Your page asks your own backend:
POST /messenger/token, on your own origin, with the visitor's session cookie. - Your backend finds the user its session names, signs a token for them, and answers with the token as text. Nobody signed in: it answers 204 with no body, and the visitor writes as a guest.
- Your page hands the token to the messenger, which sends it to the platform.
- The platform checks the token and starts the visitor's session. If a check fails, the visitor goes on as a guest, and the partner console's Integration health card shows why.
The token
Before you start, make a key pair and upload its public half in the partner console (guide 1). The console gives the key a kid.
A user token is a signed JWT. Decoded, the header and claims of Marta's token, as the example signs it, read:
{ "alg": "EdDSA", "kid": "tk_...", "typ": "JWT" }
{
"iss": "pk_test_...",
"aud": "messenger-widget",
"sub": "user-1042",
"iat": 1792455862,
"exp": 1792455982,
"jti": "9f2c4e8a1b7d4c3e8f0a6b5d2c1e7f90",
"name": "Marta"
}
algisEdDSAfor an Ed25519 key andES256for a P-256 key, andkidis the id the console showed.issis your site's publishable key.audismessenger-widgetfor a visitor, andmessenger-inboxfor a profile owner in the inbox.subis your own stable ID for the user, at most 128 characters. Never an email address, a phone number or a name.expis at most 5 minutes afteriat. The example gives each token 2 minutes, since the page uses it at once.jtiis random, 16 to 64 characters, and new for every token. The platform accepts each token once.nameis optional, at most 60 characters: the name the profile owner sees for this visitor.
The token is the full rule. Put nothing else personal in a token: it travels through the visitor's browser.
Sign widget tokens with messenger-widget. The older audience messenger still works for now, but the platform refuses it everywhere from the day the first site goes live (Audience).
Signing on your backend
user-token.ts signs a token with Node's own crypto:
import { readSigningKey, signUserToken } from "./user-token.ts";
// Once, at start-up: the private key from its file outside your project, and the kid the console showed.
const keyFile = process.env.MESSENGER_TOKEN_KEY_FILE;
const kid = process.env.MESSENGER_TOKEN_KID;
if (!keyFile || !kid) throw new Error("Set MESSENGER_TOKEN_KEY_FILE and MESSENGER_TOKEN_KID.");
const key = readSigningKey(keyFile, kid);
// For each request, for the user your session names:
const token = signUserToken(key, {
publishableKey: "pk_test_...",
audience: "messenger-widget",
subject: user.id,
name: user.firstName,
});
Any JWT library that supports EdDSA or ES256 does the same. With ES256, the signature must be the raw 64 bytes of r and s, as JWTs use, not DER.
If your site runs OAuth or OpenID Connect
Your site's own sign-in can be anything: passwords, a social login, or your own OAuth 2.0 or OpenID Connect server. The messenger never sees it. It trusts the user token your backend signs once your sign-in has done its work. An OpenID Connect server already does much of this, with these differences:
| In your OpenID Connect server | In a user token |
|---|---|
| A signing key, often RS256 | Ed25519 (EdDSA) or P-256 (ES256) only. Add such a key to your server's key set, or keep one just for the platform, and upload its public half |
iss: your issuer's address |
iss: your site's publishable key |
aud: your client's id |
aud: messenger-widget for a visitor, messenger-inbox for a profile owner |
sub |
The same stable id works, unless it is an email address, a phone number or a name. For a profile owner, it must be the owner your profile sync sends |
| An ID token that lasts an hour or so | At most 5 minutes, with a new jti every time |
Claims such as email or picture |
Only an optional name |
So an ID token from your server cannot go to the messenger as it is. Sign a user token on your backend for the user your session names, as above; your sessions, refresh tokens and consent screens stay as they are. The messenger does not send visitors through your server's sign-in pages either: the visitor is already signed in on your page, and the token says so without a redirect or a second consent.
Signing in to your site with an account on the platform, the other way round, is not available yet (Coming later).
Your token route
Your page needs a route on your own server that answers your own pages with a fresh token as plain text. Signing in gives its rules: POST only, no CORS headers, and a session cookie with SameSite=Lax or SameSite=Strict. token-server.ts is a complete one you can run. It signs only for the user the session names, signs a new token for every request, sends it with Cache-Control: no-store, and never logs it or puts it in an address.
When nobody is signed in, the example answers 204 with no body. In the page script below, response.text() then gives an empty string, and the messenger takes anything but a non-empty string as a guest (Signing in). An error status such as 401 would work too, but the browser would log a failed request for every guest.
The example also checks the Sec-Fetch-Site header, which browsers send with each request. Anything but same-origin, such as cross-site from a page on another site, gets 403 before the session is read. A request without the header, as curl sends it, is not from a browser and goes on to the session check. The check adds to the rules above and replaces none of them.
Handing the token to the messenger
With the loader
Give createWidget a getUserToken function. It returns a fresh token for the signed-in visitor, or undefined for a guest (Signing in):
const widget = MessengerWidget.createWidget({
appUrl: "https://chat.example",
site: "pk_test_...",
getUserToken: async () => {
const response = await fetch("/messenger/token", { method: "POST" });
return response.ok ? response.text() : undefined;
},
});
// After the visitor signs in or out without loading a new page:
widget.userChanged();
A page that loads after a sign-in or sign-out needs nothing more: the messenger asks again as it starts. When else it asks, and what a slow answer does, is in Ordering, retries and idempotency.
With the mp-messenger element
Set the same function as the element's getUserToken property. Setting it again after a sign-in or sign-out tells the element to ask again (Attributes and properties):
const messenger = document.querySelector("mp-messenger");
messenger.getUserToken = async () => {
const response = await fetch("/messenger/token", { method: "POST" });
return response.ok ? response.text() : undefined;
};
Signing out
When the visitor signs out of your site, tell the messenger: call widget.userChanged(), or set getUserToken again on the element, and let it return undefined. The messenger ends that visitor's session in this browser at once, so on a shared computer the next person never sees their chats. If your page forgets, the session still ends within an hour (Signing out).
Checking it works
Sign in on your sandbox site as a test user, open a profile page and press its "Message" button. Above the first message, a signed-in visitor sees:
Hello, Marta.
You are signed in on SpaGuide, so this chat is kept with your account. No sign-up needed.
The greeting appears only when your token carries name, here Marta. SpaGuide stands for your site's name. The panel's Settings say "You are signed in on SpaGuide as Marta." Once the visitor has written, the list of chats says "These chats are kept with your SpaGuide account."
A guest sees "Write your first message below. You don't need an account." in a chat with no messages yet. After their first message, the list of chats says "You are a guest, so these chats last until you close your browser."
Then sign out on your page and call widget.userChanged(), or set getUserToken again on the element. The panel starts again as a new guest, with none of the chats the signed-in visitor had. If a visitor who is signed in on your site sees what a guest sees, the token failed: the next section says where to look.
When a token fails
The visitor carries on as a guest, and the Integration health card on the Integration page counts the failure by reason. The usual causes:
| Reason in the console | What to check |
|---|---|
| Signed with a key this site does not have | The kid header matches a key uploaded for this site |
| The signature does not match the key | You sign with the private half of that key; for ES256, the raw signature format |
| Made for another site | iss is this site's publishable key |
| Made for another use | aud is messenger-widget in the messenger, messenger-inbox in the inbox |
| Expired before it arrived, or dated in the future | Your server's clock; the platform allows 60 seconds of difference |
| Valid for longer than 5 minutes | exp is at most 5 minutes after iat |
| Used twice | Every token gets its own jti, and your route signs a new one for every request |
If a key leaks
A leaked private key lets someone sign in as any of your users on your site, and on no other site. Remove the key in the console at once, upload a new one, and sign with it (Security).
Next: the owner inbox in your dashboard, where profile owners answer their visitors.