---
title: X
description: X (Twitter) adapter using the X API v2 and the X Activity API.
tagline: Reply to public mentions and hold direct message conversations on X.
package: @chat-adapter/x
---

# X



## Install

<PackageInstall package="@chat-adapter/x @chat-adapter/state-memory" />

## Quick start

<Callout type="info">
  The adapter auto-detects `X_CONSUMER_SECRET`, `X_USER_ACCESS_TOKEN`, `X_USER_ID`, and `X_USERNAME` from the environment.
</Callout>

```typescript title="lib/bot.ts" lineNumbers
import { Chat } from "chat";
import { createXAdapter } from "@chat-adapter/x";
import { createMemoryState } from "@chat-adapter/state-memory";

const bot = new Chat({
  userName: "mybot",
  adapters: {
    x: createXAdapter(),
  },
  state: createMemoryState(),
});

bot.onNewMention(async (thread, message) => {
  await thread.post(`Hi @${message.author.userName}!`);
});

bot.onDirectMessage(async (thread, message) => {
  await thread.post("Hello from X!");
});
```

The memory state adapter keeps subscriptions and locks in process memory, which suits local development. Use [Redis](/adapters/official/redis) or [PostgreSQL](/adapters/official/postgres) in production.

```typescript title="app/api/webhooks/x/route.ts" lineNumbers
import { after } from "next/server";
import { bot } from "@/lib/bot";

export async function GET(request: Request) {
  return bot.webhooks.x(request);
}

export async function POST(request: Request) {
  return bot.webhooks.x(request, {
    waitUntil: (task) => after(() => task),
  });
}
```

## X adapter vs XChat adapter

