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.
Prerequisites
Section titled “Prerequisites”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_URLandCHAT_API_RESOURCE.
-
Get a machine token. Post a form to
$CCS_ISSUER_URL/oauth/tokenwith HTTP Basic authentication (client id and secret),grant_type=client_credentials,scope=chat:syncandresource=$CHAT_API_RESOURCE. Cache it until shortly beforeexpires_in. -
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 assubject_tokenandscope=chat:access, and hands the result to the app. See Issue Chat tokens to your users. -
Generate a client from the OpenAPI document, or call the API directly. The OpenAPI page has a generator command for each language.
-
Call the API with
Authorization: Bearer <token>on{CHAT_API_URL}/tenants/{tenant_id}/applications/{application_id}/….
A worked example
Section titled “A worked example”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"])using System.Net.Http.Headers;using System.Net.Http.Json;using System.Text;using System.Text.Json;
static string Env(string name) => Environment.GetEnvironmentVariable(name)!;var issuer = Env("CCS_ISSUER_URL");var baseUrl = $"{Env("CHAT_API_URL")}/tenants/{Env("CCS_TENANT_ID")}/applications/{Env("CCS_APPLICATION_ID")}";var http = new HttpClient();
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, $"{issuer}/oauth/token"){ Content = new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "client_credentials", ["scope"] = "chat:sync", ["resource"] = Env("CHAT_API_RESOURCE"), }),};var credentials = $"{Env("CCS_CHAT_SYNC_CLIENT_ID")}:{Env("CCS_CHAT_SYNC_CLIENT_SECRET")}";tokenRequest.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(credentials)));var tokenResponse = await http.SendAsync(tokenRequest);tokenResponse.EnsureSuccessStatusCode();var token = (await tokenResponse.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("access_token").GetString();http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var created = await http.PostAsJsonAsync($"{baseUrl}/conversations", new{ type = "DIRECT", member_user_ids = new[] { "coach-ana", "client-john" }, actor_user_id = "coach-ana",});created.EnsureSuccessStatusCode();var id = (await created.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("conversation_id").GetString();
var sent = await http.PostAsJsonAsync($"{baseUrl}/conversations/{id}/messages", new { body = "See you at 9!", actor_user_id = "coach-ana" });sent.EnsureSuccessStatusCode();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.
Pitfalls
Section titled “Pitfalls”| 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. |
Verify
Section titled “Verify”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.
Troubleshooting
Section titled “Troubleshooting”| 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. |