# Twitter / X Webhooks

Twitter/X delivers account activity through the Account Activity API. Your
public HTTPS endpoint must do two things:

1. Answer the CRC (Challenge-Response Check) — X sends a `GET` request with a
  `crc_token` query parameter at registration, roughly hourly, and on demand.
  You must reply within the timeout with a `response_token`, or the webhook is
  marked invalid and delivery stops.
2. Verify POST deliveries — every event `POST` carries an
  `x-twitter-webhooks-signature` header you validate before processing.

Both use the same primitive: HMAC-SHA256 keyed with your app's consumer
secret (API secret key), base64-encoded, prefixed with `sha256=`. Use the
consumer secret — not the bearer token or user access token.

## When to Use This Skill

* How do I receive Twitter/X (Account Activity API) webhooks?
* How do I verify the `x-twitter-webhooks-signature` header?
* How do I respond to the X CRC / `crc_token` challenge?
* How do I handle `tweet_create_events`, `follow_events`, or `direct_message_events`?
* Why is my X webhook being marked invalid / why did delivery stop?

## Verification (core)

X signs the raw request body (for POST events) or the `crc_token` value
(for the CRC GET) with HMAC-SHA256 using the consumer secret, base64-encodes it,
and prepends `sha256=`. The exact same helper produces both values:

```javascript
const crypto = require('crypto');

// sha256= + base64(HMAC-SHA256(consumerSecret, message))
function buildSignature(message, consumerSecret) {
  return 'sha256=' + crypto
    .createHmac('sha256', consumerSecret)
    .update(message)
    .digest('base64');
}

// CRC GET: reply { response_token: buildSignature(crc_token, secret) }
// POST:    compare buildSignature(rawBody, secret) to the header, timing-safe
function verifyTwitterSignature(rawBody, signatureHeader, consumerSecret) {
  if (!signatureHeader || !consumerSecret) return false;
  const expected = buildSignature(rawBody, consumerSecret);
  try {
    return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
  } catch {
    return false; // length mismatch = invalid
  }
}

```

> Note: X's scheme has no timestamp, so there is no replay protection and
> retry-on-failure is undocumented for v2 — treat delivery as at-most-once.
> Make handlers idempotent and return `2xx` within 10 seconds.

> For complete handlers with tests, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/twitter-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/twitter-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/twitter-webhooks/examples/fastapi/).

## Common Event Types

Account Activity payloads are keyed by event type. The `for_user_id` field names
the subscribed user the activity belongs to.

| Event key | Triggered when |
| --- | --- |
| `tweet_create_events` | A Post/Tweet, Retweet, reply, @mention, or quote is created |
| `tweet_delete_events` | A Post is deleted (compliance notice) |
| `favorite_events` | A user likes a Post |
| `follow_events` | A follow or unfollow occurs (`event.type` is `follow` / `unfollow`) |
| `block_events` | A block or unblock occurs |
| `mute_events` | A mute or unmute occurs |
| `direct_message_events` | A DM is sent or received |
| `direct_message_indicate_typing_events` | A user starts typing in a DM |
| `direct_message_mark_read_events` | A DM is marked read |
| `user_event` | App authorization is revoked (subscription auto-deleted) |

> For the full event reference, see the [Account Activity API docs](https://docs.x.com/x-api/account-activity/introduction).

## Important Headers

| Header | Description |
| --- | --- |
| `x-twitter-webhooks-signature` | `sha256=<base64 HMAC-SHA256>` over the raw POST body, keyed with the consumer secret |

The CRC arrives as a GET with a `crc_token` query parameter (no signature header).

## Environment Variables

```bash
# App consumer secret / API secret key (X Developer Portal → your app → Keys and tokens)
TWITTER_CONSUMER_SECRET=your_consumer_secret_here

```

## Local Development

```bash
# Forward X events to your local server (no account required)
npx hookdeck-cli listen 3000 twitter --path /webhooks/twitter

```

Register the resulting HTTPS URL with the V2 Webhooks API
(`POST /2/webhooks`, OAuth2 App-Only bearer auth), then subscribe a user via
`POST /2/account_activity/webhooks/:webhook_id/subscriptions/all` (OAuth 1.0a
user context). See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/twitter-webhooks/references/setup.md) for the full flow.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/twitter-webhooks/references/overview.md) - Account Activity API concepts, event keys, payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/twitter-webhooks/references/setup.md) - Register the webhook, pass CRC, subscribe users
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/twitter-webhooks/references/verification.md) - Signature + CRC verification details and gotchas