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

# Real-time X Chat Events

> Receive chat.received, chat.sent, and other encrypted X Chat activity via webhooks or the activity stream, then decrypt payloads with the Chat XDK.

X delivers **`chat.received`**, **`chat.sent`**, and related X Chat activity with **ciphertext** in the payload. Decrypt with the [Chat XDK](/xchat/xchat-xdk).

| Layer              | Role                                                                                                                                |
| :----------------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| **X Activity API** | `GET /2/activity/stream`; `POST` / `GET` / `PUT` / `DELETE` `/2/activity/subscriptions` (see OpenAPI security per operation)        |
| **Webhooks**       | Optional `POST` / `GET` `/2/webhooks` and `PUT` / `DELETE` `/2/webhooks/{webhook_id}` routes if you terminate on your own HTTPS URL |
| **Chat XDK**       | `extract_conversation_keys`, `decrypt_event` / `decrypt_events`                                                                     |

Private X Chat event types need authorization for the user you monitor. Encrypted X Chat file attachments use **`media_hash_key`** and X Chat media download—not Post API `expansions=attachments.media_keys` / `media.fields=variants`.

***

## Event types

| Event                    | When                                         |
| :----------------------- | :------------------------------------------- |
| `chat.received`          | Subscribed user receives an encrypted DM     |
| `chat.sent`              | Subscribed user sends an encrypted DM        |
| `chat.conversation_join` | Subscribed user joins a group (when offered) |

***

## 1. Choose delivery

**Activity stream (often simplest for bots):** `GET /2/activity/stream` with an app Bearer token (optional `backfill_minutes`, `start_time`, `end_time` per OpenAPI). Filter client-side for `chat.received` / `chat.sent`.

**Activity subscriptions:** manage durable subscriptions with:

* `POST /2/activity/subscriptions` — create
* `GET /2/activity/subscriptions` — list (paginated)
* `PUT /2/activity/subscriptions/{subscription_id}` — update
* `DELETE /2/activity/subscriptions/{subscription_id}` or `DELETE /2/activity/subscriptions?ids=` — delete

Request bodies and required scopes are defined in the OpenAPI operation for each route. Creating an X Activity API (XAA) subscription requires **user-context authorization** (OAuth 2.0 user context with the relevant scopes, such as `dm.read` for chat events) for the user whose activity you monitor.

**Webhooks:** if you terminate events on your HTTPS endpoint, register a webhook with `POST /2/webhooks`, pass CRC challenges, then create your activity subscriptions with `POST /2/activity/subscriptions`, referencing your `webhook_id` (see Webhooks and Activity operations in OpenAPI). Python/TypeScript XDK may expose helpers for webhooks and activity when your SDK version includes them.

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

Subscribe to `chat.sent` as well if you need outbound copies. Other languages: call the same `/2/activity/*` HTTPS routes directly (user-context token to create subscriptions, app Bearer token for the stream).

***

## 2. CRC (webhooks only)

If you use webhooks, respond to Challenge-Response Checks (GET `crc_token`) with HMAC-SHA256 of the token using your consumer secret, in the JSON shape your webhook product expects (typically `sha256=<base64>`).

***

## 3. Decrypt with the Chat XDK

Live fields: **`payload.encoded_event`**, optional **`payload.conversation_key_change_event`**. Deduplicate on **`event_uuid`**.

