Skip to content

Other languages and platforms

Use Chat from a stack without the TypeScript libraries. The REST API is the whole contract: you get tokens with HTTP, generate a client from the OpenAPI document, and call the API with a bearer token. The REST-only quickstart walks through the same calls with curl.

Chat permission (any of): conversations.createmessages.sendMachine scope (any of): chat:syncchat:conversations.managechat:messages.send
  • A backend client and the values from the checklist: CCS_ISSUER_URL, CCS_CHAT_SYNC_CLIENT_ID, CCS_CHAT_SYNC_CLIENT_SECRET, CCS_TENANT_ID, CCS_APPLICATION_ID, CHAT_API_URL and CHAT_API_RESOURCE.
  1. Get a machine token. Post a form to $CCS_ISSUER_URL/oauth/token with HTTP Basic authentication (client id and secret), grant_type=client_credentials, scope=chat:sync and resource=$CHAT_API_RESOURCE. Cache it until shortly before expires_in.

  2. Get a user token when an app calls Chat as a user. Your backend sends a token exchange request with grant_type=urn:ietf:params:oauth:grant-type:token-exchange, the user’s token as subject_token and scope=chat:access, and hands the result to the app. See Issue Chat tokens to your users.

  3. Generate a client from the OpenAPI document, or call the API directly. The OpenAPI page has a generator command for each language.

  4. Call the API with Authorization: Bearer <token> on {CHAT_API_URL}/tenants/{tenant_id}/applications/{application_id}/….

Create a DIRECT conversation between coach-ana and client-john, then send a message as Ana. Both examples keep the token until a minute before it expires.

import os, time, requests
BASE = (f"{os.environ['CHAT_API_URL']}/tenants/{os.environ['CCS_TENANT_ID']}"
f"/applications/{os.environ['CCS_APPLICATION_ID']}")
_token = {"value": None, "expires_at": 0.0}
def machine_token() -> str:
if _token["value"] and time.time() < _token["expires_at"]:
return _token["value"]
r = requests.post(
f"{os.environ['CCS_ISSUER_URL']}/oauth/token",
auth=(os.environ["CCS_CHAT_SYNC_CLIENT_ID"], os.environ["CCS_CHAT_SYNC_CLIENT_SECRET"]),
data={"grant_type": "client_credentials", "scope": "chat:sync",
"resource": os.environ["CHAT_API_RESOURCE"]},
timeout=10)
r.raise_for_status()
body = r.json()
_token.update(value=body["access_token"], expires_at=time.time() + body["expires_in"] - 60)
return body["access_token"]
def chat(method: str, path: str, **kwargs) -> dict:
r = requests.request(method, BASE + path, timeout=10, **kwargs,
headers={"Authorization": f"Bearer {machine_token()}"})
if r.status_code == 429:
raise RuntimeError(f"rate limited, retry in {r.headers.get('Retry-After')} s")
r.raise_for_status() # the error body is {"error": "<code>", "message": ...}
return r.json()
conversation = chat("POST", "/conversations", json={
"type": "DIRECT", "member_user_ids": ["coach-ana", "client-john"], "actor_user_id": "coach-ana"})
message = chat("POST", f"/conversations/{conversation['conversation_id']}/messages",
json={"body": "See you at 9!", "actor_user_id": "coach-ana"})
print(message["message_id"])

These examples show the calls, not production code. Cache the token and handle 429 as described in Handle errors and retries. Calls from your backend share one budget: 600 requests per 60 s per client.

Pitfall What to do
resource=chat-api Send the resource identifier, $CHAT_API_RESOURCE, never the audience. The Identity Server answers invalid_target otherwise.
The issuer URL ends in /api CCS_ISSUER_URL is the bare origin. The token endpoint is /oauth/token under it.
Special characters in the client secret RFC 6749 says to URL-encode the id and secret before Base64 for Basic authentication. Most libraries do not. If the secret has such characters, encode both, or post them as form fields.
camelCase JSON Every field is snake_case, requests and responses. Set Jackson’s SNAKE_CASE naming strategy, .NET’s JsonNamingPolicy.SnakeCaseLower (.NET 8 and later), or struct tags in Go. The API rejects fields it does not know with invalid_request.
A user token where a machine token is needed, or the reverse A backend token cannot react, mark read or signal typing (user_required), and cannot use realtime, push or presence (principal_not_allowed). Conversations and messages accept both.
The wrong tenant or application in the URL A token works only in its client’s context: client_context_mismatch.
A client secret in a mobile app Never. A native app gets the user’s Chat token from your backend route, like a web app.
DPoP Not supported. Send plain bearer tokens.

Run the example. The response of the first call has a conversation_id, and the second has a message_id with sender_user_id set to coach-ana. Calling the first request again returns the same conversation_id, because DIRECT conversations dedupe on their members.

Symptom Cause and fix
401 invalid_token Wrong audience or expired token. Request it with the resource identifier, and renew it before expires_in.
403 insufficient_scope The token lacks a scope the call needs. See Grant Chat scopes.
422 target_user_not_eligible A user is not an active tenant member with access to your application.
400 invalid_json The body is not valid JSON, or the Content-Type is not application/json.