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

# 실시간 X Chat 이벤트

> 웹훅 또는 활동 스트림으로 chat.received, chat.sent 등 암호화된 X Chat 활동을 수신하고 Chat XDK로 페이로드를 복호화합니다.

X는 페이로드에 **암호문**을 포함하여 **`chat.received`**, **`chat.sent`** 및 관련 X Chat 활동을 전달합니다. [Chat XDK](/xchat/xchat-xdk)로 복호화하세요.

| 계층                 | 역할                                                                                                          |
| :----------------- | :---------------------------------------------------------------------------------------------------------- |
| **X Activity API** | `GET /2/activity/stream`; `POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions` (작업별 OpenAPI 보안 참조) |
| **웹훅**             | 자체 HTTPS URL에서 종료하는 경우 선택적 `POST` / `GET` `/2/webhooks` 및 `PUT` / `DELETE` `/2/webhooks/{webhook_id}` 경로    |
| **Chat XDK**       | `decrypt_event` / `decrypt_events`, 그리고 `set_signing_keys` / `set_cache_keys` 세션 저장소                        |

비공개 X Chat 이벤트 유형은 모니터링하는 사용자에 대한 승인이 필요합니다. 암호화된 X Chat 파일 첨부 파일은 \*\*`media_hash_key`\*\*와 X Chat 미디어 다운로드를 사용합니다—Post API의 `expansions=attachments.media_keys` / `media.fields=variants`가 아닙니다.

***

## 이벤트 유형

| 이벤트                      | 시점                           |
| :----------------------- | :--------------------------- |
| `chat.received`          | 구독된 사용자가 암호화된 DM을 수신할 때      |
| `chat.sent`              | 구독된 사용자가 암호화된 DM을 보낼 때       |
| `chat.conversation_join` | 구독된 사용자가 그룹에 참여할 때 (제공되는 경우) |

***

## 1. 전달 방식 선택

**활동 스트림 (봇에 종종 가장 간단함):** 앱 Bearer 토큰과 함께 `GET /2/activity/stream`을 사용합니다 (OpenAPI에 따라 선택적 `backfill_minutes`, `start_time`, `end_time`). 클라이언트 측에서 `chat.received` / `chat.sent`를 필터링하세요.

**활동 구독:** 다음으로 지속적인 구독을 관리합니다:

* `POST /2/activity/subscriptions` — 생성
* `GET /2/activity/subscriptions` — 목록 (페이지네이션됨)
* `PUT /2/activity/subscriptions/{subscription_id}` — 업데이트
* `DELETE /2/activity/subscriptions/{subscription_id}` 또는 `DELETE /2/activity/subscriptions?ids=` — 삭제

요청 본문과 필요한 스코프는 각 경로의 OpenAPI 작업에 정의되어 있습니다. X Activity API (XAA) 구독을 생성하려면 모니터링하는 사용자의 활동에 대한 **사용자 컨텍스트 승인**(관련 스코프를 포함한 OAuth 2.0 사용자 컨텍스트, 예: 채팅 이벤트의 경우 `dm.read`)이 필요합니다.

**웹훅:** HTTPS 엔드포인트에서 이벤트를 종료하는 경우, `POST /2/webhooks`로 웹훅을 등록하고 CRC 챌린지를 통과한 다음 `webhook_id`를 참조하여 `POST /2/activity/subscriptions`로 활동 구독을 생성합니다 (OpenAPI의 Webhooks 및 Activity 작업 참조). Python/TypeScript XDK는 해당 SDK 버전에 포함된 경우 웹훅과 활동을 위한 헬퍼를 노출할 수 있습니다.

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

    # Stream (app token) — exact helper names depend on your XDK version
    stream_client = Client(bearer_token="YOUR_BEARER_TOKEN")
    # for event in stream_client.activity.stream():
    #     handle_payload(event)  # see "Decrypt with the Chat XDK" below

    # Or create a subscription — requires user-context auth for the monitored user
    client = Client(access_token="YOUR_OAUTH2_USER_TOKEN")
    client.activity.create_subscription({
        "event_type": "chat.received",
        "filter": {"user_id": "USER_ID_TO_MONITOR"},
    })
    ```
  </Tab>

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

    // Creating a subscription requires user-context auth for the monitored user
    const client = new Client({ accessToken: 'YOUR_OAUTH2_USER_TOKEN' });
    await client.activity.createSubscription({
      event_type: 'chat.received',
      filter: { user_id: 'USER_ID_TO_MONITOR' },
    });
    // Stream: client.activity.stream() when available in your SDK version
    ```
  </Tab>
</Tabs>

발신 사본이 필요한 경우 `chat.sent`도 구독하세요. 다른 언어: 동일한 `/2/activity/*` HTTPS 경로를 직접 호출하세요 (구독 생성에는 사용자 컨텍스트 토큰, 스트림에는 앱 Bearer 토큰).

***

## 2. CRC (웹훅 전용)

웹훅을 사용하는 경우, 소비자 시크릿을 사용한 토큰의 HMAC-SHA256으로 챌린지-응답 검사(GET `crc_token`)에 응답하세요. 이는 웹훅 제품이 기대하는 JSON 형태(일반적으로 `sha256=<base64>`)여야 합니다.

***

## 3. Chat XDK로 복호화

실시간 필드: **`payload.encoded_event`**, 선택적 **`payload.conversation_key_change_event`**. \*\*`event_uuid`\*\*로 전달 중복을 제거하고, 복호화된 이벤트에 포함된 \*\*`message_id`\*\*로 메시지 중복을 제거하세요—`message_id`는 서명된 콘텐츠의 일부인 반면, 시퀀스 ID는 백엔드가 할당하는 서명되지 않은 메타데이터입니다.

