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

# Chat API 시작하기

> Python, TypeScript, Go, Rust, C#, Java에서 Chat XDK를 사용해 종단 간 암호화된 X Chat 메시징을 구축하는 단계별 튜토리얼입니다.

X에서 종단 간 암호화된 다이렉트 메시지를 주고받기: 키를 설정하고, 대화를 초기화하며, 메시지를 보내고, 수신 트래픽을 복호화합니다.

X Chat 앱은 두 가지 요소를 함께 사용합니다:

| 구성 요소                               | 역할                                                                                                                                |
| :---------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- |
| **[Chat XDK](/ko/xchat/xchat-xdk)** | 암호화, 복호화, 서명, 개인 키 저장(보안 키 백업 또는 키 blob)                                                                                          |
| **X API**                           | 공개 키, 대화 키, 메시지, 이벤트—[Python](/xdks/python/overview) 또는 [TypeScript](/xdks/typescript/overview) XDK, 또는 사용자 액세스 토큰을 사용한 HTTPS를 통해 |

<Note>
  **전제 조건**

  * [개발자 계정](https://developer.x.com/en/portal/petition/essential/basic-info)과 OAuth 2.0용으로 구성된 앱
  * `dm.read`, `dm.write`, `tweet.read`, `users.read` 권한이 있는 사용자 액세스 토큰
</Note>

***

## 1. 의존성 설치

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

    PyPI 패키지는 `chatxdk`이며 `chat_xdk`로 import합니다. Python 3.10+이 필요합니다.
  </Tab>

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

    컴파일된 WASM 엔진은 `@xdevplatform/chat-xdk` 내부에 포함되어 있어 별도의 빌드 단계가 없습니다. Node.js 18+가 필요합니다.
  </Tab>

  <Tab title="Rust">
    ```toml theme={null}
    [dependencies]
    # chat-xdk-core is not yet on crates.io — use the git dependency
    chat-xdk-core = { git = "https://github.com/xdevplatform/chat-xdk", tag = "v0.4.0" }
    reqwest = { version = "0.12", features = ["blocking", "json"] }
    serde_json = "1"
    base64 = "0.22"

    # 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`에서 import하세요. JDK 17+이 필요합니다.
  </Tab>
</Tabs>

**사용자** OAuth 2.0 액세스 토큰으로 API 클라이언트를 생성합니다:

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

    client = Client(access_token="YOUR_OAUTH2_USER_TOKEN")
    ```
  </Tab>

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

    const client = new Client({ accessToken: 'YOUR_OAUTH2_USER_TOKEN' });
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let access_token = std::env::var("X_ACCESS_TOKEN")?;
    let http = reqwest::blocking::Client::new();
    let auth = format!("Bearer {access_token}");
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    accessToken := os.Getenv("X_ACCESS_TOKEN")
    httpClient := &http.Client{Timeout: 30 * time.Second}
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    using var http = new HttpClient();
    http.DefaultRequestHeaders.Authorization =
        new System.Net.Http.Headers.AuthenticationHeaderValue(
            "Bearer", Environment.GetEnvironmentVariable("X_ACCESS_TOKEN"));
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    String accessToken = System.getenv("X_ACCESS_TOKEN");
    HttpClient http = HttpClient.newHttpClient();
    ```
  </Tab>
</Tabs>

***

## 2. 기존 키로 Chat XDK 초기화

이 단계는 **이미 가지고 있는 키를 로드**합니다—이 아이덴티티가 이전에 최초 설정을 완료한 경우 사용하세요:

* **보안 키 백업:** 공개 키 레코드의 `juicebox_config`로 SDK를 구성한 다음 패스코드로 `unlock`하여 개인 키를 복구합니다(예: 새 기기에서).
* **키 blob:** 이전에 `export_keys`로 내보낸 blob과 함께 등록된 키 버전을 전달하여 `import_keys`를 호출합니다(Rust와 Go에서는 이 변형을 `import_keys_with_version` / `ImportKeysWithVersion`이라고 부릅니다).

그런 다음 \*\*`set_identity(user_id, signing_key_version)`\*\*을 사용자 ID와 레코드의 `public_key_version`으로 한 번 호출합니다. 이는 세션 아이덴티티를 저장합니다: 이후 모든 encrypt와 prepare 호출은 이 아이덴티티로 서명되므로, 호출마다 발신자 ID나 서명 키 버전을 전달할 필요가 없습니다.

**처음 설정 중인가요?** 동일한 방식으로 SDK를 생성하되 `unlock`/`import_keys`를 건너뛰고, 키를 생성하고 백업 및 등록하려면 [3단계](#3-create-and-register-keys-first-time-setup)로 계속 진행하세요.

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

    resp = client.chat.get_user_public_keys(
        "YOUR_USER_ID",
        public_key_fields=[
            "public_key_version", "public_key", "signing_public_key",
            "identity_public_key_signature", "juicebox_config",
        ],
    )
    record = resp.data[0]
    signing_key_version = str(record["public_key_version"])

    chat = Chat(json.dumps(record["juicebox_config"]))
    chat.unlock("YOUR_PASSCODE")  # recovers keys stored by setup() during first-time setup (step 3)
    # Or load a key blob instead of secure key backup:
    # chat.import_keys(blob, version=signing_key_version)
    chat.set_identity("YOUR_USER_ID", signing_key_version)
    ```
  </Tab>

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

    const resp = await client.chat.getUserPublicKeys('YOUR_USER_ID', {
      publicKeyFields: [
        'public_key_version', 'public_key', 'signing_public_key',
        'identity_public_key_signature', 'juicebox_config',
      ],
    });
    const record = resp.data[0];
    const signingKeyVersion = String(record.public_key_version);

    const chat = await createChat({
      juiceboxConfig: JSON.stringify(record.juicebox_config),
      getAuthToken: async (realmId) => getRealmTokenFromYourBackend(realmId),
    });
    await chat.unlock('YOUR_PASSCODE');
    chat.setIdentity('YOUR_USER_ID', signingKeyVersion);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    use base64::{engine::general_purpose::STANDARD as B64, Engine};
    use chat_xdk_core::ChatCore;

    let chat = ChatCore::new();
    let blob = B64.decode(std::env::var("PRIVATE_KEYS_B64")?)?;
    let signing_key_version = std::env::var("SIGNING_KEY_VERSION").unwrap_or_else(|_| "1".into());
    chat.import_keys_with_version(&blob, &signing_key_version)?;
    chat.set_identity("YOUR_USER_ID", &signing_key_version);
    ```
  </Tab>

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

    chat := chatxdk.New()
    defer chat.Close()

    blob, err := chatxdk.Base64ToBytes(os.Getenv("PRIVATE_KEYS_B64"))
    if err != nil {
        log.Fatal(err)
    }
    signingKeyVersion := os.Getenv("SIGNING_KEY_VERSION")
    if signingKeyVersion == "" {
        signingKeyVersion = "1"
    }
    if err := chat.ImportKeysWithVersion(blob, signingKeyVersion); err != nil {
        log.Fatal(err)
    }
    if err := chat.SetIdentity(myUserID, signingKeyVersion); err != nil {
        log.Fatal(err)
    }
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    using ChatXdk;

    using var chat = new Chat();
    var signingKeyVersion = Environment.GetEnvironmentVariable("SIGNING_KEY_VERSION") ?? "1";
    chat.ImportKeys(Convert.FromBase64String(
        Environment.GetEnvironmentVariable("PRIVATE_KEYS_B64")!), signingKeyVersion);
    chat.SetIdentity(myUserId, signingKeyVersion);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    import com.x.chatxdk.Chat;

    String signingKeyVersion = Optional.ofNullable(System.getenv("SIGNING_KEY_VERSION")).orElse("1");
    try (Chat chat = new Chat()) {
        chat.importKeys(Base64.getDecoder().decode(System.getenv("PRIVATE_KEYS_B64")), signingKeyVersion);
        chat.setIdentity(myUserId, signingKeyVersion);
    }
    ```
  </Tab>
</Tabs>

서버와 봇 샘플은 종종 **키 blob**(`export_keys` / `import_keys`)을 사용합니다. 클라이언트 앱은 종종 **보안 키 백업**(패스코드를 사용하는 `setup` / `unlock`)을 사용합니다. 두 경로 모두에 대해서는 [Chat XDK](/ko/xchat/xchat-xdk) 레퍼런스를 참고하세요.

<Note>
  **자체 키를 가져오시나요?** `import_keys`는 Chat XDK의 `export_keys`가 생성한 불투명 blob만 받습니다—이는 원시나 PEM 인코딩된 P-256 키가 아니라, 전체 키 상태의 버전 관리된 비공개 직렬화입니다. 이 blob을 직접 만들 수는 없습니다: `generate_keypairs`([3단계](#3-create-and-register-keys-first-time-setup))로 키를 생성하고, blob을 한 번 내보내어 base64로 인코딩하여 저장하세요. 수작업으로 만들거나 수정된 blob은 import에 실패합니다.
</Note>

***

## 3. 키 생성 및 등록(최초 설정)

[2단계](#2-initialize-the-chat-xdk-with-existing-keys)에서 기존 키를 로드한 경우 이 단계를 건너뛰세요. 그렇지 않은 경우, 새 아이덴티티에 대한 일회성 설정은 **세 가지**를 수행합니다:

1. **키쌍 생성** — `generate_keypairs`가 아이덴티티 및 서명 키쌍을 생성합니다.
2. **개인 키 저장** — 패스코드로 `setup`을 호출하면 보안 키 백업에 저장하고(클라이언트), `export_keys`는 안전하게 저장할 키 blob을 반환합니다(서버 및 봇).
3. **공개 키 등록** — add-public-key 엔드포인트에 등록 페이로드를 POST하여 다른 사람이 당신에게 암호화하고 당신의 서명을 검증할 수 있게 합니다.

등록의 키 버전으로 `set_identity`를 호출하여 마무리하면, 이 세션은 새 아이덴티티로 서명합니다.

<Tip>
  모든 바인딩(Python, TypeScript, Go, Rust, C#, Java)에 대한 바로 실행 가능한 일회성 등록 스크립트가 [`chat-xdk/examples`](https://github.com/xdevplatform/chat-xdk/tree/main/examples)에 있습니다. 새 아이덴티티를 온보딩하기만 하면 될 때는 아래 흐름을 손수 구현하는 대신 이를 사용하세요.
</Tip>

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from xdk.chat.models import AddUserPublicKeyRequest

    registration = chat.generate_keypairs()
    pk = registration.public_key
    client.chat.add_user_public_key(
        "YOUR_USER_ID",
        AddUserPublicKeyRequest(
            public_key={
                "identity_public_key_signature": pk.identity_public_key_signature,
                "public_key": pk.public_key,
                "public_key_fingerprint": pk.public_key_fingerprint,
                "registration_method": pk.registration_method,
                "signing_public_key": pk.signing_public_key,
                "signing_public_key_signature": pk.signing_public_key_signature,
            },
            version=registration.version,
            generate_version=registration.generate_version,
        ),
    )
    chat.setup("YOUR_PASSCODE")
    chat.set_identity("YOUR_USER_ID", str(registration.version or "1"))
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const registration = chat.generateKeypairs();
    const pk = registration.publicKey;
    await client.chat.addUserPublicKey('YOUR_USER_ID', {
      public_key: {
        identity_public_key_signature: pk.identityPublicKeySignature,
        public_key: pk.publicKey,
        public_key_fingerprint: pk.publicKeyFingerprint,
        registration_method: pk.registrationMethod,
        signing_public_key: pk.signingPublicKey,
        signing_public_key_signature: pk.signingPublicKeySignature,
      },
      version: registration.version,
      generate_version: registration.generateVersion,
    });
    await chat.setup('YOUR_PASSCODE');
    chat.setIdentity('YOUR_USER_ID', String(registration.version ?? '1'));
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let registration = chat.generate_keypairs()?;
    let body = serde_json::to_value(&registration)?;
    let resp = http
        .post(format!("https://api.x.com/2/users/{user_id}/public_keys"))
        .header("Authorization", &auth)
        .json(&body)
        .send()?;
    if !resp.status().is_success() {
        anyhow::bail!("register keys: {}", resp.text()?);
    }
    let _blob = chat.export_keys()?; // store securely
    let key_version = registration.version.clone().unwrap_or_else(|| "1".into());
    chat.set_identity(&user_id, &key_version);
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    registration, err := chat.GenerateKeypairs()
    if err != nil {
        log.Fatal(err)
    }
    regJSON, _ := json.Marshal(registration)
    req, _ := http.NewRequest(http.MethodPost,
        "https://api.x.com/2/users/"+userID+"/public_keys",
        bytes.NewReader(regJSON))
    req.Header.Set("Authorization", "Bearer "+accessToken)
    req.Header.Set("Content-Type", "application/json")
    resp, err := httpClient.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    resp.Body.Close()
    privateKeys, _ := chat.ExportKeys() // store securely
    _ = privateKeys
    keyVersion := "1"
    if registration.Version != nil {
        keyVersion = *registration.Version
    }
    if err := chat.SetIdentity(userID, keyVersion); err != nil {
        log.Fatal(err)
    }
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var registration = chat.GenerateKeypairs();
    var regJson = System.Text.Json.JsonSerializer.Serialize(registration);
    using var content = new StringContent(regJson, Encoding.UTF8, "application/json");
    using var regResp = await http.PostAsync(
        $"https://api.x.com/2/users/{Uri.EscapeDataString(userId)}/public_keys", content);
    regResp.EnsureSuccessStatusCode();
    var blob = chat.ExportKeys(); // store securely
    chat.SetIdentity(userId, registration.Version ?? "1");
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    var registration = chat.generateKeypairs();
    String regJson = new ObjectMapper().writeValueAsString(registration);
    HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://api.x.com/2/users/" + userId + "/public_keys"))
        .header("Authorization", "Bearer " + accessToken)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(regJson))
        .build();
    HttpResponse<String> regResp = http.send(req, HttpResponse.BodyHandlers.ofString());
    if (regResp.statusCode() >= 300) {
        throw new RuntimeException("register keys: " + regResp.body());
    }
    byte[] blob = chat.exportKeys(); // store securely
    chat.setIdentity(myUserId, registration.version != null ? registration.version : "1");
    ```
  </Tab>
</Tabs>

<Warning>
  보안 키 백업에는 강력한 패스코드를 사용하세요. 패스코드를 잃거나 보호되지 않은 키 blob을 잃으면 과거 메시지를 복호화하지 못할 수 있습니다.
</Warning>

***

## 4. 대화 키 설정

\*\*`prepare_conversation_key_change`\*\*를 모든 참여자의 아이덴티티 공개 키와 함께 호출합니다. 발신자 아이덴티티는 2단계에서 설정한 세션에서 옵니다. 한 번의 호출로 새 대화 키가 생성되고, 각 참여자에 대해 암호화되며, 변경에 서명이 이루어집니다. 결과를 **add conversation keys** 엔드포인트(`POST /2/chat/conversations/{id}/keys`)에 POST하세요—본문은 `conversation_key_version`, `conversation_participant_keys`(SDK `encrypted_key` → API `encrypted_conversation_key`), 그리고 \*\*`action_signatures`\*\*가 필요합니다(필수이며, 이것이 없으면 API가 호출을 거부합니다). 전송에 사용할 **원시** 대화 키는 보관하세요.

응답은 정규 대화 ID(`data.conversation_id`—1:1의 경우 하이픈으로 연결된 쌍, 그룹의 경우 g가 접두된 ID)와 키 변경의 `data.sequence_id`를 반환합니다. 이후 요청에서는 클라이언트에서 다시 구성하는 대신 반환된 이 ID를 사용하세요. 나중에 동일한 호출로 키를 **순환**시킬 수도 있습니다: 기존 대화 ID를 `prepare_conversation_key_change`에 전달하고 새 키 버전으로 POST하세요. 대화 키가 노출된 것으로 의심되면 순환하세요—순환은 **미래** 메시지만 보호합니다. 이전 키 버전으로 암호화된 메시지는 그 버전을 가진 누구에게나 여전히 읽을 수 있습니다.

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

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    def public_key_input(user_id: str) -> dict:
        r = client.chat.get_user_public_keys(
            user_id, public_key_fields=["public_key_version", "public_key"]
        ).data[0]
        return {"user_id": user_id, "public_key": r["public_key"], "key_version": r["public_key_version"]}

    prepared = chat.prepare_conversation_key_change(
        [public_key_input("YOUR_USER_ID"), public_key_input("RECIPIENT_USER_ID")],
        # conversation_id=None for a new 1:1; pass the id to rotate later
    )
    resp = client.chat.add_conversation_keys(
        "RECIPIENT_USER_ID",
        {
            "conversation_key_version": prepared["conversation_key_version"],
            "conversation_participant_keys": [
                {
                    "user_id": pk["user_id"],
                    "encrypted_conversation_key": pk["encrypted_key"],
                    "public_key_version": pk["public_key_version"],
                }
                for pk in prepared["participant_keys"]
            ],
            "action_signatures": [
                {
                    "message_id": sig["message_id"],
                    "encoded_message_event_detail": sig["encoded_message_event_detail"],
                    "message_event_signature": {
                        "signature": sig["signature"],
                        "public_key_version": sig["public_key_version"],
                        "signature_version": sig["signature_version"],
                    },
                }
                for sig in prepared["action_signatures"]
            ],
        },
    )
    conversation_id = resp.data["conversation_id"]  # canonical id for later requests
    sequence_id = resp.data["sequence_id"]
    conv_key = prepared["conversation_key"]
    conv_key_version = prepared["conversation_key_version"]
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    async function publicKeyInput(userId: string) {
      const r = (await client.chat.getUserPublicKeys(userId, {
        publicKeyFields: ['public_key_version', 'public_key'],
      })).data[0];
      return { userId, publicKey: r.public_key, keyVersion: r.public_key_version };
    }

    // Omit conversationId for a new 1:1; pass the id to rotate later
    const prepared = chat.prepareConversationKeyChange({
      publicKeys: [
        await publicKeyInput('YOUR_USER_ID'),
        await publicKeyInput('RECIPIENT_USER_ID'),
      ],
    });
    const resp = await client.chat.addConversationKeys('RECIPIENT_USER_ID', {
      conversation_key_version: prepared.conversationKeyVersion,
      conversation_participant_keys: prepared.participantKeys.map((pk) => ({
        user_id: pk.userId,
        encrypted_conversation_key: pk.encryptedKey,
        public_key_version: pk.publicKeyVersion,
      })),
      action_signatures: prepared.actionSignatures.map((sig) => ({
        message_id: sig.messageId,
        encoded_message_event_detail: sig.encodedMessageEventDetail,
        message_event_signature: {
          signature: sig.signature,
          public_key_version: sig.publicKeyVersion,
          signature_version: sig.signatureVersion,
        },
      })),
    });
    const conversationId = resp.data.conversation_id; // canonical id for later requests
    const sequenceId = resp.data.sequence_id;
    const convKey = prepared.conversationKey;
    const convKeyVersion = prepared.conversationKeyVersion;
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // public_key_inputs: Vec<PublicKeyInput> from GET public keys
    // (user_id, public_key, key_version ← public_key_version)
    // New 1:1; set params.conversation_id = Some(id) to rotate later
    let prepared = chat.prepare_conversation_key_change(
        ConversationKeyChangeParams::new(public_key_inputs),
    )?;
    let participant_keys: Vec<_> = prepared
        .participant_keys
        .iter()
        .map(|pk| {
            serde_json::json!({
                "user_id": pk.user_id,
                "encrypted_conversation_key": pk.encrypted_key,
                "public_key_version": pk.public_key_version,
            })
        })
        .collect();
    let action_signatures: Vec<_> = prepared
        .action_signatures
        .iter()
        .map(|sig| {
            serde_json::json!({
                "message_id": sig.message_id,
                "encoded_message_event_detail": sig.encoded_message_event_detail,
                "message_event_signature": {
                    "signature": sig.signature,
                    "public_key_version": sig.public_key_version,
                    "signature_version": sig.signature_version,
                },
            })
        })
        .collect();
    let body = serde_json::json!({
        "conversation_key_version": prepared.conversation_key_version,
        "conversation_participant_keys": participant_keys,
        "action_signatures": action_signatures,
    });
    let resp: serde_json::Value = http
        .post(format!("https://api.x.com/2/chat/conversations/{recipient_id}/keys"))
        .header("Authorization", &auth)
        .json(&body)
        .send()?
        .json()?;
    // Canonical id for later requests
    let conversation_id = resp["data"]["conversation_id"].as_str().unwrap().to_string();
    // conversation_key is Option<XChatConversationKey>; encrypt_message wants owned bytes
    let conv_key = prepared.conversation_key.expect("key present").to_bytes();
    let conv_key_version = prepared.conversation_key_version;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    // KeyVersion comes from the public_key_version field on each record
    prepared, err := chat.PrepareConversationKeyChange(chatxdk.ConversationKeyChangeParams{
        PublicKeys: []chatxdk.PublicKeyInput{
            {UserID: myUserID, PublicKey: myIdentityPubB64, KeyVersion: myKeyVersion},
            {UserID: recipientID, PublicKey: theirIdentityPubB64, KeyVersion: theirKeyVersion},
        },
        // ConversationID empty for a new 1:1; pass the id to rotate later
    })
    var parts []map[string]string
    for _, pk := range prepared.ParticipantKeys {
        parts = append(parts, map[string]string{
            "user_id":                    pk.UserID,
            "encrypted_conversation_key": pk.EncryptedKey,
            "public_key_version":         pk.PublicKeyVersion,
        })
    }
    var sigs []map[string]any
    for _, sig := range prepared.ActionSignatures {
        sigs = append(sigs, map[string]any{
            "message_id":                   sig.MessageID,
            "encoded_message_event_detail": sig.EncodedMessageEventDetail,
            "message_event_signature": map[string]string{
                "signature":          sig.Signature,
                "public_key_version": sig.PublicKeyVersion,
                "signature_version":  sig.SignatureVersion,
            },
        })
    }
    body, _ := json.Marshal(map[string]any{
        "conversation_key_version":      prepared.ConversationKeyVersion,
        "conversation_participant_keys": parts,
        "action_signatures":             sigs,
    })
    req, _ := http.NewRequest(http.MethodPost,
        "https://api.x.com/2/chat/conversations/"+recipientID+"/keys",
        bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+accessToken)
    req.Header.Set("Content-Type", "application/json")
    resp, err := httpClient.Do(req)
    // Response data.conversation_id is the canonical id for later requests
    _ = resp
    convKey := prepared.ConversationKey
    convKeyVersion := prepared.ConversationKeyVersion
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    // KeyVersion comes from the public_key_version field on each record
    var prepared = chat.PrepareConversationKeyChange(new ConversationKeyChangeParams(new[] {
        new PublicKeyInput { UserId = myUserId, PublicKey = myPk, KeyVersion = myVer },
        new PublicKeyInput { UserId = recipientId, PublicKey = theirPk, KeyVersion = theirVer },
    })); // ConversationId null for a new 1:1; set it to rotate later
    var keysBody = new {
        conversation_key_version = prepared.ConversationKeyVersion,
        conversation_participant_keys = prepared.ParticipantKeys.Select(pk => new {
            user_id = pk.UserId,
            encrypted_conversation_key = pk.EncryptedKey,
            public_key_version = pk.PublicKeyVersion,
        }),
        action_signatures = prepared.ActionSignatures.Select(sig => new {
            message_id = sig.MessageId,
            encoded_message_event_detail = sig.EncodedMessageEventDetail,
            message_event_signature = new {
                signature = sig.Signature,
                public_key_version = sig.PublicKeyVersion,
                signature_version = sig.SignatureVersion,
            },
        }),
    };
    var json = System.Text.Json.JsonSerializer.Serialize(keysBody);
    using var content = new StringContent(json, Encoding.UTF8, "application/json");
    using var resp = await http.PostAsync(
        $"https://api.x.com/2/chat/conversations/{Uri.EscapeDataString(recipientId)}/keys",
        content);
    resp.EnsureSuccessStatusCode();
    var data = System.Text.Json.JsonDocument.Parse(await resp.Content.ReadAsStringAsync())
        .RootElement.GetProperty("data");
    string conversationId = data.GetProperty("conversation_id").GetString()!; // canonical id
    byte[] convKey = prepared.ConversationKey!;
    string convKeyVersion = prepared.ConversationKeyVersion;
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    // keyVersion comes from the public_key_version field on each record
    PublicKeyInput mine = new PublicKeyInput();
    mine.userId = myUserId; mine.publicKey = myIdentityPubB64; mine.keyVersion = myKeyVersion;
    PublicKeyInput theirs = new PublicKeyInput();
    theirs.userId = recipientId; theirs.publicKey = theirIdentityPubB64; theirs.keyVersion = theirKeyVersion;

    // conversationId stays null for a new 1:1; set it to rotate later
    PreparedConversationChange prepared =
        chat.prepareConversationKeyChange(new ConversationKeyChangeParams(List.of(mine, theirs)));

    List<Map<String, String>> parts = new ArrayList<>();
    for (var pk : prepared.participantKeys) {
        parts.add(Map.of(
            "user_id", pk.userId,
            "encrypted_conversation_key", pk.encryptedKey,
            "public_key_version", pk.publicKeyVersion));
    }
    List<Map<String, Object>> sigs = new ArrayList<>();
    for (var sig : prepared.actionSignatures) {
        sigs.add(Map.of(
            "message_id", sig.messageId,
            "encoded_message_event_detail", sig.encodedMessageEventDetail,
            "message_event_signature", Map.of(
                "signature", sig.signature,
                "public_key_version", sig.publicKeyVersion,
                "signature_version", sig.signatureVersion)));
    }
    ObjectMapper mapper = new ObjectMapper();
    String body = mapper.writeValueAsString(Map.of(
        "conversation_key_version", prepared.conversationKeyVersion,
        "conversation_participant_keys", parts,
        "action_signatures", sigs));
    HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://api.x.com/2/chat/conversations/" + recipientId + "/keys"))
        .header("Authorization", "Bearer " + accessToken)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();
    HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
    JsonNode data = mapper.readTree(resp.body()).path("data");
    String conversationId = data.path("conversation_id").asText(); // canonical id
    byte[] convKey = prepared.conversationKey;
    String convKeyVersion = prepared.conversationKeyVersion;
    ```
  </Tab>
</Tabs>

***

## 5. 메시지 보내기

4단계의 **원시** 대화 키로 암호화합니다. SDK는 메시지 ID(UUID)를 생성하여 서명된 이벤트에 포함시키고 페이로드에 반환합니다—절대 직접 만들지 마세요. 전송 요청에서는 다음을 매핑하세요:

| Chat XDK 필드                                                                   | 요청 본문 필드                          |
| :---------------------------------------------------------------------------- | :-------------------------------- |
| `encrypted_content` / `encryptedContent` / `EncryptedContent`                 | `encoded_message_create_event`    |
| `encoded_event_signature` / `encodedEventSignature` / `EncodedEventSignature` | `encoded_message_event_signature` |
| 페이로드 `message_id` / `messageId` / `MessageId`                                 | `message_id`                      |

API가 요구하는 경우 URL 경로에는 **하이픈이 있는** 대화 ID를 사용하세요(`:` → `-`). SDK 자체는 유연합니다: `encrypt_message`와 `encrypt_reply`는 당신이 보유한 어떤 형태의 ID든 받아들입니다—이벤트의 `A:B`, 목록이나 URL 경로의 `A-B`(순서 무관), 혹은 그저 수신자의 사용자 ID까지—그리고 서명 전에 정규화합니다. 그룹 ID(접두사 `g`)는 변경 없이 통과합니다.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from xdk.chat.models import SendMessageRequest

    # Sender identity resolves from set_identity (step 2)
    payload = chat.encrypt_message(
        "CONVERSATION_ID",
        "Hello!",
        conversation_key=conv_key,
        conversation_key_version=conv_key_version,
    )
    client.chat.send_message(
        "RECIPIENT_USER_ID",
        SendMessageRequest(
            message_id=payload.message_id,  # SDK-generated, embedded in the signed event
            encoded_message_create_event=payload.encrypted_content,
            encoded_message_event_signature=payload.encoded_event_signature,
        ),
    )
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // Sender identity resolves from setIdentity (step 2)
    const payload = chat.encryptMessage({
      conversationId: 'CONVERSATION_ID',
      text: 'Hello!',
      conversationKey: convKey,
      conversationKeyVersion: convKeyVersion,
    });
    await client.chat.sendMessage('RECIPIENT_USER_ID', {
      message_id: payload.messageId, // SDK-generated, embedded in the signed event
      encoded_message_create_event: payload.encryptedContent,
      encoded_message_event_signature: payload.encodedEventSignature,
    });
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    use chat_xdk_core::EncryptMessageParams;

    // Sender identity resolves from set_identity (step 2)
    let payload = chat.encrypt_message(
        EncryptMessageParams::new(&conversation_id, "Hello!")
            .with_conversation_key(conv_key, &conv_key_version),
    )?;
    let body = serde_json::json!({
        // SDK-generated, embedded in the signed event
        "message_id": payload.message_id,
        "encoded_message_create_event": payload.encrypted_content,
        "encoded_message_event_signature": payload.encoded_event_signature,
    });
    let path_id = conversation_id.replace(':', "-");
    http.post(format!("https://api.x.com/2/chat/conversations/{path_id}/messages"))
        .header("Authorization", &auth)
        .json(&body)
        .send()?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    // Sender identity resolves from SetIdentity (step 2)
    payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
        ConversationID:         conversationID,
        Text:                   "Hello!",
        ConversationKey:        convKey,
        ConversationKeyVersion: convKeyVersion,
    })
    if err != nil {
        log.Fatal(err)
    }
    body, _ := json.Marshal(map[string]string{
        // SDK-generated, embedded in the signed event
        "message_id":                      payload.MessageID,
        "encoded_message_create_event":    payload.EncryptedContent,
        "encoded_message_event_signature": payload.EncodedEventSignature,
    })
    pathID := strings.ReplaceAll(conversationID, ":", "-")
    req, _ := http.NewRequest(http.MethodPost,
        "https://api.x.com/2/chat/conversations/"+pathID+"/messages",
        bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+accessToken)
    req.Header.Set("Content-Type", "application/json")
    resp, err := httpClient.Do(req)
    _ = resp
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    // Sender identity resolves from SetIdentity (step 2)
    var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hello!") {
        ConversationKey = convKey,
        ConversationKeyVersion = convKeyVersion,
    });
    var sendJson = System.Text.Json.JsonSerializer.Serialize(new Dictionary<string, string> {
        // SDK-generated, embedded in the signed event
        ["message_id"] = payload.MessageId,
        ["encoded_message_create_event"] = payload.EncryptedContent,
        ["encoded_message_event_signature"] = payload.EncodedEventSignature,
    });
    using var content = new StringContent(sendJson, Encoding.UTF8, "application/json");
    var pathId = conversationId.Replace(':', '-');
    using var resp = await http.PostAsync(
        $"https://api.x.com/2/chat/conversations/{Uri.EscapeDataString(pathId)}/messages",
        content);
    resp.EnsureSuccessStatusCode();
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    // Sender identity resolves from setIdentity (step 2)
    EncryptMessageParams params = new EncryptMessageParams(conversationId, "Hello!");
    params.conversationKey = convKey;
    params.conversationKeyVersion = convKeyVersion;
    SendPayload payload = chat.encryptMessage(params);

    String pathId = conversationId.replace(':', '-');
    String sendJson = new ObjectMapper().writeValueAsString(Map.of(
        // SDK-generated, embedded in the signed event
        "message_id", payload.messageId,
        "encoded_message_create_event", payload.encryptedContent,
        "encoded_message_event_signature", payload.encodedEventSignature));
    HttpRequest req = HttpRequest.newBuilder()
        .uri(URI.create("https://api.x.com/2/chat/conversations/" + pathId + "/messages"))
        .header("Authorization", "Bearer " + accessToken)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(sendJson))
        .build();
    http.send(req, HttpResponse.BodyHandlers.ofString());
    ```
  </Tab>
</Tabs>

<Note>
  스니펫은 이 흐름에서 방금 4단계에서 대화 키를 생성했기 때문에 명시적으로 전달합니다. 키 캐시가 켜져 있고 `decrypt_events` 실행이 대화의 키를 검증한 후에는([6단계](#6-receive-and-decrypt)), `encrypt_message(conversation_id, text)`만으로 충분합니다—SDK가 최신 검증된 키를 채웁니다. 재시도는 **같은** 암호화된 페이로드를 다시 보내야 하므로 ID가 두 번 생성되지 않습니다.
</Note>

***

## 6. 수신 및 복호화

실시간 트래픽에는 [웹훅 또는 활동 스트림](/ko/xchat/real-time-events)을 사용하고, 히스토리에는 대화 **events**를 페이지 조회하세요.

* 실시간 페이로드 필드: `encoded_event`, 선택적 `conversation_key_change_event`
* 히스토리: `GET /2/chat/conversations/{id}/events` — 모든 이벤트에 \*\*`decrypt_events`\*\*와 `meta.conversation_key_events`를 함께 사용하는 것을 권장
* 복호화하려면 발신자의 **서명 키**가 필요하므로 SDK가 각 메시지의 작성자를 검증할 수 있습니다. 이는 다른 참여자의 *공개* 키입니다—4단계에서 사용한 동일한 공개 키 엔드포인트에서 가져와 `SigningKeyEntry`로 필드를 매핑하세요(아래 스니펫에 매핑이 포함되어 있습니다).
* 모든 호출에 서명 키를(그리고 `decrypt_event`의 경우 대화 키를) 전달하거나, 두 개의 선택적 세션 저장소를 한 번 설정한 후 짧은 호출 형태를 사용할 수 있습니다. 아래 스니펫은 저장소를 사용합니다: `set_signing_keys(entries)`는 참여자의 키를 보관하고, `set_cache_keys(true)`(기본은 꺼짐)는 각 대화의 최신 **서명 검증된** 키를 보관하여 이후 호출이 키 인자를 생략할 수 있게 합니다. 두 스타일 모두 동일하게 검증합니다.
* JavaScript는 카멜케이스 이벤트 타입(`message`)을 사용하고, 다른 언어는 JSON에서 `"Message"`와 스네이크케이스 필드를 사용합니다.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    # Once per process: fill the signing-key store and enable the key cache
    def signing_keys_for(user_id: str) -> list[dict]:
        resp = client.chat.get_user_public_keys(
            user_id,
            public_key_fields=[
                "public_key_version", "public_key", "signing_public_key", "identity_public_key_signature",
            ],
        )
        return [
            {
                "user_id": user_id,
                "public_key_version": r["public_key_version"],
                "public_key": r["signing_public_key"],
                "identity_public_key": r["public_key"],
                "identity_public_key_signature": r["identity_public_key_signature"],
            }
            for r in resp.data
        ]

    chat.set_signing_keys(
        signing_keys_for("YOUR_USER_ID") + signing_keys_for("RECIPIENT_USER_ID")
    )
    chat.set_cache_keys(True)

    # Initial load or pagination: batch decrypt. Conversation keys are
    # extracted from the KeyChange events in the batch; per-event failures
    # are collected in result["errors"], never raised.
    result = chat.decrypt_events(all_events_b64)
    for dm in result["messages"]:
        event = dm["event"]
        if event["type"] == "Message" and event["content"]["content_type"] == "Text":
            print(event["sender_id"], event["content"]["text"], event["verified"])

    # Live traffic: one event at a time
    def handle_payload(payload: dict):
        if payload.get("conversation_key_change_event"):
            # A rotation enters the key cache only after its signature
            # verifies, which is what decrypt_events does
            chat.decrypt_events([payload["conversation_key_change_event"]])
        event = chat.decrypt_event(payload["encoded_event"])  # raises on failure
        if event["type"] == "Message" and event["content"]["content_type"] == "Text":
            print(event["sender_id"], event["content"]["text"], event["verified"])
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // Once per process: fill the signing-key store and enable the key cache
    async function signingKeysFor(userId: string) {
      const resp = await client.chat.getUserPublicKeys(userId, {
        publicKeyFields: [
          'public_key_version', 'public_key', 'signing_public_key', 'identity_public_key_signature',
        ],
      });
      return resp.data.map((r: {
        public_key_version: string;
        public_key: string;
        signing_public_key: string;
        identity_public_key_signature: string;
      }) => ({
        userId,
        publicKeyVersion: r.public_key_version,
        publicKey: r.signing_public_key,
        identityPublicKey: r.public_key,
        identityPublicKeySignature: r.identity_public_key_signature,
      }));
    }

    chat.setSigningKeys([
      ...(await signingKeysFor('YOUR_USER_ID')),
      ...(await signingKeysFor('RECIPIENT_USER_ID')),
    ]);
    chat.setCacheKeys(true);

    // Initial load or pagination: batch decrypt. Conversation keys are
    // extracted from the KeyChange events in the batch; per-event failures
    // are collected in result.errors, never thrown.
    const result = chat.decryptEvents(allEventsB64);
    for (const dm of result.messages) {
      if (dm.event.type === 'message' && dm.event.content?.contentType === 'text') {
        console.log(dm.event.senderId, dm.event.content.text, dm.event.verified);
      }
    }

    // Live traffic: one event at a time
    function handlePayload(payload: {
      encoded_event: string;
      conversation_key_change_event?: string;
    }) {
      if (payload.conversation_key_change_event) {
        // A rotation enters the key cache only after its signature
        // verifies, which is what decryptEvents does
        chat.decryptEvents([payload.conversation_key_change_event]);
      }
      const event = chat.decryptEvent(payload.encoded_event); // throws on failure
      if (event.type === 'message' && event.content?.contentType === 'text') {
        console.log(event.senderId, event.content.text, event.verified);
      }
    }
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // Once per instance: fill the signing-key store (Vec<SigningKeyEntry>
    // from GET /2/users/{id}/public_keys) and enable the key cache
    chat.set_signing_keys(participant_signing_keys);
    chat.set_cache_keys(true);

    // Initial load: batch decrypt — per-event failures land in result.errors
    let result = chat.decrypt_events(&all_events_b64, &[]);

    // Live traffic: a rotation enters the key cache only after its
    // signature verifies, which is what decrypt_events does
    if let Some(kc) = key_change_b64.as_deref() {
        chat.decrypt_events(&[kc], &[]);
    }
    let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    // Once per instance: fill the signing-key store ([]SigningKeyEntry
    // from GET /2/users/{id}/public_keys) and enable the key cache
    if err := chat.SetSigningKeys(participantSigningKeys); err != nil {
        log.Fatal(err)
    }
    chat.SetCacheKeys(true)

    // Initial load: batch decrypt — per-event failures land in result.Errors
    result, err := chat.DecryptEvents(allEventsB64, nil)
    if err != nil {
        log.Fatal(err)
    }
    for _, dm := range result.Messages {
        if dm.Event.Type == "Message" {
            fmt.Println(dm.Event.AsMessage().Text())
        }
    }

    // Live traffic: a rotation enters the key cache only after its
    // signature verifies, which is what DecryptEvents does
    if keyChange != "" {
        chat.DecryptEvents([]string{keyChange}, nil)
    }
    event, err := chat.DecryptEvent(encodedEvent, nil, nil)
    if err == nil && event.Type == "Message" {
        fmt.Println(event.AsMessage().Text())
    }
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    // Once per instance: fill the signing-key store (SigningKeyEntry list
    // from GET /2/users/{id}/public_keys) and enable the key cache
    chat.SetSigningKeys(participantSigningKeys);
    chat.SetCacheKeys(true);

    // Initial load: batch decrypt — per-event failures land in result.Errors
    var result = chat.DecryptEvents(allEventsB64);
    foreach (var dm in result.Messages)
    {
        if (dm.Event.GetProperty("type").GetString() == "Message")
            Console.WriteLine(dm.Event.GetProperty("content").GetProperty("text").GetString());
    }

    // Live traffic: a rotation enters the key cache only after its
    // signature verifies, which is what DecryptEvents does
    if (!string.IsNullOrEmpty(keyChangeB64))
        chat.DecryptEvents(new[] { keyChangeB64 });
    var evt = chat.DecryptEvent(encodedEvent);  // throws on failure
    if (evt.GetProperty("type").GetString() == "Message")
        Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString());
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    // Once per instance: fill the signing-key store (SigningKeyEntry list
    // from GET /2/users/{id}/public_keys) and enable the key cache
    chat.setSigningKeys(participantSigningKeys);
    chat.setCacheKeys(true);

    // Initial load: batch decrypt — per-event failures land in result.errors
    DecryptEventsResult result = chat.decryptEvents(allEventsB64, null);
    for (DecryptedMessage dm : result.messages) {
        if ("Message".equals(dm.event.path("type").asText())) {
            System.out.println(dm.event.path("content").path("text").asText());
        }
    }

    // Live traffic: a rotation enters the key cache only after its
    // signature verifies, which is what decryptEvents does
    if (keyChangeB64 != null && !keyChangeB64.isEmpty()) {
        chat.decryptEvents(List.of(keyChangeB64), null);
    }
    JsonNode evt = chat.decryptEvent(encodedEvent, (Map<String, byte[]>) null, null);
    if ("Message".equals(evt.path("type").asText())) {
        System.out.println(evt.path("content").path("text").asText());
    }
    ```
  </Tab>
</Tabs>

<Note>
  **서버리스나 멀티 인스턴스인가요?** 서명 키 저장소와 키 캐시는 SDK 인스턴스의 메모리에 있습니다. 이 방식이 맞지 않는 경우—한 호출이 복호화하고 다른 호출이 전송하는—키를 명시적으로 전달하세요: `decrypt_events(events, signing_keys)`, `decrypt_event(event_b64, conversation_keys, signing_keys)`, 그리고 encrypt 메서드의 `conversation_key`/`conversation_key_version` 오버라이드를 사용하세요. `decrypt_events`가 반환하는 `conversation_keys`는 직접 저장하여 다시 전달하세요.
</Note>

모든 언어에 대한 완전한 폴링 및 답장 봇: [chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples).

***

## 모범 사례

* 서명 키 저장소를 최신 상태로 유지하세요: 발신자가 새 키 버전을 등록할 때 전체 참여자 세트로 `set_signing_keys`를 다시 호출하고, 서명 검증 실패 시 새로 고치세요
* 실시간 전달은 `event_uuid`로 중복 제거하세요
