Sign your users in to the support chat.
Your backend vouches for the user it has already signed in, and Revvly hands back a short-lived token. The browser or app uses that token to chat with your support team as that user, and as nobody else.
#How it works
#Create a support secret key
- Org Settings
- Support
- Secret keys
- Create secret key
Admins and owners only. The key is shown once: copy it into your server's secret store.
A support secret key (rvly_sk_live_…) can do one thing: sign your users in to the support chat. It can read your data through the public API but cannot change anything, so it is not the key your billing integration uses, and that key cannot mint support tokens. Keep it on your server only, never in a browser, a mobile app or a repository.
#Mint a token
Call createEndUserToken on the public GraphQL API with the secret key as the Bearer token. Mint a token when the user opens the chat, not on every page load.
Mutation and variablesmutation CreateEndUserToken($input: EndUserClaimsInput!) {
createEndUserToken(input: $input) {
token
expiresAt
}
}
{
"input": {
"externalUserId": "user_8412",
"name": "Ana Silva",
"email": "[email protected]",
"role": "founder",
"plan": "growth",
"organizationExternalId": "acct_123",
"attributes": [
{ "key": "company_stage", "value": "seed" },
{ "key": "cohort", "value": "Spring 2026" }
]
}
}With curlcurl -s -X POST https://api.revvly.ai/api/v1/graphql \
-H "Authorization: Bearer $REVVLY_SUPPORT_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation($input: EndUserClaimsInput!) { createEndUserToken(input: $input) { token expiresAt } }",
"variables": { "input": { "externalUserId": "user_8412", "name": "Ana Silva", "email": "[email protected]" } }
}'Response{
"data": {
"createEndUserToken": {
"token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"expiresAt": "2026-10-05T11:42:07.000Z"
}
}
}Claims you send
#Give the token to the browser
Add one route to your backend that your signed-in pages call. It looks up the user from your own session, mints the token and returns it. A minimal Express version:
- User opens the chatYour page asks your backend for a token.
- More than a minute leftYour server returns the cached token.orNo token, or about to expireYour server mints one with the secret key.
- Chat API callsThe token is kept in memory only.
- UNAUTHENTICATEDSkip your cache, mint a new token and retry once.
// Your backend: one route your signed-in pages call to get a support token.
// The secret key stays here; the browser only ever sees the token.
app.post('/api/support-token', requireSignedInUser, async (req, res) => {
const user = req.user; // whoever your own session says is signed in
const response = await fetch('https://api.revvly.ai/api/v1/graphql', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.REVVLY_SUPPORT_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `mutation($input: EndUserClaimsInput!) {
createEndUserToken(input: $input) { token expiresAt }
}`,
variables: {
input: {
externalUserId: user.id,
name: user.name,
email: user.email,
role: user.role, // your own value; your Revvly role rules decide what it grants
organizationExternalId: user.accountId,
},
},
}),
});
const { data, errors } = await response.json();
if (errors) return res.status(502).json({ error: errors[0].extensions?.code });
res.json(data.createEndUserToken); // { token, expiresAt }
});#Sync your users in bulk (optional)
Each token you mint also updates what Revvly knows about that user. To have everyone in Support, End users before they first open the chat, or to update attributes without waiting for the next sign-in, send up to 500 users per call to upsertSupportUsers with the same secret key. It takes the same fields as the token request, without ttlSeconds, matches users by externalUserId, and never mints a token.
mutation UpsertSupportUsers($users: [SupportUserInput!]!) {
upsertSupportUsers(users: $users) {
created
updated
failed
results { index externalUserId status code message organizationLinked contactLinked }
}
}
{
"users": [
{
"externalUserId": "user_8412",
"name": "Ana Silva",
"email": "[email protected]",
"role": "founder",
"organizationExternalId": "acct_123",
"attributes": [{ "key": "cohort", "value": "Spring 2026" }]
},
{ "externalUserId": "user_9001", "name": "Ben Ode", "role": "investor" },
{ "externalUserId": "", "name": "No id" }
]
}Response{
"data": {
"upsertSupportUsers": {
"created": 2,
"updated": 0,
"failed": 1,
"results": [
{ "index": 0, "externalUserId": "user_8412", "status": "CREATED", "code": null,
"message": null, "organizationLinked": true, "contactLinked": true },
{ "index": 1, "externalUserId": "user_9001", "status": "CREATED", "code": null,
"message": null, "organizationLinked": null, "contactLinked": false },
{ "index": 2, "externalUserId": "", "status": "FAILED", "code": "BAD_REQUEST",
"message": "externalUserId is required",
"organizationLinked": null, "contactLinked": null }
]
}
}
}#Revoke and rotate
Revvly checks the minting key on every chat request, not only at mint. Revoke a secret key (Org Settings, Support, Secret keys) and every token it minted stops working on the next request, without waiting for them to expire. Rotating a key gives you a new one and keeps the old one working for its grace period, so you can deploy the new secret before the old one stops.
#Errors
#Verify a token on your side (optional)
You only need this if your own services want to accept the same token. Tokens are RS256 JWTs; the public keys are published at https://api.revvly.ai/api/v1/support/jwks.json (cacheable for an hour, the header’s kid names the key).
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://api.revvly.ai/api/v1/support/jwks.json'));
export async function verifySupportToken(token) {
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ['RS256'],
issuer: 'revvly-support',
audience: process.env.REVVLY_TENANT_ID, // required: see below
});
return { userId: payload.sub, role: payload.role ?? null };
}{
"iss": "revvly-support",
"aud": "<your tenant id>",
"sub": "user_8412",
"name": "Ana Silva",
"email": "[email protected]",
"role": "founder",
"plan": "growth",
"org": "acct_123",
"jti": "5b0c…",
"mk": "<id of the secret key that minted it>",
"iat": 1791283327,
"exp": 1791286927
}#Try it in the sandbox
- Support
- Inbox
- Sandbox
Needs no secret key and touches no real customer.
The sandbox is a practice copy of your Support setup. Pick a practice customer (a name, a role value, attributes, a plan, a company), chat as them through the same API your widget will use, and answer from the inbox next to it. Nothing reaches your real inbox, client pages, metrics or email, so you can watch the unread count, notifications and replies behave as a customer would see them before you write any code.