How groups differ from 1:1
Crypto is still: Chat XDK for keys and payloads; X API to create the group, publish participant key wraps, send messages, and load events.
Create the group and establish keys
- Mint the group id with
POST /2/chat/conversations/group/initialize— the response’sdata.conversation_idis the g-prefixed id you use everywhere below. - Load each member’s identity public key and
public_key_version(GETpublic-key routes under Encryption keys;GET /2/users/public_keysfetches several users in one request). Verify each record withverify_key_bindingbefore using it (see the warning in Getting Started). - Run
prepare_group_createonce, with all members (including yourself), the g-prefixed id, and the member/admin id lists. One call generates the conversation key, wraps it for every member, and signs the create with the session identity fromset_identity— it returns two action signatures (the conversation-key change and the group create). POST /2/chat/conversations/groupwith the group members/admins,conversation_key_version,conversation_participant_keys(SDKencrypted_key→ APIencrypted_conversation_key), and bothaction_signatures. Validation failures come back as stable, human-readable messages, for example"Too many members: adding these members would exceed the allowed group size."or"Cannot add all members: one or more of the requested members cannot be added to this conversation.".- Keep the raw conversation key and version for encrypt/decrypt.
prepare_group_create signs the title and avatar_url you pass in and embeds them verbatim in the group-create event. The server matches those against your request, so the group_name / group_avatar_url values in the POST body must be byte-identical to what you passed the SDK — otherwise the call fails signature validation.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
message_id, encoded_message_event_detail, nested message_event_signature) is the same as the keys POST in Getting Started — conversation keys.
When membership changes, call prepare_group_members_change with the new member ids plus the current roster (members, admins, pending members, and the current title/avatar/TTL if set). It rotates the conversation key and, like group create, returns two action signatures — POST all of it to add members (POST /2/chat/conversations/{id}/members). Then expect key-change traffic: treat it like key rotation in Getting Started (extract_conversation_keys / decrypt_events, then encrypt with the latest version).
Because prepare_group_members_change generates a fresh conversation key wrapped only to the roster you pass, new members receive the new key version and cannot decrypt messages sent under earlier versions. The reverse is not true: rotation never revokes access to earlier versions — anyone who already holds an old key can still read the messages encrypted under it. If you suspect a conversation key was exposed, rotate with prepare_conversation_key_change; this protects future messages only.
Encrypted group metadata
Some conversation fields (for example display name or avatar URL) may arrive encrypted under the conversation key. That is notencrypt_message; it is the generic Chat XDK encrypt / decrypt pair (UTF-8 string in, base64 ciphertext out, with the raw conversation key).
Whether a given field is stored encrypted is decided by the client that writes it: prepare_group_create signs and sends the title exactly as you provide it (the conversation key does not exist until that call generates it, so a create-time title cannot be encrypted under it). When you read a conversation whose fields are ciphertext, decrypt them with decrypt and the key version that was active when the field was written.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
Messages and events
Sending and receiving in a group is the same as 1:1 once you have the raw conversation key:- Send:
encrypt_message→ send-message API (Getting Started) - Receive: events API or real-time delivery →
decrypt_event/decrypt_events - Media: Media with the group conversation id
Key changes from departed members
A group’s key-change events are signed by whoever performed them—often the creator or an admin. If that member later leaves the group (or deactivates), the public-keys endpoints stop returning their keys, so the verified decrypt path (decrypt_events with signing keys) fails on those key-change events with signature missing or no matching signing key. The events are not corrupt; the verification material is simply no longer served.
Long-lived groups should expect this and fall back to extract_conversation_keys for key-change events that cannot be verified. This path skips the signature check and recovers the conversation key by decrypting it with your identity key. The security model holds because:
- Only key material that was encrypted to your identity key can be recovered at all—a third party cannot inject a key you can read
- Every message is still signature-verified against its own sender, so message authorship is unaffected
decrypt_events (which also feeds the SDK’s key cache when set_cache_keys(true) is enabled), and reach for extract_conversation_keys only for the key-change events it rejects.
Checklist
- Mint the g-prefixed id with
POST /2/chat/conversations/group/initialize prepare_group_createwith every member; POST participant key wraps and both action signatures toPOST /2/chat/conversations/group- Cache raw key + version; update on key-change events
- On membership changes,
prepare_group_members_change(two signatures) →POST /2/chat/conversations/{id}/members - Decrypt group metadata with
decryptwhen fields are ciphertext - Send/receive with the same patterns as 1:1