> ## Documentation Index
> Fetch the complete documentation index at: https://x-preview-mintlify-7f49d7a1.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Referencia del Chat XDK

> Referencia del Chat XDK, el SDK de cifrado que gestiona claves, cifrado, descifrado y firmas para X Chat en los lenguajes compatibles.

El **Chat XDK** se encarga de la gestión de claves, cifrado, descifrado y firma para X Chat. **No** llama a la HTTP API de X—combínalo con el **XDK** de [Python](/xdks/python/overview) o [TypeScript](/xdks/typescript/overview), o con HTTPS y un token de acceso de usuario.

Recorrido de la app: [Primeros pasos](/xchat/getting-started). Bots de ejemplo: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples).

### Instalar

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

    El paquete de PyPI es `chatxdk`; impórtalo como `chat_xdk`. Requiere 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
    ```

    El motor WASM compilado viene dentro del paquete: no hay paso de build. Requiere 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.4.0" }

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

    Se incluyen bibliotecas estáticas precompiladas (macOS arm64/amd64, Linux amd64 glibc/musl): necesitas un compilador de C, pero no Rust. Requiere Go 1.21+.
  </Tab>

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

    El paquete es autónomo: incluye las bibliotecas nativas para macOS (arm64, x64), Linux (x64) y Windows (x64). Requiere .NET 8+.
  </Tab>

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

    Disponible en Maven Central. El jar incluye la biblioteca nativa para macOS (arm64, x64), Linux (x64) y Windows (x64): no necesitas configurar `jna.library.path`. Importa desde `com.x.chatxdk`. Requiere JDK 17+.
  </Tab>
</Tabs>

***

## Inicio rápido

Carga claves, establece tu identidad una vez, descifra un backlog, descifra un evento en vivo y cifra un mensaje. Conecta el cuerpo de envío a [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) como se explica en [Primeros pasos](/xchat/getting-started).

Los snippets usan los dos almacenes de sesión **opcionales** para las formas más cortas de llamada: `set_signing_keys` mantiene las claves públicas de los demás participantes (obtenidas del [endpoint de claves públicas](/x-api/chat/get-user-public-keys)) para que las llamadas de descifrado puedan verificar a los remitentes sin un argumento por llamada, y `set_cache_keys(true)` deja que el SDK recuerde la clave verificada de cada conversación para que las llamadas de cifrado solo necesiten el id de conversación y el texto. Omite cualquiera de los dos y pasa los mismos valores por llamada—ambos estilos verifican de forma idéntica; consulta [Descifrar](#decrypt).

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

    chat = Chat(juicebox_config_json)  # or Chat() + import_keys(blob, version)
    chat.unlock("YOUR_PASSCODE")

    # Session defaults: identity for signing, stored signing keys for
    # verification, opt-in cache for conversation keys
    chat.set_identity(my_user_id, signing_key_version)
    chat.set_signing_keys(signing_keys)  # all participants
    chat.set_cache_keys(True)

    # Batch-decrypt the backlog; senders verify against the stored keys
    result = chat.decrypt_events(raw_events)
    for dm in result["messages"]:
        ev = dm["event"]
        if ev["type"] == "Message":
            print(ev["sender_id"], ev["content"]["text"])

    # Decrypt one live event with the cached conversation key
    event = chat.decrypt_event(one_event_b64)

    # Encrypt and sign as the session identity, under the cached key
    payload = chat.encrypt_message(event["conversation_id"], "Hi!")
    message_id = payload.message_id  # SDK-generated — send as message_id
    ```
  </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');

    // Session defaults: identity for signing, stored signing keys for
    // verification, opt-in cache for conversation keys
    chat.setIdentity(myUserId, signingKeyVersion);
    chat.setSigningKeys(signingKeys); // all participants
    chat.setCacheKeys(true);

    // Batch-decrypt the backlog; senders verify against the stored keys
    const result = chat.decryptEvents(rawEvents);
    for (const dm of result.messages) {
      if (dm.event.type === 'message') {
        console.log(dm.event.senderId, dm.event.content?.text);
      }
    }

    // Decrypt one live event with the cached conversation key
    const event = chat.decryptEvent(oneEventB64);

    // Encrypt and sign as the session identity, under the cached key
    const payload = chat.encryptMessage({ conversationId: event.conversationId!, text: 'Hi!' });
    const messageId = payload.messageId; // SDK-generated — send as message_id
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // chat_xdk_core::Chat + unlock(b"…").await, or ChatCore + import_keys_with_version

    // Session defaults: identity for signing, stored signing keys for
    // verification, opt-in cache for conversation keys
    chat.set_identity(my_user_id, signing_key_version);
    chat.set_signing_keys(signing_keys); // all participants
    chat.set_cache_keys(true);

    // Batch-decrypt the backlog; senders verify against the stored keys
    let result = chat.decrypt_events(&raw_events, &[]);
    for dm in &result.messages {
        if let Event::Message(msg) = &dm.event {
            println!("{}: {}", msg.meta.sender_id.as_deref().unwrap_or("?"), msg.text().unwrap_or(""));
        }
    }

    // Decrypt one live event with the cached conversation key
    let event = chat.decrypt_event(one_event_b64, &Default::default(), &[])?;

    // Encrypt and sign as the session identity, under the cached key
    let payload = chat.encrypt_message(EncryptMessageParams::new(conversation_id, "Hi!"))?;
    let message_id = payload.message_id; // SDK-generated — send as message_id
    ```
  </Tab>

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

    // Session defaults: identity for signing, stored signing keys for
    // verification, opt-in cache for conversation keys
    chat.SetIdentity(myUserID, signingKeyVersion)
    _ = chat.SetSigningKeys(signingKeys) // all participants
    chat.SetCacheKeys(true)

    // Batch-decrypt the backlog; senders verify against the stored keys
    result, err := chat.DecryptEvents(rawEvents, nil)
    for _, dm := range result.Messages {
        if dm.Event.Type == "Message" {
            fmt.Println(dm.Event.AsMessage().Text())
        }
    }

    // Decrypt one live event with the cached conversation key
    event, err := chat.DecryptEvent(oneEventB64, nil, nil)
    msg := event.AsMessage() // nil unless event.Type == "Message"

    // Encrypt and sign as the session identity, under the cached key
    payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
        ConversationID: *msg.ConversationID,
        Text:           "Hi!",
    })
    messageID := payload.MessageID // SDK-generated — send as message_id
    _ = messageID
    _ = err
    ```
  </Tab>

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

    // Session defaults: identity for signing, stored signing keys for
    // verification, opt-in cache for conversation keys
    chat.SetIdentity(myUserId, signingKeyVersion);
    chat.SetSigningKeys(signingKeys); // all participants
    chat.SetCacheKeys(true);

    // Batch-decrypt the backlog; senders verify against the stored keys
    var result = chat.DecryptEvents(rawEvents);
    foreach (var dm in result.Messages)
    {
        if (dm.Event.GetProperty("type").GetString() == "Message")
            Console.WriteLine(dm.Event.GetProperty("content").GetProperty("text").GetString());
    }

    // Decrypt one live event with the cached conversation key
    var evt = chat.DecryptEvent(oneEventB64);
    var conversationId = evt.GetProperty("conversation_id").GetString()!;

    // Encrypt and sign as the session identity, under the cached key
    var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hi!"));
    var messageId = payload.MessageId; // SDK-generated — send as message_id
    ```
  </Tab>

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

        // Session defaults: identity for signing, stored signing keys for
        // verification, opt-in cache for conversation keys
        chat.setIdentity(myUserId, signingKeyVersion);
        chat.setSigningKeys(signingKeys); // all participants
        chat.setCacheKeys(true);

        // Batch-decrypt the backlog; senders verify against the stored keys
        DecryptEventsResult result = chat.decryptEvents(rawEvents, null);
        for (DecryptedMessage dm : result.messages) {
            if ("Message".equals(dm.event.path("type").asText())) {
                System.out.println(dm.event.path("content").path("text").asText());
            }
        }

        // Decrypt one live event with the cached conversation key
        JsonNode event = chat.decryptEvent(oneEventB64, (Map<String, byte[]>) null, null);
        String conversationId = event.path("conversation_id").asText();

        // Encrypt and sign as the session identity, under the cached key
        SendPayload payload = chat.encryptMessage(new EncryptMessageParams(conversationId, "Hi!"));
        String messageId = payload.messageId; // SDK-generated — send as message_id
    }
    ```
  </Tab>
