Skip to content

Use the REST API from any stack

This page is for backend developers on Java, .NET, Python, PHP, Go or any stack without the TypeScript libraries, and for native mobile developers. You get the two token types with curl, make three REST calls and generate a typed client from the OpenAPI document.

Your backend creates a DIRECT conversation between coach-ana and client-john, sends a message as Ana and lists the messages. The same calls work from any language that can send HTTPS requests. The examples use these variables, from the values table:

Terminal window
CCS_ISSUER_URL=https://auth.centralcodestation.com
CCS_CHAT_SYNC_CLIENT_ID=<backend client id>
CCS_CHAT_SYNC_CLIENT_SECRET=<backend client secret>
CCS_TENANT_ID=fitness
CCS_APPLICATION_ID=fitness-app
CHAT_API_URL=https://chat.centralcodestation.com/api/v1
CHAT_API_RESOURCE=https://chat.centralcodestation.com/api

Your backend client uses the client_credentials grant. Send the client id and secret with HTTP Basic authentication and the resource identifier as resource. Cache the machine token until shortly before expires_in.

Terminal window
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)

The response is a standard OAuth token response. Its access_token has the audience chat-api and the scope chat:sync.

A user token comes from token exchange (RFC 8693). Your backend sends the user’s token for your own API and receives a short-lived Chat token, which it hands to your app. Use the same client.

Terminal window
curl -s -X POST "$CCS_ISSUER_URL/oauth/token" \
-u "$CCS_CHAT_SYNC_CLIENT_ID:$CCS_CHAT_SYNC_CLIENT_SECRET" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
--data-urlencode "subject_token=$USER_TOKEN" \
--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
--data-urlencode "resource=$CHAT_API_RESOURCE" \
--data-urlencode "scope=chat:access"

The result is a user token with sub set to the user, the audience chat-api and no refresh token. It lives at most 10 minutes. The subject token must be your own API’s access token: not an ID token. See Issue Chat tokens to your users.

Every call goes to {CHAT_API_URL}/tenants/{tenant_id}/applications/{application_id}/… with Authorization: Bearer <token>. Routes are kebab-case and every field is snake_case.

Chat permission: conversations.createMachine scope (any of): chat:conversations.managechat:sync
Terminal window
curl -s -X POST "$CHAT_API_URL/tenants/$CCS_TENANT_ID/applications/$CCS_APPLICATION_ID/conversations" \
-H "Authorization: Bearer $CHAT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"DIRECT","member_user_ids":["coach-ana","client-john"],"actor_user_id":"coach-ana"}'

Response 201. For a DIRECT conversation that already exists, you get the existing one back.

{
"conversation_id": "6b1f4c7e-3c52-4a0a-9a39-7d2e2f1f8a10",
"tenant_id": "fitness",
"application_id": "fitness-app",
"type": "DIRECT",
"visibility": null,
"name": null,
"description": null,
"status": "ACTIVE",
"created_by_user_id": null,
"created_by_client_id": "2f1c9e5a-7f4d-4f0e-8c35-9b1a3d6e4c21",
"last_activity_at": "2026-10-14T09:30:00.000Z",
"created_at": "2026-10-14T09:00:00.000Z",
"updated_at": "2026-10-14T09:30:00.000Z",
"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,
"my_role": "MEMBER",
"has_unread": true
}

Operation: create a conversation.

Chat permission: messages.sendMachine scope (any of): chat:messages.sendchat:sync
Terminal window
curl -s -X POST "$CHAT_API_URL/tenants/$CCS_TENANT_ID/applications/$CCS_APPLICATION_ID/conversations/$CONVERSATION_ID/messages" \
-H "Authorization: Bearer $CHAT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"body":"See you at 9!","actor_user_id":"coach-ana"}'

Response 201. A backend client sends on behalf of actor_user_id, so sender_client_id holds your client.

{
"message_id": "0d9e6a52-8b0c-4c47-b1f2-5c9a2c1d7e33",
"conversation_id": "6b1f4c7e-3c52-4a0a-9a39-7d2e2f1f8a10",
"tenant_id": "fitness",
"application_id": "fitness-app",
"sender_user_id": "coach-ana",
"sender_client_id": "2f1c9e5a-7f4d-4f0e-8c35-9b1a3d6e4c21",
"sender": { "user_id": "coach-ana", "display_name": "Ana Coach", "avatar_url": null },
"body": "See you at 9!",
"message_type": "TEXT",
"reply_to_message_id": null,
"attachments": [],
"status": "ACTIVE",
"edited_at": null,
"deleted_at": null,
"created_at": "2026-10-14T09:30:00.000Z",
"updated_at": "2026-10-14T09:30:00.000Z",
"reactions": [],
"mentioned_user_ids": []
}

