Create direct conversations
Start a DIRECT Conversation between two users, but only when your own rules allow it. Chat knows permissions and membership, not who coaches whom. Your backend checks the rule first and then creates the conversation.
Prerequisites
Section titled “Prerequisites”conversations.createMachine scope (any of): chat:conversations.managechat:sync- A backend client with one of those scopes, and the
ccs-chat-clientlibrary installed. - Both users are active members of your tenant with access to your application. Otherwise Chat answers 422
target_user_not_eligible.
-
Create one client for the process. It fetches its machine token on first use, caches it and renews it a minute before it expires.
chat-client.ts import { ChatClient } from '@central-code-station/ccs-chat-client';// One client per process. It fetches and caches its machine token itself.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!,// Optional. Leave it out for chat:sync, or list only the granular scopes you need.scope: process.env.CHAT_SCOPE,}); -
Check your rule, then create. Pass both user ids and the user the conversation is created for as
actor_user_id.open-chat.ts import { ChatUnprocessableError } from '@central-code-station/ccs-chat-client';import { chat } from './guide-chat-client';// POST /fitness/chats { client_id }: called by the signed-in coach's app.export async function openCoachChat(coachId: string, clientId: string): Promise<string> {// Your rule first. Chat only checks permissions and membership, never who coaches whom.if (!(await isCoachOf(coachId, clientId))) throw new Error('forbidden');try {// DIRECT conversations dedupe on their two members, so this is safe to call every time.const conversation = await chat.conversations.create({type: 'DIRECT',member_user_ids: [coachId, clientId],actor_user_id: coachId,});return conversation.conversation_id;} catch (error) {if (error instanceof ChatUnprocessableError && error.code === 'target_user_not_eligible') {throw new Error('One of the two users cannot use this application yet');}throw error;}}// Or create the conversation when the relationship starts and keep its id.export async function startCoaching(coachId: string, clientId: string): Promise<void> {const conversationId = await openCoachChat(coachId, clientId);await saveConversationId(coachId, clientId, conversationId);}declare function isCoachOf(coachId: string, clientId: string): Promise<boolean>;declare function saveConversationId(coachId: string, clientId: string, conversationId: string): Promise<void>;Terminal window curl -s -X POST "$CHAT_API_URL/tenants/$CCS_TENANT_ID/applications/$CCS_APPLICATION_ID/conversations" \-H "Authorization: Bearer $CHAT_MACHINE_TOKEN" \-H "Content-Type: application/json" \-d '{"type":"DIRECT","member_user_ids":["coach-ana","client-john"],"actor_user_id":"coach-ana"}'{"conversation_id": "6b1f4c7e-3c52-4a0a-9a39-7d2e2f1f8a10","type": "DIRECT","status": "ACTIVE","member_count": 2} -
Return or store the id. Chat ids are opaque UUIDs. Either create the conversation when the relationship starts and store its
conversation_id, or create it on demand as above. Creating again is safe.
How creating behaves
Section titled “How creating behaves”- A DIRECT conversation has exactly two members. The
actor_user_idcounts as one of them. - It dedupes on its two members. Creating it again returns the existing conversation.
- Every member must be an eligible tenant member. One who is not fails the whole call.
- Your backend client needs a non-empty
member_user_ids.
Verify
Section titled “Verify”Call your route twice with the same pair. Both responses carry the same conversation_id. Open it with chat.conversations.get(id): type is DIRECT and member_count is 2. The coach and the client can now read it with their Chat tokens.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause and fix |
|---|---|
422 target_user_not_eligible |
A user is not an active member of the tenant, or has no access to the application. Fix it in the Identity Server. A newly added user becomes visible to Chat shortly after. |
403 insufficient_scope |
The client’s token has neither chat:conversations.manage nor chat:sync. See Grant Chat scopes. |
403 client_context_mismatch |
CCS_TENANT_ID or CCS_APPLICATION_ID is not the context of the client’s own application. |
400 invalid_request |
member_user_ids is empty, or the conversation would not have exactly two members. |