Prerequisites
- Developer account and an app configured for OAuth 2.0
- User access token with
dm.read,dm.write,tweet.read, andusers.read
1. Install dependencies
- Python
- TypeScript
- Rust
- Go
- C#
- Java
chatxdk; import it as chat_xdk. Requires Python 3.10+.- Python
- TypeScript
- Rust
- Go
- C#
- Java
2. Initialize the Chat XDK with existing keys
This step loads keys you already have—use it when this identity completed first-time setup before:- Secure key backup: construct the SDK with the
juicebox_configfrom your public-key record, thenunlockwith your passcode to recover the private keys (for example, on a new device). - Key blob:
import_keyswith a blob you previously exported viaexport_keys, passing the registered key version alongside it (Rust and Go name this variantimport_keys_with_version/ImportKeysWithVersion).
set_identity(user_id, signing_key_version) once, with your user ID and your record’s public_key_version. This stores the session identity: every later encrypt and prepare call signs as this identity, so you never pass a sender ID or signing key version per call.
Setting up for the first time? Construct the SDK the same way but skip unlock/import_keys, and continue to step 3 to create, back up, and register your keys.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
export_keys / import_keys). Client apps often use secure key backup (setup / unlock with a passcode). See the Chat XDK reference for both paths.
Bringing your own keys?
import_keys only accepts the opaque blob produced by export_keys from the Chat XDK—it is a versioned, private serialization of the full key state, not raw or PEM-encoded P-256 keys. You cannot construct this blob yourself: generate keys through generate_keypairs (step 3), export the blob once, and store it base64-encoded. Hand-crafted or modified blobs fail to import.3. Create and register keys (first-time setup)
Skip this step if you loaded existing keys in step 2. Otherwise, one-time setup for a new identity does three things:- Create the keypairs —
generate_keypairsproduces the identity and signing keypairs. - Store the private keys —
setupwith a passcode writes them to secure key backup (clients), orexport_keysreturns a key blob for you to store securely (servers and bots). - Register the public keys — POST the registration payload to the add-public-key endpoint so others can encrypt to you and verify your signatures.
set_identity with the registration’s key version, so this session signs as the new identity.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
4. Set up conversation keys
Callprepare_conversation_key_change with every participant’s identity public key; the sender identity comes from the session you set in step 2. One call generates a fresh conversation key, encrypts it for each participant, and signs the change. POST the result to the add conversation keys endpoint (POST /2/chat/conversations/{id}/keys)—the body needs conversation_key_version, conversation_participant_keys (SDK encrypted_key → API encrypted_conversation_key), and action_signatures (required; the API rejects the call without them). Keep the raw conversation key for sending.
The response returns the canonical conversation id (data.conversation_id—the hyphen-joined pair for a 1:1, or the g-prefixed id for a group) and the key change’s data.sequence_id. Use that returned id for subsequent requests instead of reconstructing it client-side. The same call also rotates keys later: pass the existing conversation id to prepare_conversation_key_change and POST with the newer key version. Rotate when you suspect the conversation key was exposed—rotation protects future messages only; messages encrypted under earlier key versions stay readable to anyone who holds those versions.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
5. Send a message
Encrypt with the raw conversation key from step 4. The SDK generates the message id (a UUID), embeds it in the signed event, and returns it on the payload—you never mint one yourself. On the send request, map:
Use a hyphenated conversation id in the URL path when the API requires it (
: → -). The SDK itself is flexible: encrypt_message and encrypt_reply accept the id in any form you hold—A:B from events, A-B from listings or URL paths (in either order), or just the recipient’s user id—and canonicalize it before signing. Group ids (prefixed with g) pass through unchanged.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
The snippets pass the conversation key explicitly because in this flow you just created it in step 4. Once the key cache is on and a
decrypt_events pass has verified the conversation’s key (step 6), encrypt_message(conversation_id, text) alone is enough—the SDK fills in the latest verified key. Retries should resend the same encrypted payload, so an id is never minted twice.6. Receive and decrypt
Use webhooks or the activity stream for live traffic, or page conversation events for history.- Live payload fields:
encoded_event, optionalconversation_key_change_event - History:
GET /2/chat/conversations/{id}/events— preferdecrypt_eventson all events plusmeta.conversation_key_events - Decrypting needs the senders’ signing keys so the SDK can verify who wrote each message. These are the other participants’ public keys — fetch them from the same public-keys endpoint you used in step 4 and map the fields into
SigningKeyEntry(the snippets below include the mapping) - You can pass the signing keys (and, for
decrypt_event, the conversation keys) on every call, or set two optional session stores once and use the short call forms. The snippets below use the stores:set_signing_keys(entries)holds the participants’ keys, andset_cache_keys(true)(off by default) keeps each conversation’s latest signature-verified key so later calls can omit key arguments. Both styles verify identically - JavaScript uses camelCase event types (
message); other languages use"Message"and snake_case fields in JSON
- Python
- TypeScript
- Rust
- Go
- C#
- Java
Serverless or multi-instance? The signing-key store and key cache live in the SDK instance’s memory. Where that doesn’t fit—one invocation decrypts, another sends—pass keys explicitly instead:
decrypt_events(events, signing_keys), decrypt_event(event_b64, conversation_keys, signing_keys), and the conversation_key/conversation_key_version overrides on the encrypt methods. Persist the conversation_keys returned by decrypt_events yourself and pass them back in.Best practices
- Keep the signing-key store fresh: re-call
set_signing_keyswith the full participant set when a sender registers a new key version, and refresh on signature verification failures - Deduplicate live deliveries with
event_uuid