아래 스니펫은 가장 짧은 핸들러를 위해 두 개의 **선택적** 세션 저장소를 사용합니다: `set_signing_keys`는 참가자의 공개 키(한 번만 [public-keys 엔드포인트](/x-api/chat/get-user-public-keys)에서 가져옴)를 보관하고, `set_cache_keys(true)`는 각 대화의 검증된 키를 유지하므로 `decrypt_event`는 이벤트만 있으면 됩니다. 페이로드가 `conversation_key_change_event`를 포함하는 경우 먼저 `decrypt_events`로 통과시키세요: 그러면 키 변경이 검증되고, 캐싱이 켜져 있으면 해당 키가 `decrypt_event` 호출을 위해 유지됩니다. 인스턴스 상태를 두지 않는 편을 선호하시나요? 대신 호출마다 키를 전달하세요—이 섹션 끝의 참고를 확인하세요.

JavaScript는 camelCase 이벤트 유형(`message`)을 사용합니다; 다른 바인딩은 `"Message"`와 snake\_case 필드를 사용합니다.

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    # chat has keys loaded and set_identity called (see Getting Started)
    chat.set_cache_keys(True)
    chat.set_signing_keys(participant_signing_keys)  # all participants, from the public-key routes

    data = body.get("data") or {}
    if data.get("event_type") in ("chat.received", "chat.sent"):
        p = data.get("payload") or {}
        if p.get("conversation_key_change_event"):
            # Verify the key change and retain its key in the cache
            chat.decrypt_events([p["conversation_key_change_event"]])
        ev = chat.decrypt_event(p["encoded_event"])
        if ev["type"] == "Message":
            print(ev["sender_id"], ev["content"]["text"])
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    // chat has keys loaded and setIdentity called (see Getting Started)
    chat.setCacheKeys(true);
    chat.setSigningKeys(participantSigningKeys); // all participants, from the public-key routes

    const data = body?.data ?? {};
    if (data.event_type === 'chat.received' || data.event_type === 'chat.sent') {
      const p = data.payload ?? {};
      if (p.conversation_key_change_event) {
        // Verify the key change and retain its key in the cache
        chat.decryptEvents([p.conversation_key_change_event]);
      }
      const ev = chat.decryptEvent(p.encoded_event);
      if (ev.type === 'message') {
        console.log(ev.senderId, ev.content.text);
      }
    }
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // chat has keys loaded and set_identity called (see Getting Started)
    chat.set_cache_keys(true);
    chat.set_signing_keys(participant_signing_keys); // all participants

    if let Some(kc) = key_change.as_deref() {
        // Verify the key change and retain its key in the cache
        let _ = chat.decrypt_events(&[kc], &[]);
    }
    // Decrypt with the cached conversation key; verify against the stored signing keys
    let event = chat.decrypt_event(&encoded_event, &Default::default(), &[])?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    // chat has keys loaded and SetIdentity called (see Getting Started)
    chat.SetCacheKeys(true)
    _ = chat.SetSigningKeys(participantSigningKeys) // all participants

    if keyChange != "" {
        // Verify the key change and retain its key in the cache
        _, _ = chat.DecryptEvents([]string{keyChange}, nil)
    }
    // Decrypt with the cached conversation key; verify against the stored signing keys
    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}
    // chat has keys loaded and SetIdentity called (see Getting Started)
    chat.SetCacheKeys(true);
    chat.SetSigningKeys(participantSigningKeys); // all participants

    if (!string.IsNullOrEmpty(keyChangeB64))
    {
        // Verify the key change and retain its key in the cache
        chat.DecryptEvents(new[] { keyChangeB64 });
    }
    // Decrypt with the cached conversation key; verify against the stored signing keys
    var evt = chat.DecryptEvent(encodedEvent);
    if (evt.GetProperty("type").GetString() == "Message")
        Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString());
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    // chat has keys loaded and setIdentity called (see Getting Started)
    chat.setCacheKeys(true);
    chat.setSigningKeys(participantSigningKeys); // all participants

    if (keyChangeB64 != null && !keyChangeB64.isEmpty()) {
        // Verify the key change and retain its key in the cache
        chat.decryptEvents(List.of(keyChangeB64), null);
    }
    // Decrypt with the cached conversation key; verify against the stored signing keys
    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>

키 맵을 직접 관리하려면 `extract_conversation_keys`가 `conversation_key_change_event`에서 키를 복호화하고, `decrypt_event`는 이를 (그리고 발신자의 서명 키를) 명시적 인자로 받아들입니다—명시적으로 전달된 비어 있지 않은 인자는 언제나 저장소보다 우선합니다.

이력: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — [시작하기](/xchat/getting-started#6-receive-and-decrypt)를 참조하세요.

***

## 페이로드 형태 (실시간)

```json theme={null}
{
  "data": {
    "event_type": "chat.received",
    "event_uuid": "0f52b591-4b7e-4f13-92cd-30e6b2a3f18a",
    "payload": {
      "conversation_id": "1215441834412953600-1843439638876491776",
      "sender_id": "1843439638876491776",
      "encoded_event": "BASE64_ENCODED_MESSAGE_EVENT",
      "conversation_key_version": "1782945126642",
      "conversation_key_change_event": "BASE64_ENCODED_KEY_CHANGE_EVENT"
    }
  }
}
```

***

## 관행

* 플랫폼 요구 사항에 따라 웹훅 서명을 검증하세요
* 세션 저장소를 한 번만 설정하세요: 모든 참가자에 대해 `set_signing_keys`, 대화 키에 대해 `set_cache_keys(true)`
* 의존 메시지를 복호화하기 전에 (`decrypt_events`를 통해) 키 변경 blob을 적용하세요
* `event_uuid`로 전달 중복을 제거하고, 서명된 `message_id`로 메시지 중복을 제거하세요