The body can hold up to 10,000 characters and each message up to 10 attachments. Operation: send a message.

Chat permission: messages.readMachine scope (any of): chat:conversations.readchat:sync
Terminal window
curl -s "$CHAT_API_URL/tenants/$CCS_TENANT_ID/applications/$CCS_APPLICATION_ID/conversations/$CONVERSATION_ID/messages?limit=50" \
-H "Authorization: Bearer $CHAT_TOKEN"

Response 200, newest first. Pass next_cursor as before to get the next page. The value is null on the last page. limit defaults to 50 items and is at most 100 items.

{
"items": [
{
"message_id": "0d9e6a52-8b0c-4c47-b1f2-5c9a2c1d7e33",
"conversation_id": "6b1f4c7e-3c52-4a0a-9a39-7d2e2f1f8a10",
"sender_user_id": "coach-ana",
"body": "See you at 9!",
"message_type": "TEXT",
"status": "ACTIVE",
"created_at": "2026-10-14T09:30:00.000Z"
}
],
"next_cursor": "0d9e6a52-8b0c-4c47-b1f2-5c9a2c1d7e32"
}

The items are trimmed here. Each has every field shown in step 2. Operation: list messages.

Every error has the same shape. Branch on error, which is a stable code, and never on message.

{
"status_code": 422,
"error": "conversation_archived",
"message": "Someone tried to post to an archived conversation",
"path": "/api/v1/tenants/fitness/applications/fitness-app/conversations/6b1f4c7e-3c52-4a0a-9a39-7d2e2f1f8a10/messages",
"timestamp": "2026-10-14T09:30:00.000Z"
}

The codes are in the error catalog. On rate_limited (429) wait the number of seconds in the Retry-After header. The limit for a backend client is 600 requests per 60 s per client.

Generate a client from the OpenAPI document

Section titled “Generate a client from the OpenAPI document”

The Chat API is described by one OpenAPI 3 document, /openapi.json. Download it and run the generator for your stack. These commands use OpenAPI Generator:

Terminal window
curl -O https://docs.chat.centralcodestation.com/openapi.json
# Java
npx @openapitools/openapi-generator-cli generate -i openapi.json -g java -o chat-client-java \
--library native --additional-properties=groupId=com.fitness,artifactId=chat-client,invokerPackage=com.fitness.chat
# .NET
npx @openapitools/openapi-generator-cli generate -i openapi.json -g csharp -o chat-client-dotnet \
--additional-properties=packageName=Fitness.Chat.Client
# Python
npx @openapitools/openapi-generator-cli generate -i openapi.json -g python -o chat-client-python \
--additional-properties=packageName=fitness_chat_client
# Go
npx @openapitools/openapi-generator-cli generate -i openapi.json -g go -o chat-client-go \
--git-user-id=fitness --git-repo-id=chat-client --additional-properties=packageName=chatclient

Options differ between generator versions, so check the generator’s documentation for yours. The generated client has no token logic. Fetch and cache the token yourself, as above, and pass it to the client before each call.

  • resource= takes the identifier. Send https://chat.centralcodestation.com/api, never the audience chat-api. The Identity Server finds the resource by its identifier and answers invalid_target otherwise.
  • A token for your own API is not a Chat token. Chat rejects it with 401 invalid_token. Exchange it first.
  • Fields are snake_case. member_user_ids, actor_user_id, next_cursor, ?before=. If your generator maps names to camelCase in code, the JSON on the wire stays snake_case.
  • Use the default error body. Read error, not the text of message.
  • A backend client works only in its own tenant and application. Another context answers 403 client_context_mismatch.
  • Keep the client secret on your server. Never put it in a browser or a mobile app. Native apps get user tokens from your backend by token exchange.
  • Do not send DPoP proofs. The Chat API accepts only Authorization: Bearer.
  • Ids are opaque strings. Store them as given.