Skip to main content
The Chat XDK handles key management, encryption, decryption, and signing for X Chat. It does not call the X HTTP API—pair it with the Python or TypeScript XDK, or with HTTPS and a user access token. App walkthrough: Getting Started. Sample bots: chat-xdk/examples.

Install


Quick start

Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send body to POST /2/chat/conversations/{id}/messages as in Getting Started.

Lifecycle and keys

Construct the SDK, store private keys (Juicebox PIN or a local key blob), register public keys with the Chat API, and set your registered public-key version after unlock or import. Call generate_keypairs once per device/app identity; post the registration payload to the public-keys endpoint. Use setup / unlock (and related PIN helpers) for Juicebox PIN storage 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 Juicebox. A JS server that wants to avoid a Juicebox round-trip per request should reuse one unlocked Chat instance across requests, or run a native binding where key blobs are supported.
The Juicebox config accepts three shapes: the X API 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 Juicebox 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. 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.
Verify fetched keys before wrapping. The prepare methods encrypt the fresh conversation key to whatever public keys you pass. Before passing them, call verify_key_binding(identity, signing, signature) on each fetched record—its public_key, signing_public_key, and identity_public_key_signature fields from the public-keys API—so a substituted identity key cannot receive the conversation key.
Use extract_conversation_keys on key-change event payloads to rebuild { keys, latest_version }. decrypt_conversation_key unwraps a single ECIES blob.
For group create and member adds, pass the params each method needs (member/admin id lists for 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 when you already have a key cache; it raises/throws on failure. Pass signing keys so the SDK can verify senders. Map API public-key fields into SigningKeyEntry: public_key_versionpublic_key_version (same name), signing_public_keypublic_key, public_keyidentity_public_key, plus identity_public_key_signature and user_id. Verification is mandatory by default: omitting or passing an empty signing-key list does not skip it—signed events fail (collected in errors for decrypt_events, thrown for decrypt_event). To actually skip verification you must first call set_reject_unverified(false) (not recommended in production).

Encrypt and send helpers

encrypt_message builds the signed ciphertext for a text message (optional entities, attachments via media_hash_key, TTL, notify flags). Map the returned payload into the send-message body: encrypted_contentencoded_message_create_event, encoded_event_signatureencoded_message_event_signature, plus your message_id. Use encrypt_reply, encrypt_add_reaction, and encrypt_remove_reaction for replies and reactions (sequence_id targets the parent). 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.

Media streams

Encrypt file bytes with the same conversation key used for text, upload via Chat media APIs, and attach media_hash_key on encrypt_message. This is not the Posts media model (expansions=attachments.media_keys). Full upload/download flow: Media.

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.
JS/WASM only: finish() consumes and frees the underlying WASM object—never call free() after finish() (it throws). Call free() only to abandon a stream before finishing (e.g. on an error path).

Utilities

Base64/hex helpers, MIME sniffing, and image dimensions are available as module-level functions (Python/JS/Rust/Go) or ChatXdkUtilities (C#/Java)—useful when building attachment metadata without pulling in extra libraries.

Important types

These conceptual types show up across languages (exact field names differ; JS often uses camelCase event discriminators like message):
  • SendPayload — return value of encrypt_message and related encrypt helpers; 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.
  • PreparedConversationChange — output of the three prepare methods: the derived or passed conversation_id, the raw conversation_key bytes, conversation_key_version, participant_keys (user_id, encrypted_key, public_key_version), and action_signatures (message_id, encoded_message_event_detail, signature, signature_version, public_key_version, optional signature_payload—omitted on key-change signatures because that payload embeds the plaintext key).
  • DecryptEventsResult — messages, optional errors, and extracted conversation_keys.
For complete field lists, use language stubs in the chat-xdk repo (docs/API.md, *.pyi, index.d.ts).

Errors

Python typically raises ValueError with a descriptive message (for example an invalid PIN). 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