> ## 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 イベント

> Webhook またはアクティビティストリーム経由で 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 security を参照） |
| **Webhooks**       | 独自の 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 ファイル添付は、Post API の `expansions=attachments.media_keys` / `media.fields=variants` ではなく、**`media_hash_key`** と X Chat メディアダウンロードを使用します。

***

## イベントタイプ

| イベント                     | いつ                           |
| :----------------------- | :--------------------------- |
| `chat.received`          | サブスクライブされたユーザーが暗号化された DM を受信 |
| `chat.sent`              | サブスクライブされたユーザーが暗号化された DM を送信 |
| `chat.conversation_join` | サブスクライブされたユーザーがグループに参加（提供時）  |

***

## 1. 配信方法を選択する

**アクティビティストリーム（ボットには多くの場合最もシンプル）:** アプリの Bearer トークンで `GET /2/activity/stream`（オプションの `backfill_minutes`、`start_time`、`end_time` は OpenAPI ごと）。クライアント側で `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）サブスクリプションの作成には、監視対象ユーザーに対する**ユーザーコンテキスト認可**（`dm.read` などチャットイベントに関連するスコープを持つ OAuth 2.0 ユーザーコンテキスト）が必要です。

**Webhook:** HTTPS エンドポイントでイベントを終端する場合、`POST /2/webhooks` で Webhook を登録し、CRC チャレンジをパスし、その後 `POST /2/activity/subscriptions` で `webhook_id` を参照してアクティビティサブスクリプションを作成します（OpenAPI の Webhooks および Activity オペレーションを参照）。Python/TypeScript XDK は、SDK のバージョンに含まれる場合、Webhook とアクティビティのヘルパーを公開することがあります。

<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（Webhook のみ）

Webhook を使用する場合、コンシューマシークレットを使ったトークンの HMAC-SHA256 を、Webhook 製品が期待する JSON 形式（通常は `sha256=<base64>`）で Challenge-Response Checks（GET `crc_token`）に応答してください。

***

## 3. Chat XDK で復号する

ライブフィールド: **`payload.encoded_event`**、オプションの **`payload.conversation_key_change_event`**。**`event_uuid`** で配信の重複を排除します；メッセージは、復号済みイベントに含まれる **`message_id`** で重複を排除します — これは署名済みコンテンツの一部です。一方、シーケンス ID はバックエンドが割り当てる、署名されていないメタデータです。

以下のスニペットは、最短のハンドラを実現するために 2 つの**オプションの**セッションストアを使用します: `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`** — [Getting Started](/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"
    }
  }
}
```

***

## プラクティス

* プラットフォームの要件に従って Webhook 署名を検証する
* セッションストアを一度だけ設定する: 参加者全員に対して `set_signing_keys`、会話鍵には `set_cache_keys(true)`
* 依存するメッセージを復号する前に、鍵変更 blob を（`decrypt_events` 経由で）適用する
* `event_uuid` で配信を、署名済み `message_id` でメッセージを重複排除する
