This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @chat-adapter/slack@4.36.0 ### Minor Changes -0153a39: Add `DateInput` and `NumberInput` modal children. The Slack adapter renders them as a `datepicker` and a `number_input`, the Teams adapter as `Input.Date` and `Input.Number`, and both submitted values arrive in `event.values` as strings. Teams submit values that arrive as JSON numbers are now stringified into `event.values` instead of being dropped. This fixes `Input.Number`, but applies to any numeric value a Teams dialog submits — a key that was previously absent from `event.values` will now be present as a string. ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## @chat-adapter/teams@4.36.0 ### Minor Changes -0153a39: Add `DateInput` and `NumberInput` modal children. The Slack adapter renders them as a `datepicker` and a `number_input`, the Teams adapter as `Input.Date` and `Input.Number`, and both submitted values arrive in `event.values` as strings. Teams submit values that arrive as JSON numbers are now stringified into `event.values` instead of being dropped. This fixes `Input.Number`, but applies to any numeric value a Teams dialog submits — a key that was previously absent from `event.values` will now be present as a string. ### Patch Changes -257a32d: Route Teams personal and group conversations using their explicit conversation type so group chats use buffered fallback even when their IDs resemble direct messages. -3c37cfb: Authenticate connector-hosted inline attachments and parse Teams file download cards. - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## @chat-adapter/x@4.36.0 ### Minor Changes -caa6325: Add XChat support to `@chat-adapter/x`, shipped from the new `@chat-adapter/x/chat` subpath so it sits alongside the existing X adapter. The XChat crypto stack (`@xdevplatform/chat-xdk`, `@xdevplatform/xdk`, `juicebox-sdk`) is an optional peer dependency, so existing `@chat-adapter/x` users are unaffected. All cryptography is handled inside the adapter via `@xdevplatform/chat-xdk` (wasm) and all REST goes through the typed `@xdevplatform/xdk` client. Only a bot token and a Juicebox PIN are required: the bot's identity (user id and @handle) is resolved from `GET /2/users/me` at startup. - Encrypted send/receive in DMs and groups (webhook push + polling), signature verification on by default; undecryptable or unverified events are dropped - Webhook POSTs must carry a valid `x-twitter-webhooks-signature`, which X sends on every delivery. Set `consumerSecret` (or `X_CONSUMER_SECRET`) to receive webhooks, or `disableWebhookVerification` when an upstream layer already verifies them. Polling deployments are unaffected - Mention detection from structured mention entities, swipe-replies to the bot, and a plain-text `@handle` fallback; group replies sent as quoted replies - `openDM(userId)` starts (or reuses) an encrypted 1:1, running a full key exchange when needed so the bot can message first - Media both ways: inbound attachments with lazy download+decrypt, outbound encrypted uploads - Edit and delete of the bot's own messages; the first edit of a fresh message is age-gated by `editSafetyDelayMs` (default 5000ms) so receiving clients have stored the original - Reactions in and out, read receipts (`sendReadReceipts`, default on), typing keep-alive, configurable group welcome message - Cards degrade to text with tappable URL/mention entities plus a URL preview attachment - Requests carry a `chat-sdk-xchat/<version>` User-Agent product token so Chat SDK traffic is identifiable in X API request logs (a User-Agent set via `apiHeaders` takes precedence) - Registered in the `chat/adapters` catalog and the `create-chat-sdk` CLI scaffold, with a new optional `importPath` catalog field for adapters that ship on a subpath ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## chat@4.36.0 ### Minor Changes -c5d86b1: confine built-in agent read tools to the conversation being handled, with an optional scope override -0153a39: Add `DateInput` and `NumberInput` modal children. The Slack adapter renders them as a `datepicker` and a `number_input`, the Teams adapter as `Input.Date` and `Input.Number`, and both submitted values arrive in `event.values` as strings. Teams submit values that arrive as JSON numbers are now stringified into `event.values` instead of being dropped. This fixes `Input.Number`, but applies to any numeric value a Teams dialog submits — a key that was previously absent from `event.values` will now be present as a string. ### Patch Changes -257a32d: Route Teams personal and group conversations using their explicit conversation type so group chats use buffered fallback even when their IDs resemble direct messages. -b547f45: Stop treating email addresses as bot mentions. A message containing `jane@acme.com` no longer triggers a bot named `acme`, because the `@` in `detectMention` must not follow a word character. Real mentions are unaffected, including at the start of a message, after punctuation, and suffixed names such as GitHub's `mybot[bot]`. -caa6325: Add XChat support to `@chat-adapter/x`, shipped from the new `@chat-adapter/x/chat` subpath so it sits alongside the existing X adapter. The XChat crypto stack (`@xdevplatform/chat-xdk`, `@xdevplatform/xdk`, `juicebox-sdk`) is an optional peer dependency, so existing `@chat-adapter/x` users are unaffected. All cryptography is handled inside the adapter via `@xdevplatform/chat-xdk` (wasm) and all REST goes through the typed `@xdevplatform/xdk` client. Only a bot token and a Juicebox PIN are required: the bot's identity (user id and @handle) is resolved from `GET /2/users/me` at startup. - Encrypted send/receive in DMs and groups (webhook push + polling), signature verification on by default; undecryptable or unverified events are dropped - Webhook POSTs must carry a valid `x-twitter-webhooks-signature`, which X sends on every delivery. Set `consumerSecret` (or `X_CONSUMER_SECRET`) to receive webhooks, or `disableWebhookVerification` when an upstream layer already verifies them. Polling deployments are unaffected - Mention detection from structured mention entities, swipe-replies to the bot, and a plain-text `@handle` fallback; group replies sent as quoted replies - `openDM(userId)` starts (or reuses) an encrypted 1:1, running a full key exchange when needed so the bot can message first - Media both ways: inbound attachments with lazy download+decrypt, outbound encrypted uploads - Edit and delete of the bot's own messages; the first edit of a fresh message is age-gated by `editSafetyDelayMs` (default 5000ms) so receiving clients have stored the original - Reactions in and out, read receipts (`sendReadReceipts`, default on), typing keep-alive, configurable group welcome message - Cards degrade to text with tappable URL/mention entities plus a URL preview attachment - Requests carry a `chat-sdk-xchat/<version>` User-Agent product token so Chat SDK traffic is identifiable in X API request logs (a User-Agent set via `apiHeaders` takes precedence) - Registered in the `chat/adapters` catalog and the `create-chat-sdk` CLI scaffold, with a new optional `importPath` catalog field for adapters that ship on a subpath ## @chat-adapter/discord@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## @chat-adapter/gchat@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## @chat-adapter/github@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## @chat-adapter/linear@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## @chat-adapter/messenger@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## @chat-adapter/shared@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 ## @chat-adapter/telegram@4.36.0 ### Patch Changes -53bf73d: Preserve Telegram stable media identifiers in normalized attachment metadata and report photo attachments as JPEG. - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## @chat-adapter/twilio@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## @chat-adapter/web@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## @chat-adapter/whatsapp@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 - @chat-adapter/shared@4.36.0 ## create-chat-sdk@0.2.1 ### Patch Changes -caa6325: Add XChat support to `@chat-adapter/x`, shipped from the new `@chat-adapter/x/chat` subpath so it sits alongside the existing X adapter. The XChat crypto stack (`@xdevplatform/chat-xdk`, `@xdevplatform/xdk`, `juicebox-sdk`) is an optional peer dependency, so existing `@chat-adapter/x` users are unaffected. All cryptography is handled inside the adapter via `@xdevplatform/chat-xdk` (wasm) and all REST goes through the typed `@xdevplatform/xdk` client. Only a bot token and a Juicebox PIN are required: the bot's identity (user id and @handle) is resolved from `GET /2/users/me` at startup. - Encrypted send/receive in DMs and groups (webhook push + polling), signature verification on by default; undecryptable or unverified events are dropped - Webhook POSTs must carry a valid `x-twitter-webhooks-signature`, which X sends on every delivery. Set `consumerSecret` (or `X_CONSUMER_SECRET`) to receive webhooks, or `disableWebhookVerification` when an upstream layer already verifies them. Polling deployments are unaffected - Mention detection from structured mention entities, swipe-replies to the bot, and a plain-text `@handle` fallback; group replies sent as quoted replies - `openDM(userId)` starts (or reuses) an encrypted 1:1, running a full key exchange when needed so the bot can message first - Media both ways: inbound attachments with lazy download+decrypt, outbound encrypted uploads - Edit and delete of the bot's own messages; the first edit of a fresh message is age-gated by `editSafetyDelayMs` (default 5000ms) so receiving clients have stored the original - Reactions in and out, read receipts (`sendReadReceipts`, default on), typing keep-alive, configurable group welcome message - Cards degrade to text with tappable URL/mention entities plus a URL preview attachment - Requests carry a `chat-sdk-xchat/<version>` User-Agent product token so Chat SDK traffic is identifiable in X API request logs (a User-Agent set via `apiHeaders` takes precedence) - Registered in the `chat/adapters` catalog and the `create-chat-sdk` CLI scaffold, with a new optional `importPath` catalog field for adapters that ship on a subpath ## @chat-adapter/state-ioredis@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 ## @chat-adapter/state-memory@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 ## @chat-adapter/state-pg@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 ## @chat-adapter/state-redis@4.36.0 ### Patch Changes - Updated dependencies [257a32d] - Updated dependencies [c5d86b1] - Updated dependencies [0153a39] - Updated dependencies [b547f45] - Updated dependencies [caa6325] - chat@4.36.0 ## @chat-adapter/tests@4.36.0 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@chat-adapter/x
npm package:
@chat-adapter/x
X (Twitter) adapter for Chat SDK, using the X API v2 and the X Activity API. Reply to public mentions, hold DM conversations, post from the bot account, and like posts.
Documentation: chat-sdk.dev/adapters/official/x · Guides: vercel.com/kb/chat-sdk
Installation
pnpm add @chat-adapter/x
Scaffold with the CLI
To scaffold a new X bot with this adapter preselected:
npx create-chat-sdk@latest my-bot --adapter x memory
Visit the adapters directory to see other available official and vendor-official adapters.
Usage
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!");
});
When using createXAdapter() without arguments, credentials are auto-detected from environment variables.
X setup
1. Create an X app
- Go to the X developer portal and create a Project and App
- Under Keys and tokens, copy the API Key Secret (consumer secret): this becomes
X_CONSUMER_SECRET - Enable OAuth 2.0 user authentication with the scopes
tweet.read,tweet.write,users.read,dm.read,dm.write,like.write, andoffline.access - Complete the OAuth 2.0 flow for the bot account. Either store the access token as
X_USER_ACCESS_TOKEN, or storeX_CLIENT_IDplusX_REFRESH_TOKENto let the adapter manage token refresh
2. Register a webhook
X delivers events through the X Activity API. Set this up once in the X developer console, which handles the auth for you:
- Register your webhook URL (
https://your-domain.com/api/webhooks/x). It must be public HTTPS without a port. X immediately sends a CRC challenge, which the adapter answers automatically - Create subscriptions for the events the adapter consumes:
post.mention.create,dm.received, anddm.sent(private events, so the bot user must have authorized your app first)
Subscription and webhook management is one-time setup, not adapter runtime. If you script it instead of using the console, the Activity API endpoints are auth-picky and operation-specific and do not fully match the published spec (creating a private-event subscription needed OAuth 1.0a user context in testing, while list and delete used the app-only bearer token), so the console is the simpler path.
3. Environment variables
X_CONSUMER_SECRET=... # App API key secret, used for webhook CRC and signature verification
# Auth option A: static access token
X_USER_ACCESS_TOKEN=... # OAuth 2.0 user-context access token for outbound calls
# Auth option B: managed OAuth refresh (recommended for long-running bots)
X_CLIENT_ID=... # OAuth 2.0 client ID
X_REFRESH_TOKEN=... # OAuth 2.0 refresh token (requires the offline.access scope)
X_CLIENT_SECRET=... # Optional, only for confidential clients
X_ENCRYPTION_KEY=... # Optional, base64 32-byte key to encrypt persisted tokens
X_USER_ID=... # Bot account user ID. Optional if omitted it is fetched from /2/users/me; the adapter requires a resolvable bot id and fails init otherwise
X_USERNAME=... # Optional, bot @handle for mention detection (fetched when omitted)
X_API_BASE_URL=... # Optional, override the X API base URL
Token refresh
X OAuth 2.0 user tokens are short-lived (about two hours). With X_CLIENT_ID and X_REFRESH_TOKEN set, the adapter refreshes the access token before expiry and persists the rotated refresh token in your state adapter, so the bot survives restarts. Set X_ENCRYPTION_KEY to store those tokens AES-256-GCM encrypted.
Alternatively, pass a token provider and plug in your own refresh logic:
import { createXAdapter } from "@chat-adapter/x";
const adapter = createXAdapter({
userAccessToken: async () => refreshTokenFromMyStore(),
});
Webhook setup
X uses two webhook mechanisms, both handled by the adapter:
- CRC challenge (GET): X sends a
crc_tokenthat the adapter answers with an HMAC-SHA256 response keyed by your consumer secret. X re-validates hourly - Event delivery (POST): activity events signed via the
x-twitter-webhooks-signatureheader, verified against the raw request body
// Next.js App Router example
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);
}
Features
Messaging
| Feature | Supported |
|---|---|
| Post message | Yes (mention replies and DMs) |
| Top-level posts | Yes (channel.post on x:public) |
| Edit message | Posts only (X edit eligibility rules apply) |
| Delete message | Posts and own DM events |
| Streaming | Buffered (accumulates then posts once) |
| Typing indicator | No |
Rich content
| Feature | Supported |
|---|---|
| Card format | Plain text fallback |
| Buttons | No (link buttons render as text) |
| Tables | ASCII |
| Modals | No |
| Image uploads | Yes (png, jpeg, webp; up to 4 per post; also DMs) |
Attach images by passing files (or attachments) on the message; the adapter uploads each through X's chunked media endpoints (initialize then append then finalize) and attaches the returned media_ids to the post or DM. A post can carry media with or without text.
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 your OAuth 2.0 token, in addition to tweet.write. Mint the token with media.write included or uploads fail with a 403.
Conversations
| Feature | Supported |
|---|---|
| Mentions | Yes (post.mention.create) |
| DMs | Yes (dm.received / dm.sent) |
| Reactions | Likes only (emoji.heart or "like") |
| User lookup | Yes |
Message history
| Feature | Supported |
|---|---|
| Fetch messages | DMs via API, posts from cache |
| Fetch single message | Posts via API, DMs from cache |
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 sends route to POST /2/dm_conversations/with/783214/messages. Top-level posts go through channel.post on the x:public channel.
Automation policy
X enforces automation rules. Before deploying a bot:
- get explicit consent before sending automated replies or DMs, and honor opt-outs immediately
- disclose the bot identity in the account profile
- never send bulk, duplicate, or aggressive automated content
- use only the official API (no scraping or browser automation)
The adapter is strict by default: it never streams by post-and-edit, rejects unsupported interactions loudly, and only replies where your handlers decide to.
XChat (encrypted messaging)
XChat is X's encrypted messaging product. It ships from the @chat-adapter/x/chat
subpath of this package, so it installs with @chat-adapter/x and adds no
dependencies unless you import it.
import { createXchatAdapter } from "@chat-adapter/x/chat";
The XChat crypto stack (@xdevplatform/chat-xdk, @xdevplatform/xdk,
juicebox-sdk) is declared as optional peer dependencies. Install them
alongside @chat-adapter/x when you use this subpath.
Usage
import { Chat } from "chat";
import { createXchatAdapter } from "@chat-adapter/x/chat";
import { createMemoryState } from "@chat-adapter/state-memory";
const bot = new Chat({
userName: "mybot",
adapters: {
xchat: createXchatAdapter(),
},
state: createMemoryState(),
});
// DMs always
bot.onDirectMessage(async (thread, message) => {
await thread.post(`You said: ${message.text}`);
});
// Group chats when the bot is @mentioned
bot.onNewMention(async (thread, message) => {
await thread.post(`You said: ${message.text}`);
});
Wire the webhook route (e.g. app/api/webhooks/xchat/route.ts in Next.js):
import { bot } from "@/lib/bot";
export async function GET(request: Request) {
return bot.webhooks.xchat(request);
}
export async function POST(request: Request) {
return bot.webhooks.xchat(request);
}
When using createXchatAdapter() without arguments, credentials are auto-detected from environment variables. The bot's @handle is resolved from GET /2/users/me at startup, so mention detection works without any hardcoding.
X Chat setup
1. Register encryption keys
The bot account needs registered public keys and a Juicebox-backed private-key store before it can participate in encrypted conversations:
- Generate keypairs with
chat-xdk(generateKeypairs()) - Register them via
POST /2/users/{id}/public_keys - Store the private keys in Juicebox with a PIN (
chat.setup(pin))
The adapter unlocks the keys at startup with the same PIN (XCHAT_PIN). Registration is a one-time step per account — see the chat-xdk documentation for the full flow.
2. Create a webhook
Register a webhook URL so the X Activity API can deliver events (docs):
curl -X POST "https://api.x.com/2/webhooks" \
-H "Authorization: Bearer $APP_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-domain.com/api/webhooks/xchat"}'
X validates the URL with a CRC challenge (see Webhook route below), so the endpoint must be live before you create the webhook.
Webhooks and activity subscriptions can also be created and managed in the X Developer Portal instead of via the API.
3. Subscribe to chat events
Create activity subscriptions for the bot user with the bot's OAuth2 user token:
curl -X POST "https://api.x.com/2/activity/subscriptions" \
-H "Authorization: Bearer $BOT_USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_type": "chat.received",
"filter": {"user_id": "YOUR_BOT_USER_ID"},
"tag": "bot-chat-received",
"webhook_id": "YOUR_WEBHOOK_ID"
}'
Subscribe to chat.conversation_join as well if you want the bot to post a welcome message when it is added to a group.
Webhook route
X sends two kinds of requests:
- CRC challenge (GET) —
?crc_token=...must be answered with an HMAC-SHA256 of the token, keyed by your app's consumer secret. Handle this at the route level; it needs no adapter state. - Event delivery (POST) — chat events, verified by the adapter via the
x-twitter-webhooks-signatureheader.
import { createHmac } from "node:crypto";
import { bot } from "@/lib/bot";
export async function GET(request: Request) {
const url = new URL(request.url);
const crcToken = url.searchParams.get("crc_token");
if (!crcToken) {
return new Response("Missing crc_token", { status: 400 });
}
const hash = createHmac("sha256", process.env.X_CONSUMER_SECRET!)
.update(crcToken)
.digest("base64");
return Response.json({ response_token: `sha256=${hash}` });
}
export async function POST(request: Request) {
return bot.webhooks.xchat(request);
}
Configuration
All options are auto-detected from environment variables when not provided.
| Option | Required | Description |
|---|---|---|
botToken / accessToken |
No* | OAuth2 user access token for the bot account. Auto-detected from XCHAT_BOT_TOKEN or X_ACCESS_TOKEN |
pin |
No | Juicebox PIN; when set, keys unlock automatically during initialize(). Auto-detected from XCHAT_PIN |
consumerSecret |
For webhooks | App consumer secret for webhook signature verification. Auto-detected from X_CONSUMER_SECRET. Required to receive webhooks: X signs every POST, so without it every POST is rejected with 401. Polling deployments do not need it |
disableWebhookVerification |
No | Accept webhook POSTs without verifying their HMAC. Auto-detected from X_DISABLE_WEBHOOK_VERIFICATION. Only use it when an upstream layer verifies the signature. Not recommended in production |
editSafetyDelayMs |
No | Minimum age (ms) a freshly posted message must reach before its first edit is sent, so receiving clients have stored the original the edit targets. Defaults to 5000; 0 disables the wait |
sendReadReceipts |
No | Send a read receipt for each delivered inbound message before handlers run (default true) |
userName |
No | Bot @handle for mention detection. Auto-detected from X_BOT_USERNAME, otherwise resolved from GET /2/users/me |
welcomeMessage |
No | Message posted when the bot joins a group. false disables; omitted uses a default that explains @mention-to-reply |
verifySignatures |
No | Require verifiable signatures on incoming messages (default true). X_VERIFY_SIGNATURES=false opts out |
signingKeyVersion |
No | Signing key version override. Normally fetched during initialize(). Auto-detected from X_SIGNING_KEY_VERSION |
apiBaseUrl |
No | Base URL for media REST calls (defaults to https://api.x.com) |
apiHeaders |
No | Extra headers on every X API request. Unless it includes a User-Agent, the adapter prepends chat-sdk-xchat/<version> to the client's default so Chat SDK traffic is identifiable |
logger |
No | Logger instance (defaults to ConsoleLogger("info")) |
*Required at runtime — either via config or environment variable.
Environment variables
XCHAT_BOT_TOKEN=xcbot_... # OAuth2 user token for the bot account
XCHAT_PIN=... # Juicebox PIN for key unlock
X_CONSUMER_SECRET=... # App secret (CRC + webhook signature verification)
X_BOT_USERNAME=... # Optional @handle override; resolved from /2/users/me otherwise
X_VERIFY_SIGNATURES=true # Optional; set false to accept unverifiable messages
Features
Messaging
| Feature | Supported |
|---|---|
| Post message | Yes (encrypted + signed) |
| Group replies | Yes (quoted reply to the triggering message) |
| Edit message | Yes (encrypted edit event targeting the original's sequence id; only the bot's own text messages). The first edit of a fresh message is held until the message is editSafetyDelayMs old, so receiving clients have stored the original before the edit arrives |
| Delete message | Yes (delete-for-all: a locally signed delete action removes the message for every participant; only the bot's own messages) |
| Streaming | Limited (message edits work, but the first edit is age-gated by editSafetyDelayMs, so rapid token-by-token updates are coarse) |
| Read receipts | Yes (sent for each delivered inbound message unless sendReadReceipts: false; falls back to the latest conversation event) |
| TTL propagation | Yes (replies inherit the inbound message's disappearing-message TTL) |
Rich content
| Feature | Supported |
|---|---|
| Markdown | No client-side rendering — outgoing markdown appears literally as plain text (tables are the exception, see below) |
| URL / @mention entities | Yes (detected in outgoing text, rendered as tappable links and mention pills) |
| Post cards | Yes (first x.com/.../status/... URL in outgoing text auto-attaches) |
| Media out | Yes (files encrypt-streamed and uploaded via the 3-step media upload flow) |
| Media in | Yes (attachments exposed with lazy download + decrypt via fetchData()) |
| Cards / buttons / modals | No (X Chat has no interactive message surface) |
| Tables | ASCII code blocks |
Conversations
| Feature | Supported |
|---|---|
| Mentions | Yes (structured mention entities, swipe-reply-to-bot, plain-text @handle fallback) |
| Add / remove reactions | Yes |
| Incoming reactions | Yes (routed to onReaction) |
| Typing indicator | Yes (keep-alive re-sent every 3s while a handler runs) |
| DMs | Yes |
| Group chats | Yes (g-prefixed conversation IDs) |
| Group join welcome | Yes (configurable via welcomeMessage) |
| Open DM | Yes (openDM(userId) reuses an existing conversation or runs a fresh key exchange; requires the recipient to have encrypted chat set up) |
Message history
| Feature | Supported |
|---|---|
| Fetch messages | Yes (GET /2/chat/conversations/{id}/events, batch-decrypted) |
| Fetch thread info | Yes |
| List threads | No |
Mention behavior in groups
chat.onNewMention(handler) fires for group messages that mention the bot. A group message counts as a mention when:
- Its rich-text mention entities include the bot's @handle or user ID — the authoritative signal, or
- It is a swipe-reply to one of the bot's own messages, or
- Its plain text contains
@handle(fallback when no entities are present).
To reply to every group message instead, register a catch-all chat.onNewMessage(/.+/, handler).
Thread ID format
xchat:{conversationId}
- 1:1 conversations:
xchat:1234567890-9876543210(both participant IDs) - Group conversations:
xchat:g123456789(opaqueg-prefixed ID)
REST paths use the other participant's user ID for 1:1 conversations and the g... ID for groups; the adapter converts automatically.
Encryption notes
- Conversation keys arrive via
KeyChangeevents and are cached per conversation and key version. When a key is missing at send time, the adapter fetches recent conversation events to extract one. - Incoming message signatures are verified against participants' signing keys (fetched from
GET /2/users/{id}/public_keysand cached). WithverifySignatures: true(the default), unverifiable messages are dropped. - Media is encrypted separately from message text using streaming encryption; the message carries a
media_hash_keyreference that the recipient uses to download and decrypt the blob.
Troubleshooting
CRC validation failing
- Confirm
X_CONSUMER_SECRETmatches your app's consumer secret in the developer portal - The response must be
{"response_token": "sha256=<base64 HMAC>"}for GET requests
Events not arriving
- Verify the subscription exists:
GET /2/activity/subscriptionswith the app bearer token - Confirm the subscription's
webhook_idpoints at a webhook whose URL is your live endpoint - Subscription changes can take a few minutes to provision
"Invalid signature" on webhook POSTs
- The adapter verifies
x-twitter-webhooks-signaturewith HMAC-SHA256 keyed byX_CONSUMER_SECRET— make sure it is the same app that owns the webhook
Messages decrypt but the bot never replies in groups
- Group replies require a mention — check that the sender actually @mentioned the bot's handle (the one from
/2/users/me), or use{ allGroupMessages: true }
"No conversation key" when posting
- The bot can only send in conversations where a conversation key has been shared with it (i.e., someone messaged the bot first, added it to the group, or the bot opened the conversation itself via
openDM(userId)).openDMrequires the recipient to have encrypted chat set up, and the server requires the recipient to trust the bot (e.g. follow it) before the first message is accepted.
AI Coding Agents
If you use an AI coding agent such as OpenAI Codex, Claude Code, or Cursor, install the Chat SDK skill so it knows the SDK APIs, adapter patterns, and project conventions before writing code.
npx skills add vercel/chat
The skill references bundled documentation in node_modules/chat/docs, plus adapter guides and starter templates in the published package.
You can also install the Vercel Plugin for a broader agent toolkit: it includes the Chat SDK skill alongside specialist agents, agent slash commands, and more:
npx plugins add vercel/vercel-plugin
The plugin is optional; the skill alone is enough to build with Chat SDK.
For agent-readable documentation, see chat-sdk.dev/llms.txt (page index) or chat-sdk.dev/llms-full.txt (full text).
License
MIT