Integrated Chat, part 1: your backend
This quickstart is for the backend developer of a tenant application. Your Node.js backend applies its own rule about who may talk to whom, creates the conversation, posts a first message as the coach and archives the conversation later. Part 2 builds the web side: your web app.
What you will build
Section titled “What you will build”A function openCoachChat(coachId, clientId) in the Fitness app. It checks that the client belongs to the coach, creates a DIRECT conversation between coach-ana and client-john, and sends “Welcome! See you at 9.” as Ana. The result is a conversation id you store or return to the app.
Prerequisites
Section titled “Prerequisites”- Node.js 20 or later. The library uses the global
fetchand has no runtime dependencies. - The values from the checklist: the issuer URL, the backend client id and secret, your tenant id, your application id and the Chat API values.
- A GitHub token with
read:packages, to install the package (step 1).
conversations.createconversations.updatemessages.sendMachine scope (any of): chat:sync-
Install the library from GitHub Packages.
The package is private. It is published on GitHub Packages under the
@central-code-stationscope, not on the public npm registry. Add this to the.npmrcat the root of your project:@central-code-station:registry=https://npm.pkg.github.com//npm.pkg.github.com/:_authToken=${GITHUB_PACKAGES_TOKEN}Export
GITHUB_PACKAGES_TOKENas a GitHub personal access token (classic) with theread:packagesscope, from an account that has access to the package. In CI use the workflow’sGITHUB_TOKENwithpackages: read. Never commit a real token. Then install:Terminal window npm install @central-code-station/ccs-chat-clientThe package ships ES modules and CommonJS, so it works in NestJS, Express or a plain script. See SDKs.
-
Configure the client.
Set these environment variables. The library reads none itself; these are the suggested names, with the secret kept on the server.
Terminal window CCS_ISSUER_URL=https://auth.centralcodestation.comCCS_CHAT_SYNC_CLIENT_ID=<backend client id>CCS_CHAT_SYNC_CLIENT_SECRET=<backend client secret>CCS_TENANT_ID=<your tenant id>CCS_APPLICATION_ID=<your tenant application id>CHAT_API_URL=https://chat.centralcodestation.com/api/v1CHAT_API_RESOURCE=https://chat.centralcodestation.com/apiCreate one
ChatClientfor the process. It fetches its machine token on first use, caches it and renews it 60 seconds before it expires.chat-client.ts import { ChatClient } from '@central-code-station/ccs-chat-client';// One client per process: it fetches its machine token on first use and renews it before it expires.export const chat = new ChatClient({issuerUrl: process.env.CCS_ISSUER_URL!,clientId: process.env.CCS_CHAT_SYNC_CLIENT_ID!,clientSecret: process.env.CCS_CHAT_SYNC_CLIENT_SECRET!,chatApiUrl: process.env.CHAT_API_URL!,resource: process.env.CHAT_API_RESOURCE!,tenantId: process.env.CCS_TENANT_ID!,applicationId: process.env.CCS_APPLICATION_ID!,});resourcetakes the resource identifier, never the audiencechat-api. The client works only in its own tenant and application. Any other context answersclient_context_mismatch. -
Apply your rule, then create the conversation and send the first message.
Check your own rule first. Chat does not know who coaches whom. Then create the
DIRECTconversation.actor_user_idrecords Ana as the creator. Send the first message on her behalf.open-coach-chat.ts import { ChatUnprocessableError } from '@central-code-station/ccs-chat-client';import { chat } from './quickstart-chat-client';// Your own business rule, answered from your own database.declare function isCoachOf(coachId: string, clientId: string): Promise<boolean>;export async function openCoachChat(coachId: string, clientId: string): Promise<string> {if (!(await isCoachOf(coachId, clientId))) throw new Error('forbidden');// DIRECT conversations dedupe on their two members, so calling this twice returns the same one.const conversation = await chat.conversations.create({type: 'DIRECT',member_user_ids: [coachId, clientId],actor_user_id: coachId,});// Stored with coach-ana as the sender and your backend client as sender_client_id.await chat.messages.send(conversation.conversation_id, { body: 'Welcome! See you at 9.', actor_user_id: coachId });return conversation.conversation_id;}export async function endCoaching(conversationId: string): Promise<void> {// The conversation stays readable; nobody can post any more.await chat.conversations.archive(conversationId);}export async function postIfActive(conversationId: string, coachId: string, body: string): Promise<void> {try {await chat.messages.send(conversationId, { body, actor_user_id: coachId });} catch (error) {if (error instanceof ChatUnprocessableError && error.code === 'conversation_archived') return;throw error;}}Both users must be active members of the tenant with access to your application. Otherwise Chat answers 422
target_user_not_eligible. ADIRECTconversation has exactly two members. The sender must also be a member, or you getactor_not_member. -
Archive the conversation when the relationship ends.
endCoachingin the sample callschat.conversations.archive(conversationId). Users keep reading, and nobody can post. A later post answers 422conversation_archived, whichpostIfActivein the sample handles. Callchat.conversations.reactivate(conversationId)to reopen it.
Verify
Section titled “Verify”Check the result with curl. First get a machine token the way the library does, then list the conversations of your application.
CHAT_TOKEN=$(curl -s -X POST "$CCS_ISSUER_URL/oauth/token" \ -u "$CCS_CHAT_SYNC_CLIENT_ID:$CCS_CHAT_SYNC_CLIENT_SECRET" \ --data-urlencode "grant_type=client_credentials" \ --data-urlencode "scope=chat:sync" \ --data-urlencode "resource=$CHAT_API_RESOURCE" | jq -r .access_token)
curl -s "$CHAT_API_URL/tenants/$CCS_TENANT_ID/applications/$CCS_APPLICATION_ID/conversations" \ -H "Authorization: Bearer $CHAT_TOKEN"You get a page with your conversation, trimmed here. After step 4 the status is ARCHIVED.
{ "items": [ { "conversation_id": "6b1f4c7e-3c52-4a0a-9a39-7d2e2f1f8a10", "tenant_id": "fitness", "application_id": "fitness-app", "type": "DIRECT", "status": "ACTIVE", "members": [ { "user_id": "coach-ana", "display_name": "Ana Coach", "avatar_url": null }, { "user_id": "client-john", "display_name": "John Client", "avatar_url": null } ], "member_count": 2 } ], "next_cursor": null}Real ids are UUIDs or the ids your Identity Server issues. The samples use readable placeholders.
Troubleshooting
Section titled “Troubleshooting”| You see | Likely cause |
|---|---|
401 invalid_token |
The token has the wrong audience or lacks the scope. Check that resource is the identifier and that the client holds chat:sync. |
403 client_context_mismatch |
CCS_TENANT_ID or CCS_APPLICATION_ID is not the application of the client. |
403 insufficient_scope |
The token carries none of the Chat scopes. Check the client’s Resource Permissions. |
403 missing_capability |
The client’s scopes do not cover the call. Grant chat:sync or the matching granular scope. |
422 target_user_not_eligible |
A user is not an active tenant member with access to the application. |
429 rate_limited |
The limit is 600 requests per 60 s per client. Wait the Retry-After seconds. |
The full list is in Troubleshooting and the error catalog.
Next steps
Section titled “Next steps”- Integrated Chat, part 2: your web app: the token exchange route and the user’s side.
- Create direct conversations, groups and channels, and send on behalf and SYSTEM messages.
- Handle errors and retries.
- The API behind the calls: create a conversation, send a message and the
ccs-chat-clientreference.