> ## Documentation Index
> Fetch the complete documentation index at: https://docs.x.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat XDK Reference

> Reference for the Chat XDK, the encryption SDK handling key management, encryption, decryption, and signing for X Chat across supported languages.

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](/xdks/python/overview) or [TypeScript](/xdks/typescript/overview) **XDK**, or with HTTPS and a user access token.

App walkthrough: [Getting Started](/xchat/getting-started). Sample bots: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples).

### Install

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    pip install chatxdk
    ```

    The PyPI package is `chatxdk`; import it as `chat_xdk`. Requires Python 3.10+.
  </Tab>

  <Tab title="TypeScript">
    ```bash theme={null}
    npm install @xdevplatform/chat-xdk
    npm install juicebox-sdk   # optional peer dependency — required for setup()/unlock() secure key backup
    ```

    The compiled WASM engine ships inside the package—no build step. Requires Node.js 18+.
  </Tab>

  <Tab title="Rust">
    ```toml theme={null}
    [dependencies]
    # chat-xdk-core is not yet on crates.io — use the git dependency.
    # It exports both ChatCore and the async secure-key-backup Chat type.
    chat-xdk-core = { git = "https://github.com/xdevplatform/chat-xdk", tag = "v0.2.1" }

    # Required until thrift 0.24 is released on crates.io
    [patch.crates-io]
    thrift = { git = "https://github.com/apache/thrift.git", rev = "deb36fa409849de45973b04ffc3ce49d277ca90a" }
    ```
  </Tab>

  <Tab title="Go">
    ```bash theme={null}
    go get github.com/xdevplatform/chat-xdk/go/chatxdk
    ```

    Precompiled static libraries are included (macOS arm64/amd64, Linux amd64 glibc/musl)—you need a C compiler but not Rust. Requires Go 1.21+.
  </Tab>

  <Tab title="C#">
    ```bash theme={null}
    dotnet add package XDevPlatform.ChatXdk
    ```

    The package is self-contained: native libraries for macOS (arm64, x64), Linux (x64), and Windows (x64) ship inside it. Requires .NET 8+.
  </Tab>

  <Tab title="Java">
    ```xml theme={null}
    <dependency>
      <groupId>com.x</groupId>
      <artifactId>chatxdk</artifactId>
      <version>0.2.1</version>
    </dependency>
    ```

    Available on Maven Central. The jar bundles the native library for macOS (arm64, x64), Linux (x64), and Windows (x64)—no `jna.library.path` setup needed. Import from `com.x.chatxdk`. Requires JDK 17+.
  </Tab>
</Tabs>

***

## Quick start

Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send body to [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) as in [Getting Started](/xchat/getting-started).

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from chat_xdk import Chat

    chat = Chat(juicebox_config_json)  # or Chat() + import_keys(blob)
    chat.unlock("YOUR_PASSCODE")
    chat.set_key_version(signing_key_version)

    result = chat.decrypt_events(raw_events, signing_keys)
    for dm in result["messages"]:
        ev = dm["event"]
        if ev.get("type") == "Message":
            print(ev.get("sender_id"), ev.get("content", {}).get("text"))

    cached = result["conversation_keys"]["keys"]
    event = chat.decrypt_event(one_event_b64, cached, sender_signing_keys)

    raw_key = cached[result["conversation_keys"]["latest_version"]]
    payload = chat.encrypt_message(
        message_id, sender_id, conversation_id, raw_key, "Hi!",
        conversation_key_version, signing_key_version,
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { createChat } from '@xdevplatform/chat-xdk';

    const chat = await createChat({
      juiceboxConfig: juiceboxConfigJson,
      getAuthToken: async (realmId) => getRealmToken(realmId),
    });
    await chat.unlock('YOUR_PASSCODE');
    chat.setKeyVersion(signingKeyVersion);

    const result = chat.decryptEvents(rawEvents, signingKeys);
    for (const dm of result.messages) {
      if (dm.event.type === 'message') {
        console.log(dm.event.senderId, dm.event.content?.text);
      }
    }

    const cached = result.conversationKeys.keys;
    const event = chat.decryptEvent(oneEventB64, cached, senderSigningKeys);

    const rawKey = cached[result.conversationKeys.latestVersion!];
    const payload = chat.encryptMessage({
      messageId, senderId, conversationId, conversationKey: rawKey, text: 'Hi!',
      conversationKeyVersion, signingKeyVersion,
    });
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // ChatCore + import_keys, or chat_xdk_core::Chat + unlock().await
    let result = chat.decrypt_events(&raw_events, &signing_keys);
    let cached = &result.conversation_keys.keys;
    let event = chat.decrypt_event(one_event_b64, cached, &sender_signing_keys)?;
    // cached values are XChatConversationKey; encrypt_message wants owned bytes
    let latest = result.conversation_keys.latest_version.as_deref().unwrap_or_default();
    let conv_key = cached[latest].to_bytes();
    let payload = chat.encrypt_message(EncryptMessageParams::new(
        &message_id, &sender_id, &conversation_id, conv_key, "Hi!",
        &conversation_key_version, &signing_key_version,
    ))?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    chat := chatxdk.New()
    defer chat.Close()
    blob, _ := chatxdk.Base64ToBytes(privateKeysB64)
    _ = chat.ImportKeys(blob)
    chat.SetKeyVersion(signingKeyVersion)

    result, err := chat.DecryptEvents(rawEvents, signingKeys)
    cached := result.ConversationKeys.Keys
    event, err := chat.DecryptEvent(oneEventB64, cached, senderSigningKeys)
    rawKey := cached[*result.ConversationKeys.LatestVersion]
    payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
        MessageID: messageID, SenderID: senderID, ConversationID: conversationID,
        ConversationKey: rawKey, Text: "Hi!",
        ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion,
    })
    _ = event
    _ = payload
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    using var chat = new Chat();
    chat.ImportKeys(privateKeyBytes);
    chat.SetKeyVersion(signingKeyVersion);

    var result = chat.DecryptEvents(rawEvents, signingKeys);
    var cached = result.ConversationKeys.Keys;
    var evt = chat.DecryptEvent(oneEventB64, cached, senderSigningKeys);
    var payload = chat.EncryptMessage(new EncryptMessageParams {
        MessageId = messageId, SenderId = senderId, ConversationId = conversationId,
        ConversationKey = rawKey, Text = "Hi!",
        ConversationKeyVersion = conversationKeyVersion, SigningKeyVersion = signingKeyVersion,
    });
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    try (Chat chat = new Chat()) {
        chat.importKeys(privateKeyBytes);
        chat.setKeyVersion(signingKeyVersion);

        DecryptEventsResult result = chat.decryptEvents(rawEvents, signingKeys);
        Map<String, byte[]> cached = result.conversationKeys.keys;
        JsonNode event = chat.decryptEvent(oneEventB64, cached, senderSigningKeys);

        EncryptMessageParams params = new EncryptMessageParams();
        params.messageId = messageId;
        params.senderId = senderId;
        params.conversationId = conversationId;
        params.conversationKey = rawKey;
        params.text = "Hi!";
        params.conversationKeyVersion = conversationKeyVersion;
        params.signingKeyVersion = signingKeyVersion;
        SendPayload payload = chat.encryptMessage(params);
    }
    ```
  </Tab>
