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

## 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";

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

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

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

```typescript title="app/api/webhooks/x/route.ts" lineNumbers
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);
}
```

## 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.",
  },
}}
/>

## Authentication

### 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`, and `offline.access`.
4. Complete the OAuth 2.0 flow for the bot account. Either store the access token as `X_USER_ACCESS_TOKEN`, or store `X_CLIENT_ID` plus `X_REFRESH_TOKEN` to let the adapter manage token refresh.

<Callout type="warn">
  X OAuth 2.0 user tokens expire after about two hours. For long-running bots, configure managed refresh (`clientId` + `refreshToken`): the adapter refreshes tokens before expiry and persists the rotated refresh token in the state adapter, encrypted when `encryptionKey` is set.
</Callout>

### 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, not something the adapter does at runtime. If you script it instead of using the console, note 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>

## Advanced

### Webhook flow

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

### 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 using post+edit updates.

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

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

### Thread ID format

```
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`).

### Automation policy

X enforces [automation rules](https://docs.x.com/developer-terms/policy): 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 />
