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

# Chat XDK 참조

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

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

앱 안내: [시작하기](/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`로 임포트합니다. 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", 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
    ```

    미리 컴파일된 정적 라이브러리가 포함되어 있습니다 (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>
      <version>0.4.0</version>
    </dependency>
    ```

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

***

## 빠른 시작

키를 로드하고, 신원을 한 번 설정하고, 백로그를 복호화하고, 라이브 이벤트 하나를 복호화한 후, 메시지를 암호화합니다. [시작하기](/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와 텍스트만 필요하도록 합니다. 둘 중 하나를 건너뛰고 호출마다 동일한 값을 전달할 수도 있습니다—두 방식 모두 동일하게 검증합니다. [복호화](#복호화)를 참고하세요.

<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 저장소](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          |
| 그룹 생성(`POST /2/chat/conversations/group/initialize`가 생성한 ID) | `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`의 경우 새 멤버와 현재 명단)—샘플은 [그룹](/xchat/groups#create-the-group-and-establish-keys)을 참고하세요. 둘 다 **두 개**의 액션 서명을 반환하며, POST에는 둘 다 포함해야 합니다.

***

## 복호화

\*\*`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는 이로부터 인용된 미리보기(시퀀스 ID, 발신자, 텍스트, 엔티티, 첨부 파일)를 유도하고, 서명된 원본을 발신 메시지에 삽입하여 수신자가 인용을 검증할 수 있도록 합니다. 원본이 답장보다 이전 키 버전으로 암호화된 경우 `reply_to_ckces`—원시 키 변경 이벤트—를 전달하세요. 원본이 **편집된** 경우, 원시 편집 이벤트를 `reply_to_edit_event`로 전달하세요: 그러면 미리보기는 메시지가 현재 말하는 내용을 인용하고(텍스트와 엔티티는 편집본에서 가져옴), 편집본이 원본과 함께 전달되어 수신자가 확인할 수 있습니다. 명시적인 `reply_to_*` 필드는 원시 이벤트를 더 이상 보유하지 않은 호출자를 위한 재정의로 남아 있습니다.

**반응 또한 이벤트 기반입니다.** `encrypt_add_reaction(target_event, emoji)`와 `encrypt_remove_reaction(...)`은 반응 대상이 되는 원시 이벤트에서 대화 ID와 대상 시퀀스 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`\*\*은 첨부 파일 바이트를 암호화합니다. [미디어](/xchat/media)를 참고하세요. 저수준 \*\*`sign` / `verify` / `verify_key_binding`\*\*은 고급 흐름을 지원합니다. 대화 키 변경, 그룹 생성 및 멤버 추가는 [prepare 메서드](#대화-키)로 서명됩니다.

`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`\*\*를 첨부하세요. 이는 게시물 미디어 모델(`expansions=attachments.media_keys`)이 아닙니다. 전체 업로드/다운로드 흐름: [미디어](/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()`는 `StreamEncryptor` / `StreamDecryptor`를 반환하며, 청크(각 약 1 MB)로 `push(chunk)`를 통해 공급한 다음 마지막에 `finish()`를 한 번 호출합니다. 복호화 시 `finish()`는 잘린 스트림을 감지합니다(최종 프레임 전에 입력이 끝나면 실패). 따라서 성공하기 전까지는 push된 평문을 완전한 것으로 취급하지 마세요.

<Warning>
  **JS/WASM에서만:** `finish()`는 기본 WASM 객체를 소비하고 해제합니다. `finish()` 이후에는 절대 `free()`를 호출하지 마세요(예외 발생). 완료 *전*에 스트림을 포기하는 경우에만(예: 오류 경로) `free()`를 호출하세요.
</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`와 같은 camelCase 이벤트 판별자를 사용합니다):

* **SendPayload** — `encrypt_message` 및 다른 encrypt 헬퍼의 반환 값: SDK가 생성한 **`message_id`**(서명된 이벤트에 삽입된 UUID—메시지의 `message_id`로 전송하고 중복 제거를 위해 보관), `encrypted_content`, `encoded_event_signature`, 서명 메타데이터, `conversation_key_version`, `should_notify`. Chat API 전송 본문에 매핑하세요.
* **PublicKeyRegistrationPayload** — 공개 키 추가 API를 위한 `generate_keypairs` / 공개 키 게터의 출력.
* **SigningKeyEntry** — 서명 검증을 위해 복호화에 전달하거나 `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** — 메시지, 선택적 오류, 그리고 추출된 `conversation_keys`. 답장을 인용하는 복호화된 메시지는 `reply_preview_validation`을 담고 있습니다([암호화 및 전송 헬퍼](#암호화-및-전송-헬퍼) 참고).

전체 필드 목록은 [chat-xdk 저장소](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 호출로도 치유할 수 없습니다. 이는 일시적 오류가 아닌 툼스톤으로 취급하세요. 대화 키를 교체하면 그 시점부터 깨끗하고 검증 가능한 이력이 시작됩니다.

***

## 다음 단계

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

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

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

  <Card title="문제 해결" icon="wrench" href="/xchat/troubleshooting">
    자주 발생하는 실패
  </Card>
</CardGroup>