</Tabs>

***

## Ciclo de vida y claves

Construye el SDK, guarda las claves privadas (copia de seguridad segura de claves protegida por código de acceso o un blob de claves local), registra las claves **públicas** con la Chat API y llama a **`set_identity(user_id, signing_key_version)`** después de unlock o import—establece el remitente y la versión de clave de firma con la que cada acción firmada predetermina, de modo que los métodos encrypt y prepare funcionan sin argumentos de identidad por llamada. Llama a `generate_keypairs` una vez por identidad de dispositivo/app; envía el payload de registro al endpoint de claves públicas. Usa `setup` / `unlock` (y helpers de código de acceso relacionados) para la copia de seguridad segura de claves en cada binding. `export_keys` / `import_keys` (persistencia de blob de claves en bruto para bots y servidores) están disponibles **solo en los bindings nativos**—Python, Go, .NET, JVM y Rust. El binding JS/WASM no expone exportación o importación de claves en bruto: en un navegador, cualquier script que alcance la instancia podría exfiltrar la identidad, así que JS mantiene las claves dentro de la copia de seguridad segura de claves. Un servidor JS que quiera evitar un round-trip a un realm de respaldo por solicitud debería reutilizar una instancia `Chat` desbloqueada entre solicitudes, o correr un binding nativo donde los blobs de claves estén soportados.

