Skip to content

Get the user's Chat token

Your web app needs a user token that the Chat API accepts. This guide shows the getAccessToken function that fetches one from your backend and keeps it until shortly before it expires.

Every Chat library calls getAccessToken before each request and each (re)connect. You return a token whose audience is the Chat API and whose scope is chat:access. A token you hold for your own API is rejected with 401. Your backend swaps one for the other with token exchange; your app only calls your backend.

  • A backend route that returns { access_token, expires_in }. See Issue Chat tokens to your users.
  • A signed-in user with a valid token for your own API, which you send to that route.
  • Your web app’s origin in allowed_origins on the Identity Server client. See Register web origins.

The token lives at most 10 minutes and there is no refresh token. Cache it and renew it one minute before it expires. Share one in-flight request, because the libraries can ask for a token several times at once.

chat-token.ts
declare function getFitnessAccessToken(): Promise<string>;
interface ChatTokenResponse {
access_token: string;
expires_in: number; // seconds
}
let cached: { token: string; expiresAt: number } | null = null;
let inFlight: Promise<string> | null = null;
/** The `getAccessToken` for every Chat library: cached, renewed one minute before it expires. */
export function getChatAccessToken(): Promise<string> {
if (cached && cached.expiresAt - 60_000 > Date.now()) return Promise.resolve(cached.token);
inFlight ??= fetchChatToken().finally(() => {
inFlight = null; // concurrent callers share one request
});
return inFlight;
}
async function fetchChatToken(): Promise<string> {
const response = await fetch('/api/chat-token', {
headers: { Authorization: `Bearer ${await getFitnessAccessToken()}` },
});
if (!response.ok) throw new Error(`Chat token unavailable (${response.status})`);
const { access_token, expires_in } = (await response.json()) as ChatTokenResponse;
cached = { token: access_token, expiresAt: Date.now() + expires_in * 1000 };
return access_token;
}

getFitnessAccessToken is your own sign-in code. It returns the user’s token for your API.

Give the same function to ChatUserClient, ChatNotificationsClient and ChatPushClient.

chat.ts
import { ChatUserClient } from '@central-code-station/ccs-chat-user-client';
import { getChatAccessToken } from './fe-guide-chat-token';
// Public values: the same ones you keep in environment.ts.
export const context = {
chatApiUrl: 'https://chat.centralcodestation.com/api/v1',
tenantId: 'fitness',
applicationId: 'fitness-app',
getAccessToken: getChatAccessToken,
};
export const chat = new ChatUserClient(context);

Put the Chat API URL, tenant id and application id in your app configuration. They are public values. The token is bound to your application, so the tenantId and applicationId must be the ones your backend client belongs to.

If getAccessToken throws, the call that needed it rejects with your error. The notifications client treats it as a failed connection and retries. If it returns nothing, ChatUserClient throws a ChatUnauthorizedError with the code no_access_token. Let your app retry, or ask the user to sign in again.

Call chat.capabilities() after you sign in. It returns the user’s roles and Chat permissions. If it does, the token, the audience and the context are correct.

You see Cause Fix
401 invalid_token Wrong audience, expired token, or no chat:access scope. Send the exchanged token, not your own API’s token. See Troubleshooting.
403 client_context_mismatch The token was issued to a client of another application than the one in the URL. Use the tenant and application of your backend client.
Your token route answers 401 The user’s own token expired, or token exchange failed. Renew the user’s sign-in, then retry.