</Tabs>

***

## 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 set your registered **public-key version** after unlock or import. Secure key backup is implemented with **Juicebox**, which is why the related config fields carry that name. 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.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from chat_xdk import Chat

    # Secure key backup (client)
    chat = Chat(juicebox_config_json)
    chat.setup("YOUR_PASSCODE")          # first time — generates keypairs
    # chat.unlock("YOUR_PASSCODE")        # later sessions
    chat.set_key_version(version)    # from add-public-key / get-public-keys response
    reg = chat.get_public_keys()     # or registration fields from generate_keypairs

    # Key blob (server / bot)
    chat2 = Chat()
    chat2.import_keys(secret_blob)
    chat2.set_key_version(version)
    blob = chat2.export_keys()       # treat as a password
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { createChat } from '@xdevplatform/chat-xdk';

    const chat = await createChat({
      juiceboxConfig: juiceboxConfigJson,
      getAuthToken: async (realmId) => getRealmToken(realmId),
    });
    await chat.setup('YOUR_PASSCODE');
    // await chat.unlock('YOUR_PASSCODE');
    chat.setKeyVersion(version);
    const publics = chat.getPublicKeys();

    // JS/WASM stores keys only through secure key backup — there is no raw key
    // export/import here. For key-blob persistence, use a native binding.
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // chat_xdk_core::Chat — async secure key backup unlock, or ChatCore + import_keys
    chat.setup("YOUR_PASSCODE").await?;
    // chat.unlock("YOUR_PASSCODE").await?;
    chat.set_key_version(&version);
    let publics = chat.get_public_keys()?;
    let blob = chat.export_keys()?;
    chat.import_keys(&blob)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    chat := chatxdk.New()
    defer chat.Close()

    // Prefer ImportKeys for servers; secure key backup unlock where supported
    keyBlob, _ := chatxdk.Base64ToBytes(privateKeysB64)
    if err := chat.ImportKeys(keyBlob); err != nil {
        log.Fatal(err)
    }
    chat.SetKeyVersion(version)
    publics, err := chat.GetPublicKeys()
    blob, err := chat.ExportKeys()
    _ = publics
    _ = blob
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    using var chat = new Chat();
    chat.ImportKeys(privateKeyBytes);
    // or secure key backup setup / unlock when config is available
    chat.SetKeyVersion(version);
    var publics = chat.GetPublicKeys();
    var blob = chat.ExportKeys();
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    try (Chat chat = new Chat()) {
        chat.importKeys(privateKeyBytes);
        chat.setKeyVersion(version);
        var publics = chat.getPublicKeys();
        byte[] blob = chat.exportKeys();
    }
    ```
  </Tab>
</Tabs>

The secure key backup 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 backup realm config changes; `is_unlocked` / `has_identity_key` for UI state. Full field lists live in the [chat-xdk repo](https://github.com/xdevplatform/chat-xdk) 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.

| Scenario                                                                                                 | Method                            | Action signatures returned |
| :------------------------------------------------------------------------------------------------------- | :-------------------------------- | :------------------------- |
| Start a 1:1 (omit the conversation id—the SDK derives it) or rotate any conversation’s key (pass the id) | `prepare_conversation_key_change` | 1                          |
| Create a group (id minted by `POST /2/chat/conversations/group/initialize`)                              | `prepare_group_create`            | 2—send both                |
| Add members to a group                                                                                   | `prepare_group_members_change`    | 2—send both                |

Keep the **raw** key bytes for `encrypt_message` and media; never pass the API’s encrypted envelope into encrypt.

<Warning>
  **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.
</Warning>

Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys, latest_version }`. `decrypt_conversation_key` unwraps a single ECIES blob.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    # One entry per participant public key, from the public-keys API:
    # participants = [
    #     {"user_id": "1215441834412953600", "public_key": "BASE64_IDENTITY_PUBLIC_KEY", "key_version": "1733889755256"},
    #     {"user_id": "1843439638876491776", "public_key": "BASE64_IDENTITY_PUBLIC_KEY", "key_version": "1766181805686"},
    # ]
    prepared = chat.prepare_conversation_key_change(my_user_id, signing_key_version, participants)
    # prepared["conversation_key"]   — raw bytes for encrypt_message
    # prepared["participant_keys"]   — per-user wraps; rename encrypted_key → encrypted_conversation_key on POST
    # prepared["action_signatures"]  — required on the POST body

    extracted = chat.extract_conversation_keys(key_change_blobs)
    keys = extracted["keys"]
    latest = extracted["latest_version"]
    raw = keys[latest]

    one = chat.decrypt_conversation_key(encrypted_blob)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const prepared = chat.prepareConversationKeyChange({
      senderId: myUserId, signingKeyVersion, publicKeys: participants,
    });
    // prepared.conversationKey — Uint8Array for encryptMessage
    // prepared.participantKeys / prepared.actionSignatures — POST body fields

    const extracted = chat.extractConversationKeys(keyChangeBlobs);
    const raw = extracted.keys[extracted.latestVersion!];

    const one = chat.decryptConversationKey(encryptedBlob);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let prepared = chat.prepare_conversation_key_change(
        ConversationKeyChangeParams::new(&my_user_id, &signing_key_version, participants),
    )?;
    let extracted = chat.extract_conversation_keys(&key_change_blobs);
    let latest = extracted.latest_version.as_deref().unwrap_or_default();
    let raw = &extracted.keys[latest];
    let one = chat.decrypt_conversation_key(&encrypted_blob)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    prepared, err := chat.PrepareConversationKeyChange(chatxdk.ConversationKeyChangeParams{
        SenderID: myUserID, SigningKeyVersion: signingKeyVersion, PublicKeys: participants,
    })
    // prepared.ConversationKey feeds EncryptMessage
    // prepared.ParticipantKeys / prepared.ActionSignatures — POST body fields
    extracted, err := chat.ExtractConversationKeys(keyChangeBlobs)
    one, err := chat.DecryptConversationKey(encryptedBlob)
    _ = prepared
    _ = extracted
    _ = one
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var prepared = chat.PrepareConversationKeyChange(new ConversationKeyChangeParams {
        SenderId = myUserId, SigningKeyVersion = signingKeyVersion, PublicKeys = participants,
    });
    var extracted = chat.ExtractConversationKeys(keyChangeBlobs);
    var raw = extracted.Keys[extracted.LatestVersion];
    var one = chat.DecryptConversationKey(encryptedBlob);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    ConversationKeyChangeParams keyParams = new ConversationKeyChangeParams();
    keyParams.senderId = myUserId;
    keyParams.signingKeyVersion = signingKeyVersion;
    keyParams.publicKeys = participants;
    PreparedConversationChange prepared = chat.prepareConversationKeyChange(keyParams);
    ConversationKeyBundle extracted = chat.extractConversationKeys(keyChangeBlobs);
    byte[] raw = extracted.keys.get(extracted.latestVersion);
    byte[] one = chat.decryptConversationKey(encryptedBlob);
    ```
  </Tab>
</Tabs>

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](/xchat/groups#create-the-group-and-establish-keys) 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_version` → `public_key_version` (same name), `signing_public_key` → `public_key`, `public_key` → `identity_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).

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    signing_keys = [{
        "user_id": uid,
        "public_key_version": row["public_key_version"],
        "public_key": row["signing_public_key"],
        "identity_public_key": row["public_key"],
        "identity_public_key_signature": row["identity_public_key_signature"],
    } for row in api_public_keys]

    result = chat.decrypt_events(raw_events, signing_keys)
    for idx, msg in (result.get("errors") or {}).items():
        log.warning("event %s failed: %s", idx, msg)
    for dm in result["messages"]:
        ev = dm["event"]
        if ev.get("type") == "Message":
            text = ev.get("content", {}).get("text")

    cached = result["conversation_keys"]["keys"]
    live = chat.decrypt_event(one_event_b64, cached, signing_keys_for_sender)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const signingKeys = apiPublicKeys.map((row) => ({
      userId: uid,
      publicKeyVersion: row.public_key_version,
      publicKey: row.signing_public_key,
      identityPublicKey: row.public_key,
      identityPublicKeySignature: row.identity_public_key_signature,
    }));

    const result = chat.decryptEvents(rawEvents, signingKeys);
    for (const [idx, msg] of Object.entries(result.errors ?? {})) {
      console.warn(`event ${idx} failed: ${msg}`);
    }
    const cached = result.conversationKeys.keys;
    const live = chat.decryptEvent(oneEventB64, cached, signingKeysForSender);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let result = chat.decrypt_events(&raw_events, &signing_keys);
    for (idx, msg) in &result.errors {
        eprintln!("event {idx} failed: {msg}");
    }
    let cached = &result.conversation_keys.keys;
    let live = chat.decrypt_event(one_event_b64, cached, &signing_keys_for_sender)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    result, err := chat.DecryptEvents(rawEvents, signingKeys)
    for idx, msg := range result.Errors {
        log.Printf("event %s failed: %s", idx, msg)
    }
    cached := result.ConversationKeys.Keys
    live, err := chat.DecryptEvent(oneEventB64, cached, signingKeysForSender)
    _ = live
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var result = chat.DecryptEvents(rawEvents, signingKeys);
    foreach (var kv in result.Errors) { /* kv.Key = event index, kv.Value = error */ }
    var cached = result.ConversationKeys.Keys;
    var live = chat.DecryptEvent(oneEventB64, cached, signingKeysForSender);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    DecryptEventsResult result = chat.decryptEvents(rawEvents, signingKeys);
    Map<String, byte[]> cached = result.conversationKeys.keys;
    JsonNode live = chat.decryptEvent(oneEventB64, cached, signingKeysForSender);
    ```
  </Tab>