The [`@chat-adapter/x`](https://www.npmjs.com/package/@chat-adapter/x) package contains two adapters for different X surfaces. This page covers the X adapter. For encrypted conversations, use the [XChat adapter](/adapters/official/xchat).

|                                     | X adapter                                                                                             | XChat adapter                                                               |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Factory                             | `createXAdapter` from `@chat-adapter/x`                                                               | `createXchatAdapter` from `@chat-adapter/x/chat`                            |
| Conversations                       | Public timeline mentions and replies (`post.mention.create`), and classic unencrypted direct messages | Encrypted XChat 1:1 and group conversations                                 |
| Top-level posts                     | Posts from the bot account to the public timeline (`x:public`)                                        | Not supported                                                               |
| Reactions                           | Likes only                                                                                            | Emoji reactions                                                             |
| Typing indicators and read receipts | Not supported                                                                                         | Supported                                                                   |
| Encryption                          | None                                                                                                  | Handled inside the adapter: Juicebox PIN, key exchange, and signed messages |

You can register both adapters on the same `Chat` instance if your bot needs public posts and DMs as well as encrypted XChat.

## Platform setup

### 1. Create an X app

1. Go to the [X developer portal](https://developer.x.com) and create a Project and App.
2. Under **Keys and tokens**, copy the **API Key Secret** to `X_CONSUMER_SECRET`.
3. Enable **OAuth 2.0** user authentication with the scopes `tweet.read`, `tweet.write`, `users.read`, `dm.read`, `dm.write`, `like.write`, `media.write`, and `offline.access`.
4. Complete the OAuth 2.0 flow for the bot account, then store the resulting tokens as described in [Authentication](#authentication).

### 2. Register a webhook and subscriptions

X delivers events through the [X Activity API](https://docs.x.com/x-api/activity/introduction). Set this up once in the [X developer console](https://console.x.com), which handles the auth for you:

1. Register your webhook URL (`https://your-domain.com/api/webhooks/x`). It must be publicly reachable over HTTPS and cannot include a port. X sends a CRC challenge immediately and revalidates hourly; the adapter answers both automatically.
2. Create subscriptions for the events the adapter consumes: `post.mention.create`, `dm.received`, and `dm.sent`. These are private events, so the bot user must have authorized your app first.

<Callout type="info">
  Subscription and webhook management is one-time setup; the adapter does not do it at runtime. If you script it instead of using the console, be aware that the Activity API endpoints are auth-picky and operation-specific (creating a private-event subscription needed OAuth 1.0a user context in testing, while list and delete used the app-only bearer token), and do not fully match the published spec. The console avoids all of this.
</Callout>

## Authentication

X OAuth 2.0 user tokens expire after about two hours, so the adapter supports two ways to supply them. Use managed token refresh for long-running bots.

### Managed token refresh

Store `X_CLIENT_ID` and `X_REFRESH_TOKEN` (plus `X_CLIENT_SECRET` for confidential clients). The refresh token requires the `offline.access` scope. The adapter refreshes access tokens before expiry and persists the rotated refresh token in the state adapter, encrypted when `encryptionKey` (`X_ENCRYPTION_KEY`) is set.

### User access token

Store the access token as `X_USER_ACCESS_TOKEN`, or pass `userAccessToken` as an async function that returns a token when you run your own refresh flow. A static token stops working when it expires.

## Configuration

<TypeTable
  type={{
  consumerSecret: {
    type: "string",
    description:
      "App API key secret used for webhook CRC responses and `x-twitter-webhooks-signature` verification. Auto-detected from `X_CONSUMER_SECRET`.",
  },
  userAccessToken: {
    type: "string | () => Promise<string>",
    description:
      "OAuth 2.0 user-context access token, or an async provider for custom refresh flows. Auto-detected from `X_USER_ACCESS_TOKEN`. Optional when managed refresh is configured.",
  },
  clientId: {
    type: "string",
    description:
      "OAuth 2.0 client ID. Enables managed token refresh together with `refreshToken`. Auto-detected from `X_CLIENT_ID`.",
  },
  refreshToken: {
    type: "string",
    description:
      "OAuth 2.0 refresh token (requires the `offline.access` scope). The adapter refreshes access tokens before expiry and persists the rotated refresh token in the state adapter. Auto-detected from `X_REFRESH_TOKEN`.",
  },
  clientSecret: {
    type: "string",
    description:
      "OAuth 2.0 client secret for confidential clients. Auto-detected from `X_CLIENT_SECRET`.",
  },
  encryptionKey: {
    type: "string",
    description:
      "Base64 32-byte AES-256-GCM key used to encrypt persisted OAuth tokens. Auto-detected from `X_ENCRYPTION_KEY`.",
  },
  userId: {
    type: "string",
    description:
      "Bot account user ID, used for DM threading and self-detection. Auto-detected from `X_USER_ID`, otherwise fetched from `GET /2/users/me`. The adapter requires a resolvable bot id and fails initialization if neither is available.",
  },
  userName: {
    type: "string",
    description:
      "Bot account handle used for mention detection. Auto-detected from `X_USERNAME`, otherwise fetched from `GET /2/users/me`.",
  },
  apiBaseUrl: {
    type: "string",
    default: '"https://api.x.com"',
    description: "Override the X API base URL. Auto-detected from `X_API_BASE_URL`.",
  },
}}
/>

### Environment variables

| Variable              | Required                                  | Description                                                             |
| --------------------- | ----------------------------------------- | ----------------------------------------------------------------------- |
| `X_CONSUMER_SECRET`   | Yes                                       | App API key secret for CRC responses and webhook signature verification |
| `X_USER_ACCESS_TOKEN` | Yes, unless managed refresh is configured | OAuth 2.0 user access token for the bot account                         |
| `X_CLIENT_ID`         | For managed refresh                       | OAuth 2.0 client ID                                                     |
| `X_REFRESH_TOKEN`     | For managed refresh                       | OAuth 2.0 refresh token                                                 |
| `X_CLIENT_SECRET`     | No                                        | OAuth 2.0 client secret for confidential clients                        |
| `X_ENCRYPTION_KEY`    | No                                        | Base64 32-byte key used to encrypt persisted OAuth tokens               |
| `X_USER_ID`           | No                                        | Bot account user ID. Fetched from `GET /2/users/me` when unset          |
| `X_USERNAME`          | No                                        | Bot account handle. Fetched from `GET /2/users/me` when unset           |
| `X_API_BASE_URL`      | No                                        | X API base URL override                                                 |

## Webhooks

The route handles two kinds of request from X:

* GET CRC challenge: X sends a `crc_token`, and the adapter answers with `sha256=<base64 HMAC>` keyed by the consumer secret.
* POST event delivery: activity events signed with `x-twitter-webhooks-signature`, verified against the raw request body with a timing-safe comparison.

## Reply threading

Replies target the most recent mention received in the conversation, and consecutive replies chain under the bot's previous reply. Top-level posts go through `channel.post` on the `x:public` channel.

## Media uploads

Attach images by passing `files` (or `attachments`) on the message. The adapter uploads each image (png, jpeg, webp) through X's chunked media endpoints (`initialize` then `append` then `finalize`) and attaches the returned `media_id`s. Posts and DMs accept up to four images, with or without text.

```typescript
await thread.post({
  markdown: "France lead the title race",
  files: [{ data: pngBuffer, filename: "odds.png", mimeType: "image/png" }],
});
```

Media upload requires the `media.write` scope on the OAuth 2.0 token, alongside `tweet.write`. Without it, uploads fail with a 403.

## Reactions

Likes are the only reaction X exposes. `addReaction` accepts `emoji.heart` or `"like"` and maps to `POST /2/users/:id/likes`; anything else throws a `ValidationError`.

## Streaming

X has no native streaming and public post edits are eligibility-gated, so the adapter buffers the entire stream and posts once on completion instead of posting and then editing.

## Thread IDs

```
x:post:{conversationId}   # public post threads (channel: x:public)
x:dm:{participantUserId}  # direct message with a single user
```

Examples: `x:post:1943467279943467279`, `x:dm:783214`.

X DM webhooks carry no conversation id, only participant ids, so DMs are threaded by the other participant's user id. `openDM("783214")` returns `x:dm:783214`, and both replies and inbound events for that conversation share the same thread. Sends route to the by-participant endpoint (`POST /2/dm_conversations/with/{id}/messages`).

## Limitations

X enforces [automation rules](https://docs.x.com/developer-terms/policy) on bots: get explicit consent before automated replies or DMs, honor opt-outs immediately, disclose the bot identity in the account profile, and never send bulk or duplicate automated content.

## Feature support

<FeatureSupport />
