Install
- Python
- TypeScript
- Rust
- Go
- C#
- Java
chatxdk; import it as chat_xdk. Requires Python 3.10+.Quick start
Load keys, set your identity once, decrypt a backlog, decrypt one live event, encrypt a message. Wire the send body toPOST /2/chat/conversations/{id}/messages as in Getting Started.
The snippets use the two optional session stores for the shortest call forms: set_signing_keys holds the other participants’ public keys (fetched from the public-keys endpoint) so decrypt calls can verify senders without a per-call argument, and set_cache_keys(true) lets the SDK remember each conversation’s verified key so encrypt calls need only the conversation id and text. Skip either and pass the same values per call instead—both styles verify identically; see Decrypt.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
Lifecycle and keys
Construct the SDK, store private keys (passcode-protected secure key backup or a local key blob), register public keys with the Chat API, and callset_identity(user_id, signing_key_version) after unlock or import—it sets the sender and signing-key version every signed action defaults to, so the encrypt and prepare methods work without per-call identity arguments. Call generate_keypairs once per device/app identity; post the registration payload to the public-keys endpoint. Use setup / unlock (and related passcode helpers) for secure key backup on every binding. export_keys / import_keys (raw key-blob persistence for bots and servers) are available on the native bindings only—Python, Go, .NET, JVM, and Rust. The JS/WASM binding does not expose raw key export or import: in a browser any script that reaches the instance could exfiltrate the identity, so JS keeps keys inside secure key backup. A JS server that wants to avoid a backup-realm round-trip per request should reuse one unlocked Chat instance across requests, or run a native binding where key blobs are supported.
The SDK also needs the version the X API reports for your registered public key, so key-change entries targeting other versions are skipped. set_identity records it together with the user id; import_keys accepts it directly as an optional argument (Rust and Go use import_keys_with_version / ImportKeysWithVersion).
- Python
- TypeScript
- Rust
- Go
- C#
- Java
juicebox_config object (recommended—passed verbatim), a full sdk_config wrapper, or a bare token_map.
Optional: signature verification is on by default (reject_unverified = true)—call set_reject_unverified(false) to disable it (not recommended); update_config if backup realm config changes; is_unlocked / has_identity_key for UI state. Full field lists live in the chat-xdk repo stubs.
Conversation keys
Three prepare methods each make one call do everything a key change needs: generate a fresh conversation key, encrypt it for every participant (from the public keys you pass), and sign the change. The sender identity and signing-key version come from the session (set_identity); set sender_id / signing_key_version on the params to override. All return the same PreparedConversationChange shape, ready to POST—rename SDK field encrypted_key to encrypted_conversation_key in conversation_participant_keys, and map the action signatures into the required action_signatures body field.
Keep the raw key bytes for
encrypt_message and media; never pass the API’s encrypted envelope into encrypt.
Use extract_conversation_keys on key-change event payloads to rebuild { keys, latest_version }. decrypt_conversation_key unwraps a single ECIES blob.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
prepare_group_create; new plus current roster for prepare_group_members_change)—see Groups for samples. Both return two action signatures; the POST must include both.
Decrypt
decrypt_events is for history and backlog: it pulls conversation keys from the stream, returns decrypted messages, and collects per-event errors instead of failing the whole batch. decrypt_event is for a single live event; it raises/throws on failure.
Pass signing keys so the SDK can verify senders. Map API public-key fields into SigningKeyEntry: public_key_version → public_key_version (same name), signing_public_key → public_key, public_key → identity_public_key, plus identity_public_key_signature and user_id.
Two opt-in session stores let you omit the per-call key arguments:
set_signing_keys(entries)stores participant signing keys; a decrypt call that omits (or passes an empty) signing-keys argument uses the store instead. Verification itself is unchanged—keys enter the store only through this call, never from the events being decrypted. Each call replaces the previous set.set_cache_keys(true)enables the conversation-key cache (off by default). While enabled,decrypt_eventscaches, per conversation, the latest key whose key change carried a valid signature;decrypt_eventfalls back to it when its conversation-keys argument is omitted, and the encrypt helpers resolve an omitted conversation key from it. Disabling clears the cache.
errors for decrypt_events, thrown for decrypt_event). To actually skip verification you must first call set_reject_unverified(false) (not recommended in production).
- Python
- TypeScript
- Rust
- Go
- C#
- Java
Encrypt and send helpers
encrypt_message(conversation_id, text) builds the signed ciphertext for a text message; optional entities, attachments (via media_hash_key), should_notify, and ttl_msec. The sender identity resolves from the session (set_identity) and the conversation key from the opt-in key cache (set_cache_keys)—or pass sender_id / signing_key_version and conversation_key + conversation_key_version explicitly. The SDK generates the message_id (a UUID embedded in the signed event) and returns it on the payload—never mint your own; reuse the same payload on retries so an id is never minted twice. Map the payload into the send-message body: message_id → message_id, encrypted_content → encoded_message_create_event, encoded_event_signature → encoded_message_event_signature.
Replies are event-based. encrypt_reply(conversation_id, text, reply_to_event) takes the base64 raw event being replied to. The SDK derives the quoted preview (sequence id, sender, text, entities, attachments) from it and embeds the signed original in the outgoing message so recipients can validate the quote. Pass reply_to_ckces—the raw key-change events—when the original was encrypted under an older key version than the reply. When the original was edited, pass the raw edit event as reply_to_edit_event: the preview then quotes what the message says now (its text and entities come from the edit), and the edit travels alongside the original for the receiver to check. The explicit reply_to_* fields remain as overrides for callers that no longer hold the raw event.
Reactions are event-based too. encrypt_add_reaction(target_event, emoji) and encrypt_remove_reaction(...) derive the conversation id and target sequence id from the raw event being reacted to; the same params can add and later remove a reaction. Set conversation_id and target_message_sequence_id explicitly only when you no longer hold the raw event.
On the receiving side, a decrypted message that quotes a reply carries reply_preview_validation ("Valid" / "Invalid"; the JS binding uses 'valid' / 'invalid'): the SDK verified the embedded original’s signature against your signing keys—never a key carried in the event—decrypted it, and compared the quoted content and author against it. When the preview embeds an edit event, the SDK verifies the edit the same way (same conversation, same author as the original) and checks the quoted text against the edited contents rather than the pre-edit text. The field is absent when the message carries no preview or the preview embeds no original. Treat Invalid previews as untrusted: the message itself is authentic, but the quoted material is not—render quotes only from the validated original.
encrypt / decrypt are for UTF-8 metadata under the conversation key (for example an encrypted group name)—not message envelopes. encrypt_stream / decrypt_stream encrypt attachment bytes; see Media. Low-level sign / verify / verify_key_binding support advanced flows; conversation-key changes, group creates, and member adds are signed by the prepare methods.
The conversation id passed to encrypt_message / encrypt_reply can be any form you hold—A:B from events, A-B from listings or URL paths (in either order), or the bare recipient user id—the SDK canonicalizes it before signing. Group ids (prefixed with g) pass through unchanged.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
Media streams
Encrypt file bytes with the same conversation key used for text, upload via Chat media APIs, and attachmedia_hash_key on encrypt_message. This is not the Posts media model (expansions=attachments.media_keys). Full upload/download flow: Media.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
Incremental streaming for large media
For large files, avoid holding the whole payload in memory:stream_encryptor() / stream_decryptor() return a StreamEncryptor / StreamDecryptor you feed in chunks (about 1 MB each) with push(chunk), then call finish() once at the end. On decrypt, finish() detects a truncated stream (it fails if input ended before the final frame), so don’t treat pushed plaintext as complete until it succeeds.
- Python
- TypeScript
Utilities
Base64/hex helpers, MIME sniffing, and image dimensions are available as module-level functions (Python/JS/Rust/Go) orChatXdkUtilities (C#/Java)—useful when building attachment metadata without pulling in extra libraries.
- Python
- TypeScript
- Rust
- Go
- C#
- Java
Important types
These conceptual types show up across languages (exact field names differ; JS often uses camelCase event discriminators likemessage):
- SendPayload — return value of
encrypt_messageand the other encrypt helpers: the SDK-generatedmessage_id(a UUID embedded in the signed event—send it as the message’smessage_idand keep it to dedup),encrypted_content,encoded_event_signature, signature metadata,conversation_key_version, andshould_notify. Map into the Chat API send body. - PublicKeyRegistrationPayload — output of
generate_keypairs/ public-key getters for the add-public-key API. - SigningKeyEntry — sender public material passed into decrypt for signature verification, or stored via
set_signing_keys. - PreparedConversationChange — output of the three prepare methods: the derived or passed
conversation_id, the rawconversation_keybytes,conversation_key_version,participant_keys(user_id,encrypted_key,public_key_version), andaction_signatures(message_id,encoded_message_event_detail,signature,signature_version,public_key_version, optionalsignature_payload—omitted on key-change signatures because that payload embeds the plaintext key). - DecryptEventsResult — messages, optional errors, and extracted
conversation_keys. Decrypted messages that quote a reply carryreply_preview_validation(see Encrypt and send helpers).
docs/API.md, *.pyi, index.d.ts).
Errors
Python typically raisesValueError with a descriptive message (for example an invalid passcode). TypeScript/JavaScript throws Error. Go returns (value, error). Prefer decrypt_events for history so one bad event does not abort the batch; inspect the errors collection for partial failures.
Some verification errors are permanent. Signatures are immutable and verified by rebuilding the signed payload from the event itself, so an old event that fails with signature missing or no matching signing key or an ECDSA mismatch will fail on every future load—no retry, key refresh, or API call can heal it. Treat these as tombstones, not transient errors. Rotating the conversation key starts a clean, verifiable history from that point forward.
Next steps
Getting Started
Wire Chat XDK to the Chat API
Media
Stream encrypt and media REST
Real-time events
Webhooks and activity delivery
Troubleshooting
Common failures