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

# Chat XDK 레퍼런스

> 지원되는 언어 전반에서 X Chat의 키 관리, 암호화, 복호화, 서명을 처리하는 암호화 SDK인 Chat XDK 레퍼런스입니다.

**Chat XDK**는 X Chat의 키 관리, 암호화, 복호화, 서명을 처리합니다. X HTTP API를 직접 호출하지 **않습니다**—[Python](/xdks/python/overview) 또는 [TypeScript](/xdks/typescript/overview) **XDK**나 사용자 액세스 토큰을 사용한 HTTPS와 함께 사용하세요.

앱 상세 안내: [시작하기](/ko/xchat/getting-started). 샘플 봇: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples).

### 설치

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

    PyPI 패키지는 `chatxdk`이며 `chat_xdk`로 import합니다. 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
    ```

    컴파일된 WASM 엔진이 패키지 내부에 포함되어 있어 별도의 빌드 단계가 없습니다. 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" }  # pin a release tag in production, e.g. tag = "vX.Y.Z"

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

    사전 컴파일된 정적 라이브러리가 포함되어 있습니다(macOS arm64/amd64, Linux amd64 glibc/musl)—C 컴파일러는 필요하지만 Rust는 필요하지 않습니다. Go 1.21+이 필요합니다.
  </Tab>

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

    패키지는 자체 포함형입니다: macOS(arm64, x64), Linux(x64), Windows(x64)용 네이티브 라이브러리가 내부에 포함되어 있습니다. .NET 8+이 필요합니다.
  </Tab>

  <Tab title="Java">
    ```xml theme={null}
    <dependency>
      <groupId>com.x</groupId>
      <artifactId>chatxdk</artifactId>
      <!-- Use the latest version from https://central.sonatype.com/artifact/com.x/chatxdk -->
      <version>x.y.z</version>
    </dependency>
    ```

    Maven Central에서 사용할 수 있습니다. jar에 macOS(arm64, x64), Linux(x64), Windows(x64)용 네이티브 라이브러리가 번들되어 있어 `jna.library.path` 설정이 필요 없습니다. `com.x.chatxdk`에서 import하세요. JDK 17+이 필요합니다.
  </Tab>
</Tabs>

***

## 빠른 시작

키를 로드하고, 아이덴티티를 한 번 설정하고, 백로그를 복호화하고, 하나의 실시간 이벤트를 복호화하고, 메시지를 암호화합니다. [시작하기](/ko/xchat/getting-started)와 같이 전송 본문을 [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message)에 연결하세요.

스니펫은 가장 짧은 호출 형태를 위해 두 개의 **선택적** 세션 저장소를 사용합니다: `set_signing_keys`는 다른 참여자의 공개 키를 보관하여([공개 키 엔드포인트](/x-api/chat/get-user-public-keys)에서 가져옴) 복호화 호출이 호출별 인수 없이 발신자를 검증할 수 있게 하고, `set_cache_keys(true)`는 SDK가 각 대화의 검증된 키를 기억하도록 하여 암호화 호출이 대화 ID와 텍스트만 필요하게 합니다. 둘 중 하나를 건너뛰고 동일한 값을 호출별로 전달하세요—두 스타일 모두 동일하게 검증합니다; [Decrypt](#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>

***

## 라이프사이클과 키

SDK를 구성하고, 개인 키를 저장하고(패스코드로 보호된 보안 키 백업 또는 로컬 키 blob), Chat API에 **공개** 키를 등록한 다음, unlock 또는 import 후 \*\*`set_identity(user_id, signing_key_version)`\*\*을 호출하세요—모든 서명된 액션이 기본적으로 사용하는 발신자와 서명 키 버전을 설정하므로, encrypt와 prepare 메서드가 호출별 아이덴티티 인수 없이 작동합니다. 기기/앱 아이덴티티당 `generate_keypairs`를 한 번 호출하고, 등록 페이로드를 공개 키 엔드포인트에 게시하세요. 모든 바인딩에서 보안 키 백업에 대해 `setup` / `unlock`(및 관련 패스코드 헬퍼)을 사용하세요. `export_keys` / `import_keys`(봇과 서버를 위한 원시 키 blob 지속성)는 **네이티브 바인딩 전용**입니다—Python, Go, .NET, JVM, Rust. JS/WASM 바인딩은 원시 키 내보내기나 가져오기를 노출하지 않습니다: 브라우저에서는 인스턴스에 접근할 수 있는 어떤 스크립트든 아이덴티티를 유출할 수 있으므로 JS는 키를 보안 키 백업 내부에 유지합니다. 요청당 백업 realm 왕복을 피하려는 JS 서버는 요청 전체에 걸쳐 잠금 해제된 하나의 `Chat` 인스턴스를 재사용하거나, 키 blob이 지원되는 네이티브 바인딩을 실행해야 합니다.

SDK는 또한 등록된 공개 키에 대해 X API가 보고하는 버전을 필요로 하므로, 다른 버전을 대상으로 하는 키 변경 항목은 건너뜁니다. `set_identity`는 이를 사용자 ID와 함께 기록합니다; `import_keys`는 이를 선택적 인수로 직접 받습니다(Rust와 Go는 `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>