JavaScript uses camelCase event types (`message`); other bindings use `"Message"` and snake\_case fields.

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

    chat = Chat(JUICEBOX_CONFIG_JSON)
    chat.unlock("YOUR_PASSCODE")
    chat.set_key_version(SIGNING_KEY_VERSION)
    conversation_keys = {}

    def signing_keys(user_id: str):
        resp = api_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
        ]

    data = body.get("data") or {}
    if data.get("event_type") in ("chat.received", "chat.sent"):
        p = data.get("payload") or {}
        cid = p.get("conversation_id")
        if p.get("conversation_key_change_event"):
            conversation_keys[cid] = chat.extract_conversation_keys(
                [p["conversation_key_change_event"]]
            )["keys"]
        ev = chat.decrypt_event(
            p["encoded_event"],
            conversation_keys.get(cid, {}),
            signing_keys(p["sender_id"]),
        )
    ```
  </Tab>

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

    const chat = await createChat({
      juiceboxConfig: JUICEBOX_CONFIG_JSON,
      getAuthToken: async (realmId) => getRealmToken(realmId),
    });
    await chat.unlock('YOUR_PASSCODE');
    chat.setKeyVersion(SIGNING_KEY_VERSION);
    const conversationKeys = new Map<string, Record<string, Uint8Array>>();

    async function signingKeys(userId: string) {
      const resp = await apiClient.chat.getUserPublicKeys(userId, {
        publicKeyFields: [
          'public_key_version', 'public_key', 'signing_public_key', 'identity_public_key_signature',
        ],
      });
      return resp.data.map((r: any) => ({
        userId,
        publicKeyVersion: r.public_key_version,
        publicKey: r.signing_public_key,
        identityPublicKey: r.public_key,
        identityPublicKeySignature: r.identity_public_key_signature,
      }));
    }

    const data = body?.data ?? {};
    if (data.event_type === 'chat.received' || data.event_type === 'chat.sent') {
      const p = data.payload ?? {};
      const cid = p.conversation_id as string;
      if (p.conversation_key_change_event) {
        conversationKeys.set(
          cid,
          chat.extractConversationKeys([p.conversation_key_change_event]).keys,
        );
      }
      const ev = chat.decryptEvent(
        p.encoded_event,
        conversationKeys.get(cid) ?? {},
        await signingKeys(p.sender_id),
      );
    }
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // chat: ChatCore or Chat, already unlocked / keys imported
    if let Some(kc) = key_change.as_deref() {
        let extracted = chat.extract_conversation_keys(&[kc]);
        conv_keys.extend(extracted.keys);
    }
    // sender_signing_keys from GET /2/users/{sender_id}/public_keys
    let event = chat.decrypt_event(&encoded_event, &conv_keys, &sender_signing_keys)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    if keyChange != "" {
        extracted, _ := chat.ExtractConversationKeys([]string{keyChange})
        for v, k := range extracted.Keys {
            convKeys[v] = k
        }
    }
    event, err := chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys)
    if err == nil && event.Type == "Message" {
        fmt.Println(event.AsMessage().Text())
    }
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    if (!string.IsNullOrEmpty(keyChangeB64))
    {
        var extracted = chat.ExtractConversationKeys(new[] { keyChangeB64 });
        foreach (var kv in extracted.Keys)
            convKeys[kv.Key] = kv.Value;
    }
    var evt = chat.DecryptEvent(encodedEvent, convKeys, senderSigningKeys);
    if (evt.GetProperty("type").GetString() == "Message")
        Console.WriteLine(evt.GetProperty("content").GetProperty("text").GetString());
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    if (keyChangeB64 != null && !keyChangeB64.isEmpty()) {
        var extracted = chat.extractConversationKeys(List.of(keyChangeB64));
        convKeys.putAll(extracted.keys);
    }
    JsonNode evt = chat.decryptEvent(encodedEvent, convKeys, senderSigningKeys);
    if ("Message".equals(evt.path("type").asText())) {
        System.out.println(evt.path("content").path("text").asText());
    }
    ```
  </Tab>
</Tabs>

History: [`GET /2/chat/conversations/{id}/events`](/x-api/chat/get-chat-conversation-events) + **`decrypt_events`** — see [Getting Started](/xchat/getting-started#6-receive-and-decrypt).

***

## Payload shape (live)

```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"
    }
  }
}
```

***

## Practices

* Verify webhook signatures per platform requirements
* Cache conversation keys and sender public keys
* Apply key-change blobs before decrypting dependent messages
* Deduplicate on `event_uuid`
