Guide to X (Twitter) Webhooks: Features and Best Practices
X (formerly Twitter) webhooks let your application react to account activity in real time: a post is created, a Direct Message arrives, someone follows an account you manage. Instead of polling the API, X pushes each activity to your endpoint as a JSON POST through the Account Activity API, delivered over the V2 Webhooks API.
This guide covers how X webhooks work, the events you can subscribe to, the two-part security model (the Challenge-Response Check plus per-delivery signatures), the limitations worth knowing before you build, and the best practices that keep an integration reliable in production.
What are X (Twitter) webhooks?
X webhooks are HTTP callbacks that deliver real-time event notifications tied to specific user accounts. When an account you're subscribed to posts, likes, follows, or sends a Direct Message, X sends a POST request with a JSON body describing the activity. This is the push-based alternative to repeatedly calling the API to check for new activity.
Webhooks are registered and managed through the V2 Webhooks API (/2/webhooks), and the events themselves come from the Account Activity API (AAA), which ties activity to the users who have authorized your app. Access requires an approved developer account with a Project and App, and your endpoint has to satisfy X's registration checks before it will receive anything.
X webhook features
| Feature | Details |
|---|---|
| Webhook configuration | V2 Webhooks API (POST/GET/DELETE /2/webhooks), OAuth 2.0 App-Only bearer auth |
| Endpoint validation | Challenge-Response Check (CRC) at registration, roughly hourly, and on demand |
| Signature scheme | HMAC-SHA256 over the raw body, keyed with the consumer secret, base64-encoded, sha256= prefix |
| Signature header | x-twitter-webhooks-signature |
| Replay protection | None; the signature carries no timestamp |
| Endpoint requirements | Public HTTPS, no port in the URL, respond within 10 seconds with a 200 |
| Event source | Account Activity API, per-user subscriptions |
| Retry behavior | Not documented for V2; treat delivery as best-effort |
| Access tiers | Pay-Per-Use (1 webhook, 3 subscriptions) or Enterprise (5+ webhooks, 5,000+ subscriptions) |
| Official verification SDK | None |
Account Activity events
The Account Activity API delivers events as JSON objects keyed by activity type. The names follow a consistent convention: lowercase, underscore-separated, plural, with an _events suffix. The one exception is user_event (singular), which carries account-level revocation activity.
| Activity | Fires when |
|---|---|
tweet_create_events | A subscribed user posts, replies, quotes, or Retweets |
tweet_delete_events | A subscribed user deletes a post |
favorite_events | A subscribed user likes a post |
follow_events | A subscribed user follows, or is followed by, another account |
block_events | A subscribed user blocks or unblocks an account |
mute_events | A subscribed user mutes or unmutes an account |
direct_message_events | A Direct Message is sent to or from a subscribed user |
direct_message_indicate_typing_events | Someone starts typing in a DM conversation with a subscribed user |
direct_message_mark_read_events | A DM conversation is marked read |
user_event | Account authorization is revoked (subscription should be removed) |
Each POST is scoped to one subscribed user and may contain one or more activity objects. Subscribe only to the activity types your integration needs, and consult X's Account Activity API reference for the authoritative, current catalog.
Setting up X webhooks
Configuration is API-only. There's no dashboard for registering a webhook endpoint.
Register a webhook endpoint
Register your endpoint against the V2 Webhooks API using an OAuth 2.0 App-Only bearer token:
curl -X POST "https://api.x.com/2/webhooks" \
-H "Authorization: Bearer $APP_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://your-app.example.com/webhook"}'
At registration, X immediately issues a Challenge-Response Check (see below) against the URL. If your endpoint doesn't answer the CRC correctly, registration fails and the webhook is marked invalid. You can list webhooks with GET /2/webhooks, remove one with DELETE /2/webhooks/:webhook_id, and force a fresh CRC with PUT /2/webhooks/:webhook_id.
Subscribe a user
Registering the webhook only creates the delivery endpoint. To actually receive a user's activity, add a subscription in that user's OAuth 1.0a context:
curl -X POST "https://api.x.com/2/account_activity/webhooks/:webhook_id/subscriptions/all" \
-H "Authorization: OAuth ..." # OAuth 1.0a user context
The subscription authorizes X to send that user's Account Activity to your registered webhook.
Securing X webhooks
X's security model has two independent parts. The CRC proves you control the endpoint and hold the consumer secret; it runs at registration and on an ongoing basis. The per-delivery signature proves each individual POST came from X. You need both.
A critical detail for both: the key is your app's consumer secret (the API secret key), never the bearer token or an access token.
The Challenge-Response Check (CRC)
X sends a GET to your webhook URL with a crc_token query parameter at registration, roughly every hour, and whenever you trigger a PUT /2/webhooks/:webhook_id. You must respond promptly with a JSON body containing a response_token: an HMAC-SHA256 of the crc_token, keyed with your consumer secret, base64-encoded and prefixed with sha256=. Fail it and X marks the webhook invalid and stops delivering events until it passes again.
const crypto = require("crypto");
const CONSUMER_SECRET = process.env.X_CONSUMER_SECRET; // API secret key
function sign(message) {
return (
"sha256=" +
crypto.createHmac("sha256", CONSUMER_SECRET).update(message).digest("base64")
);
}
// CRC handshake: X sends GET /webhook?crc_token=...
app.get("/webhook", (req, res) => {
res.status(200).json({ response_token: sign(req.query.crc_token) });
});
Verifying the signature on each delivery
Every POST carries an x-twitter-webhooks-signature header. Recompute the HMAC over the exact raw request body (verify before any JSON parsing or re-serialization would alter the bytes) and compare with a constant-time comparison.
function verifySignature(rawBody, signatureHeader) {
const expected = sign(rawBody);
const received = Buffer.from(signatureHeader || "");
const computed = Buffer.from(expected);
return (
received.length === computed.length &&
crypto.timingSafeEqual(received, computed)
);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-twitter-webhooks-signature"];
if (!verifySignature(req.body, signature)) {
return res.status(403).send("Invalid signature");
}
const event = JSON.parse(req.body);
res.sendStatus(200); // acknowledge fast
processQueue.add(event); // then process asynchronously
});
The same two flows in Python (Flask):
import base64
import hashlib
import hmac
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
CONSUMER_SECRET = os.environ["X_CONSUMER_SECRET"].encode() # API secret key
def sign(message: bytes) -> str:
digest = hmac.new(CONSUMER_SECRET, message, hashlib.sha256).digest()
return "sha256=" + base64.b64encode(digest).decode()
@app.get("/webhook")
def crc():
crc_token = request.args["crc_token"]
return jsonify(response_token=sign(crc_token.encode()))
@app.post("/webhook")
def webhook():
signature = request.headers.get("x-twitter-webhooks-signature", "")
if not hmac.compare_digest(signature, sign(request.get_data())):
return "Invalid signature", 403
# process request.get_json() asynchronously
return "", 200
X webhook limitations and pain points
No documented retry behavior
The Problem: X does not document any retry schedule for V2 webhook deliveries. In practice this means you should treat delivery as best-effort: if your endpoint is down, slow, or returns a non-200, the event may simply be gone. There's no delivery log to recover it from either.
Why It Happens: X's V2 webhook documentation focuses on registration, the CRC, and signature verification, and doesn't publish delivery-retry semantics the way many billing providers do.
Workarounds:
- Keep the endpoint highly available and acknowledge every delivery with a 200 as fast as possible.
- Reconcile against the API periodically for the activity you can't afford to miss, rather than trusting webhooks as a system of record.
- Log every raw delivery on receipt so you have your own record independent of X.
How Hookdeck Can Help: Hookdeck accepts each delivery on your behalf, acknowledges X immediately, and durably queues the event. If your downstream service is unavailable, Hookdeck retries on a schedule you control and preserves failed events for replay, so a momentary outage on your side no longer means lost activity.
No timestamp means no replay protection
The Problem: The signature scheme is an HMAC over the body only. There is no timestamp in the signed material, so a captured, validly signed payload can be replayed later and it will still verify. Signature verification alone doesn't tell you the request is fresh.
Why It Happens: X's scheme predates the timestamped designs (like Standard Webhooks) that bake replay windows into the signature, and it wasn't updated to add one.
Workarounds:
- Make event processing idempotent so replaying a payload has no additional effect.
- Track an activity identifier from the payload and drop anything you've already handled.
- Terminate TLS correctly and restrict the endpoint to reduce the chance of capture in the first place.
How Hookdeck Can Help: Hookdeck deduplicates deliveries using configurable rules before they reach your application, so a replayed or duplicated payload is filtered at the edge. See our guide to webhook idempotency.
The hourly CRC keeps your endpoint on the hook
The Problem: The CRC isn't a one-time registration step. X re-issues it roughly every hour, and a single failed check flips the webhook to invalid and halts delivery. An endpoint that can serve POSTs but mishandles the GET CRC (or loses access to the consumer secret) silently stops receiving events.
Why It Happens: The recurring CRC is how X continuously confirms you still control the endpoint and hold the signing secret.
Workarounds:
- Implement and test the
GETCRC handler as carefully as thePOSThandler; don't treat it as an afterthought. - Alert on
invalidwebhook status viaGET /2/webhooksso you catch a failing CRC before activity piles up as lost. - Keep the consumer secret available to the CRC handler in every environment the endpoint runs in.
How Hookdeck Can Help: Point X at Hookdeck and Hookdeck answers the CRC and verifies signatures at the edge, keeping the endpoint validated while your application only ever handles clean, verified events.
Ten-second response window
The Problem: Your endpoint must return a 200 within 10 seconds. Any synchronous work in the handler (database writes, downstream API calls, enrichment) eats into that budget, and a slow response counts as a failure.
Why It Happens: X caps how long it will wait for an acknowledgement to keep delivery throughput predictable across all subscribers.
Workarounds:
- Acknowledge first, process later: return 200 as soon as the signature verifies, then hand the event to a queue.
- Keep zero blocking I/O in the request path before you respond.
How Hookdeck Can Help: Hookdeck absorbs the delivery in milliseconds and forwards to your endpoint on a configurable timeout and delivery rate, decoupling X's 10-second window from however long your processing actually takes.
API-only configuration and tiered limits
The Problem: There's no UI for webhook management, and Account Activity access is tiered: Pay-Per-Use allows a single webhook and three subscriptions, while higher subscription counts require Enterprise access. Building past a small proof of concept means scripting the API and planning around the subscription ceiling.
Why It Happens: X exposes webhook lifecycle purely through the V2 API and gates activity volume behind access tiers.
Workarounds:
- Treat webhook and subscription configuration as code: script registration, subscription, and teardown so environments are reproducible.
- Track your subscription count against your tier limit and request higher access before you hit the ceiling.
How Hookdeck Can Help: Hookdeck gives you a dashboard and API over your webhook connections, plus the observability, filtering, and routing that X's raw API leaves you to build.
Best practices
Always run both the CRC and per-delivery verification
The CRC validates the endpoint; the x-twitter-webhooks-signature check validates each POST. Skipping the signature check leaves your handler acting on any unauthenticated request that reaches the URL. Do both, and key both off the consumer secret.
Verify against the raw body
Recompute the HMAC over the exact bytes X sent, before parsing. Re-serializing the JSON can change whitespace or key order and break the comparison. Use express.raw (Node) or request.get_data() (Flask) to hold the untouched body.
Use constant-time comparison
Compare the received and computed signatures with crypto.timingSafeEqual / hmac.compare_digest, never ==. String comparison can leak timing information about how much of the signature matched.
Acknowledge fast, process asynchronously
Return 200 within milliseconds and defer real work to a queue. This keeps you inside the 10-second window and means a slow downstream never turns into a failed delivery. See why to process webhooks asynchronously.
Process idempotently and reconcile
With no documented retries, no replay protection, and no delivery log, webhooks are a notification mechanism, not a source of truth. Dedupe on an activity identifier, make side effects idempotent, and reconcile against the API for anything you can't afford to lose.
Monitor webhook status
Poll GET /2/webhooks for an invalid status so a failing CRC surfaces as an alert rather than as silently missing activity.
Make X (Twitter) webhooks production-ready
Hookdeck answers the CRC, verifies signatures, and durably queues every activity so nothing is lost
Conclusion
X (Twitter) webhooks turn Account Activity into something the rest of your stack can react to without polling. The security model is more demanding than most: a recurring Challenge-Response Check plus a per-delivery HMAC signature, both keyed with your consumer secret, both easy to get wrong. And the delivery guarantees are thin: X publishes no retry schedule, the signature carries no replay window, and there's a hard 10-second response budget.
That combination puts the operational burden on you. Answering the CRC reliably, verifying every signature against the raw body, acknowledging fast, deduplicating, and reconciling for the activity you can't miss are all your responsibility unless you offload them.
For teams that would rather build on top of X's activity than babysit its delivery mechanics, Hookdeck answers the CRC, verifies signatures, deduplicates, and durably queues every event at the edge, so your application only ever sees clean, verified activity.
Get started with Hookdeck for free and handle X (Twitter) webhooks reliably in minutes.