</Tabs>

***

## 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_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_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](/xchat/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](#conversation-keys).

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.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    payload = chat.encrypt_message(
        message_id, sender_id, conversation_id, raw_conversation_key, "Hello",
        conversation_key_version, signing_key_version,
        # Optional keyword args: entities, attachments, should_notify, ttl_msec
    )
    body = {
        "message_id": message_id,
        "encoded_message_create_event": payload["encrypted_content"],
        "encoded_message_event_signature": payload["encoded_event_signature"],
    }
    # POST body to /2/chat/conversations/{id}/messages

    reply = chat.encrypt_reply(
        reply_message_id, sender_id, conversation_id, raw_conversation_key,
        "Sounds good", conversation_key_version, signing_key_version,
        parent_sequence_id,  # reply_to_sequence_id — the message being replied to
    )
    name_ct = chat.encrypt("Group title", raw_conversation_key)
    title = chat.decrypt(name_ct, raw_conversation_key)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const payload = chat.encryptMessage({
      messageId, senderId, conversationId, conversationKey: rawConversationKey, text: 'Hello',
      conversationKeyVersion, signingKeyVersion,
    });
    const body = {
      message_id: messageId,
      encoded_message_create_event: payload.encryptedContent,
      encoded_message_event_signature: payload.encodedEventSignature,
    };

    const reply = chat.encryptReply({
      messageId: replyMessageId, senderId, conversationId, conversationKey: rawConversationKey,
      text: 'Sounds good', conversationKeyVersion, signingKeyVersion,
      replyToSequenceId: parentSequenceId, // the message being replied to
    });
    const nameCt = chat.encrypt('Group title', rawConversationKey);
    const title = chat.decrypt(nameCt, rawConversationKey);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // conv_key: XChatConversationKey from extract_conversation_keys / decrypt_conversation_key
    let payload = chat.encrypt_message(EncryptMessageParams::new(
        &message_id, &sender_id, &conversation_id, conv_key.to_bytes(), "Hello",
        &conversation_key_version, &signing_key_version,
    ))?;
    // Map payload fields into the send-message JSON body as above
    let name_ct = chat.encrypt("Group title", &conv_key)?;
    let title = chat.decrypt(&name_ct, &conv_key)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
        MessageID: messageID, SenderID: senderID, ConversationID: conversationID,
        ConversationKey: rawKey, Text: "Hello",
        ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion,
    })
    // body: message_id, encoded_message_create_event, encoded_message_event_signature
    nameCt, err := chat.Encrypt("Group title", rawKey)
    title, err := chat.Decrypt(nameCt, rawKey)
    _ = payload
    _ = title
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var payload = chat.EncryptMessage(new EncryptMessageParams {
        MessageId = messageId, SenderId = senderId, ConversationId = conversationId,
        ConversationKey = rawKey, Text = "Hello",
        ConversationKeyVersion = conversationKeyVersion, SigningKeyVersion = signingKeyVersion,
    });
    // Map EncryptedContent / EncodedEventSignature into the send-message body
    var nameCt = chat.Encrypt("Group title", rawKey);
    var title = chat.Decrypt(nameCt, rawKey);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    EncryptMessageParams params = new EncryptMessageParams();
    params.messageId = messageId;
    params.senderId = senderId;
    params.conversationId = conversationId;
    params.conversationKey = rawKey;
    params.text = "Hello";
    params.conversationKeyVersion = conversationKeyVersion;
    params.signingKeyVersion = signingKeyVersion;
    SendPayload payload = chat.encryptMessage(params);
    // Map to encoded_message_create_event / encoded_message_event_signature on POST

    String nameCt = chat.encrypt("Group title", rawKey);
    String title = chat.decrypt(nameCt, rawKey);
    ```
  </Tab>
</Tabs>

***

## 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](/xchat/media).

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    ciphertext = chat.encrypt_stream(file_bytes, raw_conversation_key)
    # Upload `ciphertext`; the `media_hash_key` you attach on encrypt_message
    # comes from the media-upload finalize step, not from encrypt_stream.

    plain = chat.decrypt_stream(ciphertext, raw_conversation_key)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const ciphertext = chat.encryptStream(fileBytes, rawConversationKey);
    // Upload `ciphertext`; mediaHashKey comes from the upload finalize step.
    const plain = chat.decryptStream(ciphertext, rawConversationKey);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // conv_key: &XChatConversationKey from extract_conversation_keys / decrypt_conversation_key
    let ciphertext = chat.encrypt_stream(&file_bytes, &conv_key)?;
    let plain = chat.decrypt_stream(&ciphertext, &conv_key)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    ciphertext, err := chat.EncryptStream(fileBytes, rawKey)
    plain, err := chat.DecryptStream(ciphertext, rawKey)
    _ = plain
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var ciphertext = chat.EncryptStream(fileBytes, rawKey);
    var plain = chat.DecryptStream(ciphertext, rawKey);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    byte[] ciphertext = chat.encryptStream(fileBytes, rawKey);
    byte[] plain = chat.decryptStream(ciphertext, rawKey);
    ```
  </Tab>