El SDK también necesita la versión que la X API reporta para tu clave pública registrada, de modo que las entradas de cambio de clave dirigidas a otras versiones se omiten. `set_identity` la registra junto con el id de usuario; `import_keys` la acepta directamente como argumento opcional (Rust y Go usan `import_keys_with_version` / `ImportKeysWithVersion`).

<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_identity(user_id, 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, version)
    chat2.set_identity(user_id, 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.setIdentity(userId, 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(b"YOUR_PASSCODE").await?;
    // chat.unlock(b"YOUR_PASSCODE").await?;
    chat.set_identity(user_id, version);
    let publics = chat.get_public_keys()?;
    let blob = chat.export_keys()?;
    chat.import_keys_with_version(&blob, version)?;
    ```
  </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.ImportKeysWithVersion(keyBlob, version); err != nil {
        log.Fatal(err)
    }
    chat.SetIdentity(userID, 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, version);
    // or secure key backup setup / unlock when config is available
    chat.SetIdentity(userId, version);
    var publics = chat.GetPublicKeys();
    var blob = chat.ExportKeys();
    ```
  </Tab>

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

La configuración de copia de seguridad segura de claves acepta tres formas: el objeto `juicebox_config` de la X API (recomendado—pasado textualmente), un wrapper completo `sdk_config`, o un `token_map` desnudo.

Opcional: la verificación de firmas está **activa por defecto** (`reject_unverified = true`)—llama a `set_reject_unverified(false)` para desactivarla (no recomendado); `update_config` si cambia la configuración del realm de respaldo; `is_unlocked` / `has_identity_key` para el estado de la UI. Las listas completas de campos viven en los stubs del [repositorio chat-xdk](https://github.com/xdevplatform/chat-xdk).

***

## Claves de conversación

Tres métodos **prepare** hacen que una sola llamada haga todo lo que un cambio de clave necesita: generar una clave de conversación nueva, cifrarla para cada participante (a partir de las claves públicas que pases) y firmar el cambio. La identidad del remitente y la versión de la clave de firma provienen de la sesión (`set_identity`); establece `sender_id` / `signing_key_version` en los params para sobrescribir. Todos devuelven la misma forma **`PreparedConversationChange`**, lista para hacer POST—renombra el campo del SDK `encrypted_key` a **`encrypted_conversation_key`** en `conversation_participant_keys`, y mapea las firmas de acción al campo del cuerpo requerido **`action_signatures`**.

| Escenario                                                                                                             | Método                            | Firmas de acción devueltas |
| :-------------------------------------------------------------------------------------------------------------------- | :-------------------------------- | :------------------------- |
| Iniciar un 1:1 (omite el id de conversación—el SDK lo deriva) o rotar la clave de cualquier conversación (pasa el id) | `prepare_conversation_key_change` | 1                          |
| Crear un grupo (id generado por `POST /2/chat/conversations/group/initialize`)                                        | `prepare_group_create`            | 2—envía ambas              |
| Agregar miembros a un grupo                                                                                           | `prepare_group_members_change`    | 2—envía ambas              |

Conserva los bytes **en bruto** de la clave para `encrypt_message` y multimedia; nunca pases el sobre cifrado de la API a encrypt.

<Warning>
  **Verifica las claves recuperadas antes de envolverlas.** Los métodos prepare cifran la nueva clave de conversación hacia cualesquiera claves públicas que le pases. Antes de pasarlas, llama a `verify_key_binding(identity, signing, signature)` en cada registro obtenido—sus campos `public_key`, `signing_public_key` e `identity_public_key_signature` de la API de claves públicas—para que una clave de identidad sustituida no pueda recibir la clave de conversación.
</Warning>

Usa `extract_conversation_keys` en payloads de eventos de cambio de clave para reconstruir `{ keys, latest_version }`. `decrypt_conversation_key` desenvuelve un único blob ECIES.

<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(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({ 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(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{
        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(participants));
    var extracted = chat.ExtractConversationKeys(keyChangeBlobs);
    var raw = extracted.Keys[extracted.LatestVersion];
    var one = chat.DecryptConversationKey(encryptedBlob);
    ```
  </Tab>

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

Para group create y member adds, pasa los params que cada método necesita (listas de ids de miembros/admin para `prepare_group_create`; los nuevos más el roster actual para `prepare_group_members_change`)—consulta [Grupos](/xchat/groups#create-the-group-and-establish-keys) para ejemplos. Ambos devuelven **dos** firmas de acción; el POST debe incluir ambas.

***

## Descifrar

**`decrypt_events`** es para historial y backlog: extrae las claves de conversación del stream, devuelve los mensajes descifrados y **recopila** errores por evento en lugar de fallar todo el lote. **`decrypt_event`** es para un solo evento en vivo; lanza/arroja en caso de fallo.

Pasa **claves de firma** para que el SDK pueda verificar a los remitentes. Mapea los campos de clave pública de la API a `SigningKeyEntry`: `public_key_version` → `public_key_version` (mismo nombre), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, más `identity_public_key_signature` y `user_id`.

Dos almacenes de sesión opcionales te permiten omitir los argumentos de clave por llamada:

* **`set_signing_keys(entries)`** guarda las claves de firma de los participantes; una llamada de descifrado que omita (o pase vacío) el argumento de claves de firma usa el almacén en su lugar. La verificación en sí no cambia—las claves entran al almacén solo mediante esta llamada, nunca desde los eventos que se descifran. Cada llamada reemplaza el conjunto anterior.
* **`set_cache_keys(true)`** habilita la caché de claves de conversación (desactivada por defecto). Mientras está activa, `decrypt_events` cachea, por conversación, la clave más reciente cuyo cambio de clave llevó una firma válida; `decrypt_event` recurre a ella cuando se omite su argumento de claves de conversación, y los helpers de cifrado resuelven una clave de conversación omitida a partir de ella. Desactivarla limpia la caché.

Un argumento explícito no vacío siempre gana sobre los almacenes. Los argumentos explícitos por llamada siguen siendo de primera clase—y son la elección correcta para despliegues serverless o multi-instancia, donde una solicitud puede caer en una instancia nueva cuyos almacenes están vacíos.

La verificación es obligatoria por defecto: omitir las claves de firma nunca la salta. Sin nada pasado y nada almacenado, los eventos firmados fallan (recolectados en `errors` para `decrypt_events`, lanzados para `decrypt_event`). Para saltar realmente la verificación debes llamar primero a `set_reject_unverified(false)` (no recomendado en producción).

<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["type"] == "Message":
            text = ev["content"].get("text")

    cached = result["conversation_keys"]["keys"]
    live = chat.decrypt_event(one_event_b64, cached, signing_keys)
    ```
  </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, signingKeys);
    ```
  </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)?;
    ```
  </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, signingKeys)
    _ = 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, signingKeys);
    ```
  </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, signingKeys);
    ```
  </Tab>
</Tabs>

***

## Cifrar y helpers de envío

**`encrypt_message(conversation_id, text)`** construye el texto cifrado firmado para un mensaje de texto; opcionales `entities`, `attachments` (mediante `media_hash_key`), `should_notify` y `ttl_msec`. La identidad del remitente se resuelve desde la sesión (`set_identity`) y la clave de conversación desde la caché de claves opcional (`set_cache_keys`)—o pasa `sender_id` / `signing_key_version` y `conversation_key` + `conversation_key_version` de forma explícita. El SDK genera el **`message_id`** (un UUID incrustado en el evento firmado) y lo devuelve en el payload—nunca acuñes el tuyo; reutiliza el mismo payload en los reintentos para que un id nunca se acuñe dos veces. Mapea el payload al cuerpo de send-message: `message_id` → **`message_id`**, `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**.

**Las respuestas son basadas en eventos.** `encrypt_reply(conversation_id, text, reply_to_event)` toma el evento en bruto en base64 al que se responde. El SDK deriva la vista previa citada (sequence id, remitente, texto, entities, attachments) a partir de él e incrusta el original firmado en el mensaje saliente para que los destinatarios puedan validar la cita. Pasa `reply_to_ckces`—los eventos de cambio de clave en bruto—cuando el original se cifró bajo una versión de clave más antigua que la respuesta. Cuando el original fue **editado**, pasa el evento de edición en bruto como `reply_to_edit_event`: la vista previa cita entonces lo que el mensaje dice ahora (su texto y entities provienen de la edición) y la edición viaja junto al original para que el receptor la verifique. Los campos explícitos `reply_to_*` permanecen como sobrescrituras para llamadores que ya no tienen el evento en bruto.

**Las reacciones también son basadas en eventos.** `encrypt_add_reaction(target_event, emoji)` y `encrypt_remove_reaction(...)` derivan el id de conversación y el sequence id objetivo a partir del evento en bruto al que se reacciona; los mismos params pueden agregar y luego quitar una reacción. Establece `conversation_id` y `target_message_sequence_id` de forma explícita solo cuando ya no tengas el evento en bruto.

En el lado del receptor, un mensaje descifrado que cita una respuesta lleva **`reply_preview_validation`** (`"Valid"` / `"Invalid"`; el binding JS usa `'valid'` / `'invalid'`): el SDK verificó la firma del original incrustado contra tus claves de firma—nunca una clave transportada en el evento—lo descifró y comparó el contenido citado y el autor contra él. Cuando la vista previa incrusta un evento de edición, el SDK verifica la edición de la misma forma (misma conversación, mismo autor que el original) y comprueba el texto citado contra los contenidos editados en lugar del texto previo a la edición. El campo está ausente cuando el mensaje no lleva vista previa o la vista previa no incrusta un original. Trata las vistas previas `Invalid` como no confiables: el mensaje en sí es auténtico, pero el material citado no lo es—renderiza las citas solo desde el original validado.

**`encrypt` / `decrypt`** son para metadatos UTF-8 bajo la clave de conversación (por ejemplo un nombre de grupo cifrado)—no sobres de mensaje. **`encrypt_stream` / `decrypt_stream`** cifran bytes de adjuntos; consulta [Multimedia](/xchat/media). Las funciones de bajo nivel **`sign` / `verify` / `verify_key_binding`** admiten flujos avanzados; los cambios de clave de conversación, group creates y member adds los firman los [métodos prepare](#conversation-keys).

El id de conversación pasado a `encrypt_message` / `encrypt_reply` puede tener cualquier forma que tengas—`A:B` de eventos, `A-B` de listados o rutas URL (en cualquier orden), o solo el id de usuario del destinatario—el SDK lo canonicaliza antes de firmar. Los ids de grupo (con prefijo `g`) pasan sin cambios.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    payload = chat.encrypt_message(
        conversation_id, "Hello",
        # Optional keyword args: entities, attachments, should_notify, ttl_msec
    )
    body = {
        "message_id": payload.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

    # Preview derived from + embedded raw event so recipients can validate;
    # add reply_to_ckces=[...] when the original used an older key version
    reply = chat.encrypt_reply(conversation_id, "Sounds good", original_event_b64)

    # Conversation and target derived from the raw event
    add = chat.encrypt_add_reaction(original_event_b64, "👍")
    remove = chat.encrypt_remove_reaction(original_event_b64, "👍")

    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({
      conversationId,
      text: 'Hello',
      // Optional: entities, attachments, shouldNotify, ttlMsec
    });
    const body = {
      message_id: payload.messageId,
      encoded_message_create_event: payload.encryptedContent,
      encoded_message_event_signature: payload.encodedEventSignature,
    };
    // POST body to /2/chat/conversations/{id}/messages

    // Preview derived from + embedded raw event so recipients can validate;
    // add replyToCkces: [...] when the original used an older key version
    const reply = chat.encryptReply({
      conversationId,
      text: 'Sounds good',
      replyToEvent: originalEventB64,
    });

    // Conversation and target derived from the raw event
    const add = chat.encryptAddReaction({ emoji: '👍', targetEvent: originalEventB64 });
    const remove = chat.encryptRemoveReaction({ emoji: '👍', targetEvent: originalEventB64 });

    const nameCt = chat.encrypt('Group title', rawConversationKey);
    const title = chat.decrypt(nameCt, rawConversationKey);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let payload = chat.encrypt_message(EncryptMessageParams::new(conversation_id, "Hello"))?;
    // Send body: payload.message_id → message_id,
    // payload.encrypted_content → encoded_message_create_event,
    // payload.encoded_event_signature → encoded_message_event_signature

    // Preview derived from + embedded raw event so recipients can validate;
    // set params.reply_to_ckces when the original used an older key version
    let reply = chat.encrypt_reply(EncryptReplyParams::new(
        conversation_id, "Sounds good", original_event_b64,
    ))?;

    // Conversation and target derived from the raw event
    let reaction = EncryptReactionParams::new(original_event_b64, "👍");
    let add = chat.encrypt_add_reaction(&reaction)?;
    let remove = chat.encrypt_remove_reaction(&reaction)?;

    // conv_key: XChatConversationKey from extract_conversation_keys / decrypt_conversation_key
    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{
        ConversationID: conversationID,
        Text:           "Hello",
    })
    // Send body: payload.MessageID → message_id,
    // payload.EncryptedContent → encoded_message_create_event,
    // payload.EncodedEventSignature → encoded_message_event_signature

    // Preview derived from + embedded raw event so recipients can validate;
    // set ReplyToCkces when the original used an older key version
    reply, err := chat.EncryptReply(chatxdk.EncryptReplyParams{
        ConversationID: conversationID,
        Text:           "Sounds good",
        ReplyToEvent:   originalEventB64,
    })

    // Conversation and target derived from the raw event
    reaction := chatxdk.EncryptReactionParams{Emoji: "👍", TargetEvent: originalEventB64}
    add, err := chat.EncryptAddReaction(reaction)
    remove, err := chat.EncryptRemoveReaction(reaction)

    nameCt, err := chat.Encrypt("Group title", rawKey)
    title, err := chat.Decrypt(nameCt, rawKey)
    _ = payload
    _ = reply
    _ = add
    _ = remove
    _ = title
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hello"));
    // Send body: payload.MessageId → message_id,
    // payload.EncryptedContent → encoded_message_create_event,
    // payload.EncodedEventSignature → encoded_message_event_signature

    // Preview derived from + embedded raw event so recipients can validate;
    // set ReplyToCkces when the original used an older key version
    var reply = chat.EncryptReply(new EncryptReplyParams(conversationId, "Sounds good", originalEventB64));

    // Conversation and target derived from the raw event
    var reaction = new EncryptReactionParams(originalEventB64, "👍");
    var add = chat.EncryptAddReaction(reaction);
    var remove = chat.EncryptRemoveReaction(reaction);

    var nameCt = chat.Encrypt("Group title", rawKey);
    var title = chat.Decrypt(nameCt, rawKey);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    SendPayload payload = chat.encryptMessage(new EncryptMessageParams(conversationId, "Hello"));
    // Send body: payload.messageId → message_id,
    // payload.encryptedContent → encoded_message_create_event,
    // payload.encodedEventSignature → encoded_message_event_signature

    // Preview derived from + embedded raw event so recipients can validate;
    // set replyToCkces when the original used an older key version
    SendPayload reply =
            chat.encryptReply(new EncryptReplyParams(conversationId, "Sounds good", originalEventB64));

    // Conversation and target derived from the raw event
    EncryptReactionParams reaction = new EncryptReactionParams(originalEventB64, "👍");
    SendPayload add = chat.encryptAddReaction(reaction);
    SendPayload remove = chat.encryptRemoveReaction(reaction);

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

***

## Streams de multimedia

Cifra los bytes del archivo con la **misma** clave de conversación usada para el texto, sube mediante las APIs de multimedia del chat y adjunta **`media_hash_key`** en `encrypt_message`. Este no es el modelo de multimedia de Posts (`expansions=attachments.media_keys`). Flujo completo de subida/descarga: [Multimedia](/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>

### Streaming incremental para multimedia grande

Para archivos grandes, evita mantener toda la carga útil en memoria: `stream_encryptor()` / `stream_decryptor()` devuelven un `StreamEncryptor` / `StreamDecryptor` que alimentas por fragmentos (de aproximadamente 1 MB cada uno) con `push(chunk)`, y luego llamas a `finish()` una vez al final. Al descifrar, `finish()` detecta un stream truncado (falla si la entrada terminó antes del frame final), así que no trates el texto plano acumulado como completo hasta que `finish()` tenga éxito.

<Warning>
  **Solo JS/WASM:** `finish()` consume y libera el objeto WASM subyacente—nunca llames a `free()` después de `finish()` (lanza excepción). Llama a `free()` solo para abandonar un stream *antes* de finalizarlo (por ejemplo, en una ruta de error).
</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>

***

## Utilidades

Los helpers de base64/hex, detección MIME y dimensiones de imágenes están disponibles como funciones a nivel de módulo (Python/JS/Rust/Go) o en `ChatXdkUtilities` (C#/Java)—útiles al construir metadatos de adjuntos sin traer librerías adicionales.

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

***

## Tipos importantes

Estos tipos conceptuales aparecen entre lenguajes (los nombres exactos de campos difieren; JS suele usar discriminadores de evento en camelCase como `message`):

* **SendPayload** — valor de retorno de `encrypt_message` y los demás helpers de cifrado: el **`message_id`** generado por el SDK (un UUID incrustado en el evento firmado—envíalo como el `message_id` del mensaje y consérvalo para deduplicar), `encrypted_content`, `encoded_event_signature`, metadatos de firma, `conversation_key_version` y `should_notify`. Mapea al cuerpo de send de la Chat API.
* **PublicKeyRegistrationPayload** — salida de `generate_keypairs` / getters de clave pública para la API de add-public-key.
* **SigningKeyEntry** — material público del remitente pasado a decrypt para verificación de firmas, o almacenado mediante `set_signing_keys`.
* **PreparedConversationChange** — salida de los tres métodos prepare: el `conversation_id` derivado o pasado, los bytes en bruto de `conversation_key`, `conversation_key_version`, `participant_keys` (`user_id`, `encrypted_key`, `public_key_version`) y `action_signatures` (`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, `signature_payload` opcional—omitido en las firmas de cambio de clave porque ese payload incrusta la clave en texto plano).
* **DecryptEventsResult** — messages, `errors` opcional y `conversation_keys` extraídas. Los mensajes descifrados que citan una respuesta llevan `reply_preview_validation` (consulta [Cifrar y helpers de envío](#encrypt-and-send-helpers)).

Para listas completas de campos, usa los stubs de lenguaje en el [repositorio chat-xdk](https://github.com/xdevplatform/chat-xdk) (`docs/API.md`, `*.pyi`, `index.d.ts`).

***

## Errores

Python normalmente lanza **`ValueError`** con un mensaje descriptivo (por ejemplo, un código de acceso inválido). TypeScript/JavaScript lanza **`Error`**. Go devuelve `(value, error)`. Prefiere **`decrypt_events`** para el historial, así un evento defectuoso no aborta el lote; inspecciona la colección de errores para fallos parciales.

Algunos errores de verificación son **permanentes**. Las firmas son inmutables y se verifican reconstruyendo el payload firmado desde el evento en sí, así que un evento antiguo que falla con `signature missing or no matching signing key` o un desajuste ECDSA fallará en cada carga futura—ninguna reintentación, actualización de claves o llamada a la API puede sanarlo. Trata estos como tombstones, no como errores transitorios. Rotar la clave de conversación inicia un historial limpio y verificable desde ese punto en adelante.

***

## Próximos pasos

<CardGroup cols={2}>
  <Card title="Primeros pasos" icon="rocket" href="/xchat/getting-started">
    Conecta el Chat XDK a la Chat API
  </Card>

  <Card title="Multimedia" icon="image" href="/xchat/media">
    Cifrado de streams y REST de multimedia
  </Card>

  <Card title="Eventos en tiempo real" icon="bolt" href="/xchat/real-time-events">
    Webhooks y entrega de actividad
  </Card>

  <Card title="Solución de problemas" icon="wrench" href="/xchat/troubleshooting">
    Fallos comunes
  </Card>
</CardGroup>
