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.
What you will build
Section titled “What you will build”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:
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=fitnessCCS_APPLICATION_ID=fitness-appCHAT_API_URL=https://chat.centralcodestation.com/api/v1CHAT_API_RESOURCE=https://chat.centralcodestation.com/apiGet the tokens
Section titled “Get the tokens”The machine token
Section titled “The machine token”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.
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.
The user token
Section titled “The user token”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.
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.
Make three REST calls
Section titled “Make three REST calls”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.
1. Create the conversation
Section titled “1. Create the conversation”conversations.createMachine scope (any of): chat:conversations.managechat:synccurl -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.
2. Send a message
Section titled “2. Send a message”messages.sendMachine scope (any of): chat:messages.sendchat:synccurl -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.
3. List the messages
Section titled “3. List the messages”messages.readMachine scope (any of): chat:conversations.readchat:synccurl -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.
The error body
Section titled “The error body”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:
curl -O https://docs.chat.centralcodestation.com/openapi.json
# Javanpx @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
# .NETnpx @openapitools/openapi-generator-cli generate -i openapi.json -g csharp -o chat-client-dotnet \ --additional-properties=packageName=Fitness.Chat.Client
# Pythonnpx @openapitools/openapi-generator-cli generate -i openapi.json -g python -o chat-client-python \ --additional-properties=packageName=fitness_chat_client
# Gonpx @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=chatclientOptions 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.
Pitfalls
Section titled “Pitfalls”resource=takes the identifier. Sendhttps://chat.centralcodestation.com/api, never the audiencechat-api. The Identity Server finds the resource by its identifier and answersinvalid_targetotherwise.- 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 ofmessage. - 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.
Next steps
Section titled “Next steps”- API conventions and the API reference.
- Other languages for the same flow in more detail.
- Machine scopes: what
chat:syncand the granular scopes allow. - Handle errors and retries.