</Tabs>

### 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.

<Warning>
  **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).
</Warning>

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    enc = chat.stream_encryptor(raw_conversation_key)
    chunks = [enc.push(chunk) for chunk in read_in_chunks(file_bytes, 1 << 20)]
    chunks.append(enc.finish())
    ciphertext = b"".join(chunks)

    dec = chat.stream_decryptor(raw_conversation_key)
    out = [dec.push(chunk) for chunk in read_in_chunks(ciphertext, 1 << 20)]
    out.append(dec.finish())  # raises on truncation
    plain = b"".join(out)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const enc = chat.streamEncryptor(rawConversationKey);
    const parts: Uint8Array[] = [];
    try {
      for (const chunk of readInChunks(fileBytes, 1 << 20)) parts.push(enc.push(chunk));
      parts.push(enc.finish()); // consumes + frees enc — do not call enc.free() after this
    } catch (e) {
      enc.free(); // only when abandoning before finish()
      throw e;
    }
    const ciphertext = concat(parts);
    ```
  </Tab>
</Tabs>

***

## 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.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from chat_xdk import (
        bytes_to_base64, base64_to_bytes, bytes_to_hex, hex_to_bytes,
        detect_mime_type, detect_image_dimensions,
    )

    b64 = bytes_to_base64(raw)
    raw2 = base64_to_bytes(b64)
    hexed = bytes_to_hex(raw)
    raw3 = hex_to_bytes(hexed)
    mime = detect_mime_type(file_bytes)
    w, h = detect_image_dimensions(file_bytes)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { bytesToBase64, base64ToBytes, bytesToHex, hexToBytes, detectMimeType, detectImageDimensions } from '@xdevplatform/chat-xdk';

    const b64 = bytesToBase64(raw);
    const raw2 = base64ToBytes(b64);
    const hexed = bytesToHex(raw);
    const raw3 = hexToBytes(hexed);
    const mime = detectMimeType(fileBytes);
    const dims = detectImageDimensions(fileBytes);
    const width = dims?.width ?? 0;
    const height = dims?.height ?? 0;
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let b64 = chat_xdk_core::bytes_to_base64(&raw);
    let raw2 = chat_xdk_core::base64_to_bytes(&b64)?;
    let hexed = chat_xdk_core::bytes_to_hex(&raw);
    let raw3 = chat_xdk_core::hex_to_bytes(&hexed);
    let mime = chat_xdk_core::detect_mime_type(&file_bytes);
    let dims = chat_xdk_core::detect_image_dimensions(&file_bytes);
    let (w, h) = dims.map(|d| (d.width, d.height)).unwrap_or((0, 0));
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    b64, _ := chatxdk.BytesToBase64(raw)
    raw2, err := chatxdk.Base64ToBytes(b64)
    hexed, err := chatxdk.BytesToHex(raw)
    raw3, err := chatxdk.HexToBytes(hexed)
    mime, _ := chatxdk.DetectMimeType(fileBytes)
    dims, _ := chatxdk.DetectImageDimensions(fileBytes)
    w, h := dims.Width, dims.Height
    _ = b64
    _ = raw2
    _ = hexed
    _ = raw3
    _ = mime
    _ = w
    _ = h
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var b64 = ChatXdkUtilities.BytesToBase64(raw);
    var raw2 = ChatXdkUtilities.Base64ToBytes(b64);
    var hexed = ChatXdkUtilities.BytesToHex(raw);
    var raw3 = ChatXdkUtilities.HexToBytes(hexed);
    var mime = ChatXdkUtilities.DetectMimeType(fileBytes);
    var dims = ChatXdkUtilities.DetectImageDimensions(fileBytes);
    var w = dims?.Width ?? 0;
    var h = dims?.Height ?? 0;
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    String b64 = ChatXdkUtilities.bytesToBase64(raw);
    byte[] raw2 = ChatXdkUtilities.base64ToBytes(b64);
    String hexed = ChatXdkUtilities.bytesToHex(raw);
    byte[] raw3 = ChatXdkUtilities.hexToBytes(hexed);
    String mime = ChatXdkUtilities.detectMimeType(fileBytes);
    ImageDimensions wh = ChatXdkUtilities.detectImageDimensions(fileBytes);
    long width = wh.width, height = wh.height;
    ```
  </Tab>
</Tabs>

***

## 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](https://github.com/xdevplatform/chat-xdk) (`docs/API.md`, `*.pyi`, `index.d.ts`).

***

## Errors

Python typically raises **`ValueError`** 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

<CardGroup cols={2}>
  <Card title="Getting Started" icon="rocket" href="/xchat/getting-started">
    Wire Chat XDK to the Chat API
  </Card>

  <Card title="Media" icon="image" href="/xchat/media">
    Stream encrypt and media REST
  </Card>

  <Card title="Real-time events" icon="bolt" href="/xchat/real-time-events">
    Webhooks and activity delivery
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/xchat/troubleshooting">
    Common failures
  </Card>
</CardGroup>