보안 키 백업 구성은 세 가지 형태를 허용합니다: X API의 `juicebox_config` 객체(권장—그대로 전달), 전체 `sdk_config` 래퍼, 또는 단순 `token_map`.

선택 사항: 서명 검증은 기본적으로 **켜져 있습니다**(`reject_unverified = true`)—비활성화하려면 `set_reject_unverified(false)`를 호출하세요(권장하지 않음); 백업 realm 구성이 변경되면 `update_config`; UI 상태를 위해 `is_unlocked` / `has_identity_key`. 전체 필드 목록은 [chat-xdk repo](https://github.com/xdevplatform/chat-xdk) 스텁에 있습니다.

***

## 대화 키

세 개의 **prepare** 메서드는 각각 한 번의 호출로 키 변경에 필요한 모든 것을 수행합니다: 새 대화 키를 생성하고, 모든 참여자(전달한 공개 키에서)에 대해 암호화하고, 변경에 서명합니다. 발신자 아이덴티티와 서명 키 버전은 세션(`set_identity`)에서 옵니다. 오버라이드하려면 params에 `sender_id` / `signing_key_version`을 설정하세요. 모두 동일한 **`PreparedConversationChange`** 형태를 반환하여 POST 준비가 됩니다—`conversation_participant_keys`에서 SDK 필드 `encrypted_key`를 \*\*`encrypted_conversation_key`\*\*로 이름 변경하고, 액션 서명을 필수 **`action_signatures`** 본문 필드로 매핑하세요.

| 시나리오                                                         | 메서드                               | 반환된 액션 서명 |
| :----------------------------------------------------------- | :-------------------------------- | :-------- |
| 1:1 시작(대화 ID 생략—SDK가 도출) 또는 임의 대화의 키 순환(ID 전달)               | `prepare_conversation_key_change` | 1         |
| 그룹 생성(ID는 `POST /2/chat/conversations/group/initialize`가 생성) | `prepare_group_create`            | 2—둘 다 전송  |
| 그룹에 멤버 추가                                                    | `prepare_group_members_change`    | 2—둘 다 전송  |

`encrypt_message`와 미디어에 사용할 **원시** 키 바이트를 보관하세요; API의 암호화된 봉투를 절대 encrypt에 전달하지 마세요.

<Warning>
  **감싸기 전에 가져온 키를 검증하세요.** prepare 메서드는 전달하는 모든 공개 키에 대해 새 대화 키를 암호화합니다. 전달하기 전에 각 가져온 레코드에 대해 `verify_key_binding(identity, signing, signature)`을 호출하세요—공개 키 API에서 얻은 `public_key`, `signing_public_key`, `identity_public_key_signature` 필드—대체된 아이덴티티 키가 대화 키를 받지 못하도록 합니다.
</Warning>

키 변경 이벤트 페이로드에 대해 `extract_conversation_keys`를 사용해 `{ keys, latest_version }`을 재구성하세요. `decrypt_conversation_key`는 단일 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(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>

그룹 생성과 멤버 추가에 대해서는 각 메서드가 필요로 하는 params를 전달하세요(`prepare_group_create`에는 멤버/관리자 ID 목록; `prepare_group_members_change`에는 신규 및 현재 명단)—샘플은 [그룹](/ko/xchat/groups#create-the-group-and-establish-keys)을 참고하세요. 둘 다 **두 개**의 액션 서명을 반환합니다; POST에는 둘 모두 포함해야 합니다.

***

## Decrypt

\*\*`decrypt_events`\*\*는 히스토리와 백로그용입니다: 스트림에서 대화 키를 가져오고, 복호화된 메시지를 반환하며, 전체 배치를 실패시키는 대신 이벤트별 오류를 **수집**합니다. \*\*`decrypt_event`\*\*는 단일 실시간 이벤트용입니다; 실패 시 예외를 발생/던집니다.

SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. API 공개 키 필드를 `SigningKeyEntry`에 매핑하세요: `public_key_version` → `public_key_version`(동일 이름), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, 그리고 `identity_public_key_signature`와 `user_id`.

두 개의 옵트인 세션 저장소를 사용하면 호출별 키 인수를 생략할 수 있습니다:

* \*\*`set_signing_keys(entries)`\*\*는 참여자 서명 키를 저장합니다; 서명 키 인수를 생략(또는 빈 값을 전달)하는 복호화 호출은 저장소를 대신 사용합니다. 검증 자체는 변경되지 않습니다—키는 이 호출을 통해서만 저장소에 들어가며, 복호화 중인 이벤트에서는 결코 들어가지 않습니다. 각 호출은 이전 세트를 대체합니다.
* \*\*`set_cache_keys(true)`\*\*는 대화 키 캐시를 활성화합니다(기본은 꺼짐). 활성화된 동안 `decrypt_events`는 대화별로 유효한 서명이 있는 키 변경의 최신 키를 캐시합니다; `decrypt_event`는 대화 키 인수가 생략되면 이에 폴백하고, encrypt 헬퍼는 생략된 대화 키를 이로부터 해석합니다. 비활성화하면 캐시가 지워집니다.

명시적으로 비어 있지 않은 인수는 항상 저장소보다 우선합니다. 명시적 호출별 인수는 여전히 일급이며—서버리스 또는 멀티 인스턴스 배포에 적합한 선택입니다. 여기서는 요청이 저장소가 비어 있는 새 인스턴스에 도달할 수 있습니다.

검증은 기본적으로 필수입니다: 서명 키를 생략해도 검증을 건너뛰지 않습니다. 아무것도 전달하지 않고 아무것도 저장하지 않으면, 서명된 이벤트는 실패합니다(`decrypt_events`의 경우 `errors`에 수집되고, `decrypt_event`의 경우 던져짐). 실제로 검증을 건너뛰려면 먼저 `set_reject_unverified(false)`를 호출해야 합니다(프로덕션에서는 권장하지 않음).

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

***

## 암호화 및 전송 헬퍼

\*\*`encrypt_message(conversation_id, text)`\*\*는 텍스트 메시지에 대한 서명된 암호문을 생성합니다; 선택 사항으로 `entities`, `attachments`(`media_hash_key`를 통해), `should_notify`, `ttl_msec`. 발신자 아이덴티티는 세션(`set_identity`)에서, 대화 키는 옵트인 키 캐시(`set_cache_keys`)에서 해석됩니다—또는 `sender_id` / `signing_key_version` 및 `conversation_key` + `conversation_key_version`을 명시적으로 전달하세요. SDK가 **`message_id`**(서명된 이벤트에 포함된 UUID)를 생성하고 페이로드에 반환합니다—절대 직접 만들지 마세요; 재시도에는 동일한 페이로드를 재사용하여 ID가 두 번 생성되지 않도록 하세요. 페이로드를 send-message 본문으로 매핑하세요: `message_id` → **`message_id`**, `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**.

**답장은 이벤트 기반입니다.** `encrypt_reply(conversation_id, text, reply_to_event)`는 답장할 base64 원시 이벤트를 받습니다. SDK가 이로부터 인용된 미리보기(sequence ID, 발신자, 텍스트, entities, attachments)를 도출하고 서명된 원본을 발신 메시지에 포함하여 수신자가 인용을 검증할 수 있게 합니다. 원본이 답장보다 이전 키 버전으로 암호화된 경우 원시 키 변경 이벤트를 `reply_to_ckces`로 전달하세요. 원본이 **편집된** 경우 원시 편집 이벤트를 `reply_to_edit_event`로 전달하세요: 그러면 미리보기가 메시지가 현재 말하는 것을 인용하며(텍스트와 entities는 편집에서 옴), 편집은 수신자가 확인할 수 있도록 원본과 함께 이동합니다. 명시적 `reply_to_*` 필드는 원시 이벤트를 더 이상 보유하지 않는 호출자에 대한 오버라이드로 유지됩니다.

**리액션도 이벤트 기반입니다.** `encrypt_add_reaction(target_event, emoji)`와 `encrypt_remove_reaction(...)`는 리액션 대상인 원시 이벤트에서 대화 ID와 대상 sequence ID를 도출합니다; 동일한 params로 리액션을 추가하고 나중에 제거할 수 있습니다. 원시 이벤트를 더 이상 보유하지 않을 때에만 `conversation_id`와 `target_message_sequence_id`를 명시적으로 설정하세요.

수신 측에서, 답장을 인용하는 복호화된 메시지는 **`reply_preview_validation`**(`"Valid"` / `"Invalid"`; JS 바인딩은 `'valid'` / `'invalid'` 사용)을 가집니다: SDK가 저장된 서명 키에 대해 포함된 원본의 서명을 검증하고—결코 이벤트에 담긴 키가 아닙니다—복호화한 다음 인용된 콘텐츠와 작성자를 이에 대조합니다. 미리보기가 편집 이벤트를 포함하는 경우, SDK는 편집을 동일한 방식으로 검증하고(동일한 대화, 원본과 동일한 작성자) 편집 이전 텍스트가 아닌 편집된 내용에 대해 인용된 텍스트를 확인합니다. 메시지에 미리보기가 없거나 미리보기에 원본이 포함되지 않은 경우 이 필드는 없습니다. `Invalid` 미리보기는 신뢰할 수 없는 것으로 취급하세요: 메시지 자체는 진짜지만, 인용된 자료는 그렇지 않습니다—인용은 검증된 원본에서만 렌더링하세요.

\*\*`encrypt` / `decrypt`\*\*는 대화 키 하의 UTF-8 메타데이터용입니다(예: 암호화된 그룹 이름)—메시지 봉투용이 아닙니다. \*\*`encrypt_stream` / `decrypt_stream`\*\*은 첨부 파일 바이트를 암호화합니다; [미디어](/ko/xchat/media) 참고. 저수준 \*\*`sign` / `verify` / `verify_key_binding`\*\*은 고급 흐름을 지원합니다; 대화 키 변경, 그룹 생성, 멤버 추가는 [prepare 메서드](#conversation-keys)에 의해 서명됩니다.

`encrypt_message` / `encrypt_reply`에 전달되는 대화 ID는 당신이 보유한 어떤 형태든 될 수 있습니다—이벤트의 `A:B`, 목록이나 URL 경로의 `A-B`(순서 무관), 혹은 수신자의 사용자 ID—SDK가 서명 전에 정규화합니다. 그룹 ID(접두사 `g`)는 변경 없이 통과합니다.

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

***

## 미디어 스트림

텍스트에 사용된 것과 **동일한** 대화 키로 파일 바이트를 암호화하고, Chat 미디어 API를 통해 업로드하고, `encrypt_message`에 \*\*`media_hash_key`\*\*를 첨부하세요. 이는 Posts 미디어 모델(`expansions=attachments.media_keys`)이 아닙니다. 전체 업로드/다운로드 흐름: [미디어](/ko/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>

### 대용량 미디어를 위한 증분 스트리밍

대용량 파일의 경우 전체 페이로드를 메모리에 유지하지 않도록 하세요: `stream_encryptor()` / `stream_decryptor()`는 청크(약 1 MB 각각)로 `push(chunk)`를 공급하고 마지막에 `finish()`를 한 번 호출하는 `StreamEncryptor` / `StreamDecryptor`를 반환합니다. 복호화 시 `finish()`는 잘린 스트림을 감지합니다(마지막 프레임 전에 입력이 끝나면 실패). 따라서 성공할 때까지는 푸시된 평문을 완전한 것으로 취급하지 마세요.

<Warning>
  **JS/WASM 전용:** `finish()`는 내부 WASM 객체를 소비하고 해제합니다—`finish()` 후에는 `free()`를 절대 호출하지 마세요(예외를 던집니다). `free()`는 finish 전에 스트림을 중단할 때만(예: 오류 경로에서) 호출하세요.
</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>

***

## 유틸리티

Base64/hex 헬퍼, MIME 스니핑, 이미지 치수는 모듈 수준 함수(Python/JS/Rust/Go) 또는 `ChatXdkUtilities`(C#/Java)로 제공됩니다—추가 라이브러리를 가져오지 않고 첨부 메타데이터를 구성할 때 유용합니다.

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

***

## 중요한 타입

이러한 개념적 타입은 언어 전반에 걸쳐 나타납니다(정확한 필드 이름은 다르며, JS는 종종 `message`와 같은 카멜케이스 이벤트 판별자를 사용합니다):

* **SendPayload** — `encrypt_message`와 다른 encrypt 헬퍼의 반환 값: SDK가 생성한 **`message_id`**(서명된 이벤트에 포함된 UUID—메시지의 `message_id`로 전송하고 중복 제거를 위해 보관), `encrypted_content`, `encoded_event_signature`, 서명 메타데이터, `conversation_key_version`, `should_notify`. Chat API 전송 본문으로 매핑하세요.
* **PublicKeyRegistrationPayload** — add-public-key API를 위한 `generate_keypairs` / 공개 키 getter의 출력.
* **SigningKeyEntry** — 서명 검증을 위해 decrypt에 전달되거나 `set_signing_keys`로 저장되는 발신자 공개 자료.
* **PreparedConversationChange** — 세 prepare 메서드의 출력: 도출되거나 전달된 `conversation_id`, 원시 `conversation_key` 바이트, `conversation_key_version`, `participant_keys`(`user_id`, `encrypted_key`, `public_key_version`), `action_signatures`(`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, 선택적 `signature_payload`—해당 페이로드가 평문 키를 포함하기 때문에 키 변경 서명에서는 생략됨).
* **DecryptEventsResult** — messages, 선택적 errors, 그리고 추출된 `conversation_keys`. 답장을 인용하는 복호화된 메시지는 `reply_preview_validation`을 가집니다([암호화 및 전송 헬퍼](#encrypt-and-send-helpers) 참고).

전체 필드 목록은 [chat-xdk repo](https://github.com/xdevplatform/chat-xdk)(`docs/API.md`, `*.pyi`, `index.d.ts`)의 언어 스텁을 사용하세요.

***

## 오류

Python은 일반적으로 서술적인 메시지와 함께 \*\*`ValueError`\*\*를 발생시킵니다(예: 잘못된 패스코드). TypeScript/JavaScript는 \*\*`Error`\*\*를 던집니다. Go는 `(value, error)`를 반환합니다. 하나의 잘못된 이벤트가 배치를 중단하지 않도록 히스토리에는 \*\*`decrypt_events`\*\*를 선호하세요; 부분 실패에 대해서는 errors 컬렉션을 검사하세요.

일부 검증 오류는 **영구적입니다**. 서명은 불변이며 이벤트 자체에서 서명된 페이로드를 재구성하여 검증되므로, `signature missing or no matching signing key` 또는 ECDSA 불일치로 실패하는 오래된 이벤트는 이후 모든 로드에서 실패합니다—어떤 재시도, 키 새로 고침, API 호출도 이를 치유할 수 없습니다. 이러한 오류는 일시적 오류가 아니라 묘비(tombstone)로 취급하세요. 대화 키를 순환하면 그 시점부터 깨끗하고 검증 가능한 히스토리가 시작됩니다.

***

## 다음 단계

<CardGroup cols={2}>
  <Card title="시작하기" icon="rocket" href="/ko/xchat/getting-started">
    Chat XDK를 Chat API에 연결
  </Card>

  <Card title="미디어" icon="image" href="/ko/xchat/media">
    스트림 암호화와 미디어 REST
  </Card>

  <Card title="실시간 이벤트" icon="bolt" href="/ko/xchat/real-time-events">
    웹훅과 활동 전달
  </Card>

  <Card title="문제 해결" icon="wrench" href="/ko/xchat/troubleshooting">
    일반적인 실패
  </Card>
</CardGroup>
