Skip to content

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.

Chat permission: conversations.createMachine scope (any of): chat:conversations.managechat:sync
  1. 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,
    });
  2. 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>;
  3. 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.

  • A DIRECT conversation has exactly two members. The actor_user_id counts 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.

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.

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.