X API webhooks: the CRC the tools cannot sign
X's webhook docs recommend signing the CRC with the OAuth 2.0 client secret. The tool the same page tells you to test with still cannot do it.
On this page · 9 sections
- Quick answer
- The eight routes
- The CRC is the whole job
- The tool X recommends cannot do what X recommends
- How often CRC actually fires, according to two pages
- Three products, three billing models, one URL
- The Filtered Stream link, and the Enterprise wall
- Before you build the receiver
- When a webhook is the wrong shape
The short version
- ▸There are eight webhook routes in spec version 2.174. The Webhooks hub page lists five and leaves out all three Filtered Stream link routes, which live under /2/tweets/search/webhooks rather than /2/webhooks.
- ▸X now recommends signing the CRC response with your app's OAuth 2.0 client secret, not the OAuth 1.0 consumer secret every older tutorial uses. The signature header changes name with the key: X-Twitter-Webhooks-Signature-OAuth2 instead of X-Twitter-Webhooks-Signature.
- ▸xurl, the testing tool X's own quickstart recommends, still cannot answer a CRC with an OAuth 2.0 client secret. The feature request opened 21 October 2026 and the pull request opened 8 November 2026 were both still open on 9 November 2026.
- ▸The two live docs pages disagree on how often CRC fires. The quickstart says hourly. The hub page says every 30 minutes, and only if the webhook has not validated successfully in the past 24 hours. Do not build a heartbeat on either number.
- ▸A 3XX redirect or an SSL error marks your webhook invalid immediately. A 4XX, 5XX or timeout is treated as transient and only kills it after 28 hours without a successful validation.
Quick answer
X API webhooks are eight routes, not the five the hub page lists, and the hard part is not registration. It is the Challenge-Response Check. X now recommends signing the CRC with your app's OAuth 2.0 client secret, which renames the signature header you have to verify, and xurl - the testing tool X's own quickstart tells you to use - still could not sign with that secret as of 9 November 2026. The feature request has been open since 21 October and the pull request since 8 November.
Last updated: November 2026
Everything below is checked against the live API description at api.x.com/2/openapi.json, version 2.174, X's webhooks, filtered-stream-webhooks and rate-limit pages, and the open issues on X's own CLI. All read on 9 November 2026.
The eight routes
The hub page publishes a five-row endpoint table. The spec has eight operations, and the missing three are the ones that do the thing most people want.
| Method | Route | What it does | Auth in the spec |
|---|---|---|---|
POST | /2/webhooks | Register a URL, triggers CRC immediately | OAuth 2.0 user, OAuth 1.0a, or Bearer |
GET | /2/webhooks | List registrations and their valid flag | OAuth 2.0 user, OAuth 1.0a, or Bearer |
PUT | /2/webhooks/{webhook_id} | Fire a CRC, re-enable if it passes | OAuth 2.0 user, OAuth 1.0a, or Bearer |
DELETE | /2/webhooks/{webhook_id} | Remove a registration | OAuth 2.0 user, OAuth 1.0a, or Bearer |
POST | /2/webhooks/replay | Re-deliver up to 24 hours of events | Bearer only |
POST | /2/tweets/search/webhooks/{webhook_id} | Link a Filtered Stream to a webhook | Bearer only |
GET | /2/tweets/search/webhooks | List stream links | Bearer only |
DELETE | /2/tweets/search/webhooks/{webhook_id} | Unlink | Bearer only |
Two things to take from that table. The stream-link routes are not under /2/webhooks at all, which is why they are easy to miss in a doc tree organised by path. And the hub page's line that "all endpoints require OAuth2 App Only Bearer Token authentication" is right about the three routes it does not list and wrong about the four it does: the spec gives every /2/webhooks operation three security options, including an OAuth 1.0a user token.
A detail no prose page mentions: GET /2/webhooks accepts a webhook_config.fields parameter with four values, created_at, id, url and valid. It is the same field-group pattern as the rest of the API, undocumented here. And url has a maxLength of 200 characters, so a long signed callback path can be rejected by validation rather than by CRC.
The CRC is the whole job
X sends a GET to your URL with a crc_token query parameter. You answer with one JSON key:
{ "response_token": "sha256=<base64 of hmac_sha256(secret, crc_token)>" }
The secret is where every older guide is now wrong. X's current quickstart puts the OAuth 2.0 client secret first and keeps the OAuth 1.0 consumer secret as a backward-compatibility path. The header you verify on incoming POST deliveries changes with it:
| Header | Keyed with | Status |
|---|---|---|
X-Twitter-Webhooks-Signature-OAuth2 | OAuth 2.0 client secret | Recommended, verify when present |
X-Twitter-Webhooks-Signature | OAuth 1.0 consumer secret | Legacy, format unchanged |
An app configured with only an OAuth 2.0 client secret receives only the OAuth2 header. An app with both may receive both during migration. So a service that was written against any tutorial published before this change looks for a header that will never arrive, reads null, and either rejects every delivery or - worse - skips verification entirely.
Neither of the top non-X results is lying. The highest-ranking third-party guide for this query, and the legacy developer.twitter.com page Google still serves, both describe one header keyed with the consumer secret. They are describing the API as it was.
X is explicit about one more thing, in a warning box, because people get it wrong: the app-only Bearer token you put in the Authorization header when you call /2/webhooks is not the CRC key and not the signature key. Those always use a client or consumer secret, which never travels in the registration request.
The tool X recommends cannot do what X recommends
The quickstart's testing section says to install xurl and run xurl webhook start, which spins up an ngrok tunnel and answers CRC checks for you. On 9 November 2026 that tool cannot answer a CRC with an OAuth 2.0 client secret.
That gap is documented in X's own repository. Issue #105, opened 21 October 2026, states that an app with an OAuth 2.0 client ID and secret already registered in xurl "cannot start its temporary webhook receiver unless an OAuth1 consumer secret is also stored." Pull request #111, opened 8 November 2026, implements the fix with tests and a README change. Both were still open when this was written.
So if you are building the recommended path, the recommended tool makes you store a legacy credential you are otherwise trying to retire. Deploy the Flask example from the quickstart to a real HTTPS host instead, or keep an OAuth 1.0a consumer secret around purely for local testing and sign production with the OAuth 2.0 one. Getting both sets of credentials in the first place is covered in how to get an X API key.
How often CRC actually fires, according to two pages
Its trigger table says X "automatically validates your webhook every hour." The hub page's validation section says a CRC is sent "periodically every 30 minutes, but only if the webhook has not been successfully validated in the past 24 hours."
Those are not the same system. One is a fixed hourly ping, the other is a 30-minute retry loop that stays asleep while things are healthy. Both pages are live. The practical read: treat CRC as a liveness probe that can arrive at any time and must always work, and do not use its arrival as a heartbeat for anything of your own.
What is documented consistently is how a webhook dies, and the asymmetry there is worth coding to.
| Your response | Result |
|---|---|
2XX with the wrong response_token | Invalid immediately |
| Any 3XX | Invalid immediately |
| SSL exception | Invalid immediately |
| 4XX, 5XX, timeout, closed channel | Transient, invalid after 28 hours without a success |
A redirect is fatal and a 500 is not. If your framework answers GET /webhook with a 301 to a trailing slash, or your load balancer redirects HTTP to HTTPS on a path X is probing, the webhook goes invalid on the first check while a genuinely broken handler gets a day and a half of grace. The other requirements are the usual ones plus one that trips people: the URL cannot carry a port, so https://example.com:5000/webhook is rejected, and you have 10 seconds to answer.
When a webhook does go invalid, PUT /2/webhooks/{webhook_id} re-fires the check. Read its response carefully. The spec describes that endpoint's valid field as "Indicates whether the CRC validation check was triggered," while the quickstart says it "reflects the status after the check attempt." Those descriptions disagree, and the quickstart itself then tells you to confirm with GET /2/webhooks. Do that. Treat PUT as fire-and-verify, never as a health check.
Three products, three billing models, one URL
A registered webhook is plumbing. What fills it is one of three products, and they do not price the same way.
| Product | What arrives | Published price |
|---|---|---|
| X Activity API | Posts, replies, follows, DMs, profile edits | Per event, $0.001 to $0.010 |
| Filtered Stream webhooks | Posts matching your rules | Counts against the 3 million monthly Post read cap |
| Account Activity API | Account-scoped events | None published |
Note that the rate card's webhook event table is explicitly scoped to X Activity API deliveries. Filtered-stream Posts are billed as Post reads instead, which is a different meter with a different cap, as mapped in X API pricing. Account Activity API deliveries sit in neither table, and that API already carries a deprecation notice pointing at the Activity API.
There is no price row for registering, listing, validating, deleting or replaying a webhook. Management is free. Rate limits are generous on the same routes: 450 per 15 minutes per app on all four /2/webhooks operations, 100 per 15 minutes on replay, and no published row at all for the three stream-link routes or for any /2/account_activity subscription route. More of these gaps are catalogued in X API rate limits.
One open question before you lean on replay. Deduplication runs on a 24-hour UTC window, and a replay range can cover up to the past 24 hours. A replay that crosses midnight UTC therefore lands in a different window from the original delivery. X does not publish whether a replayed event is billed again, so price a backfill as if it might be.
The Filtered Stream link, and the Enterprise wall
Those three stream-link routes are the shortest path from "I have rules" to "X pushes matches to my server," and for most accounts they do not work. X states the filtered stream webhooks group "is currently available to Enterprise developers," with 25,000+ rules per project and rule lengths up to 2,048 characters. A pay-per-use developer can register the webhook, pass CRC and post rules successfully, then get a 403 client-not-enrolled on the link call. X API stream walks through that exact sequence.
If you do have access, the published examples need correcting on three counts:
- The retrieval example reads
GET /2/tweets/search/stream/webhooks. The route in the spec and in the docs index isGET /2/tweets/search/webhooks, with no/streamsegment. - That example's response wraps the links in
data.links. The spec declaresdataas a flat array of link objects. - The link call is documented as accepting stream query parameters such as
expansionsanduser.fields. The spec declares no parameter on that operation beyondwebhook_id, and the generated Python client'screate_stream_link(webhook_id)takes nothing else, so an SDK user cannot send them at all.
X's own sample response shows what happens to those parameters when they do get through. The stored fields array comes back as ["user.fields=username", "name", "id", "expansions=author_id"]. The query string was split on commas, so name and id lost the parameter they belonged to. Pass one field per parameter if you want the link's configuration to read back correctly.
Before you build the receiver
Two behaviours worth knowing in advance, both from X's own notes. Every incoming DM is delivered by webhook, including DMs your account sent from other clients, so a naive handler will see its own outbound messages. And duplicate deliveries happen, by design when two apps share a URL and occasionally otherwise, so deduplicate by event ID rather than assuming exactly-once. X Chat API covers the encrypted-DM case, where the events arrive as chat.received through the Activity API rather than through any legacy DM flow.
When a webhook is the wrong shape
If you are building a service that reacts to other people's activity - a support inbox, an alerting bot, a moderation queue - webhooks are the correct architecture and the per-event pricing beats polling by an order of magnitude.
If what you want is your own account posting, replying and following on a schedule, none of this helps. You would be standing up an HTTPS host and an HMAC verifier to be told about things you already did. X-Autopilot takes the other route: it drives your own logged-in session in your own browser on your own Mac, so the session cookie never leaves the machine and there is no cloud IP pool or delegate account in the path. That is a lower detection surface than a cloud scheduler rather than a safe one, and automating engagement through a browser is a gray area that X's automation rules speak to directly. The API is the sanctioned path. Use it when you need sanctioned, and price the events before you wire the receiver.
Frequently asked
Answers indexed by Google + AI assistants.
What is the X API webhooks endpoint?+
POST https://api.x.com/2/webhooks registers an HTTPS URL, GET /2/webhooks lists what you have registered, DELETE /2/webhooks/{webhook_id} removes one, and PUT /2/webhooks/{webhook_id} fires a fresh CRC check and re-enables a webhook that went invalid. POST /2/webhooks/replay re-delivers past events. Three more routes under /2/tweets/search/webhooks link a Filtered Stream ruleset to a registered webhook.
How do I respond to an X webhook CRC check?+
X sends a GET to your URL with a crc_token query parameter. Reply with JSON containing response_token set to the string sha256= followed by the base64 of an HMAC SHA-256 of that token, keyed with your app's OAuth 2.0 client secret. X's current guidance prefers the OAuth 2.0 client secret and keeps the OAuth 1.0 consumer secret as a legacy fallback. Never use the app-only Bearer token you authenticate the registration call with.
Why is my X webhook marked invalid?+
Three failures mark it invalid straight away: a 2XX response carrying the wrong response_token, any 3XX status, or an SSL exception. Everything else X calls transient, including 4XX, 5XX, request timeouts and a closed channel, and those only mark the webhook invalid once it has gone more than 28 hours without validating, which includes a four-hour grace period. Check the valid flag with GET /2/webhooks and re-enable with PUT /2/webhooks/{webhook_id}.
Do webhook management calls cost anything?+
There is no price row for registering, listing, validating, deleting or replaying a webhook on X's pay-per-usage rate card. What costs money is what flows through it. The rate card's webhook event table is scoped to X Activity API deliveries, Posts delivered by a Filtered Stream webhook count against the 3 million monthly Post read cap instead, and Account Activity API deliveries have no published price at all.
Can I use one webhook URL for more than one product?+
Yes. X states that a webhook registered for the Account Activity API works unchanged for Filtered Stream webhooks, so one endpoint can carry both if you can separate the events. The same quickstart warns that two developer apps pointed at one URL with the same user mapped to both will deliver every event twice, so deduplicate by event ID either way.
How far back can a webhook replay job go?+
Up to the past 24 hours. POST /2/webhooks/replay requires webhook_id, from_date and to_date, with both timestamps as 12 digits in yyyymmddhhmm UTC format, so the range is minute-granular. The job covers events that were delivered and events X attempted to deliver, and it returns a job_id rather than the events themselves. The endpoint is capped at 100 calls per 15 minutes per app.
- X live OpenAPI description at api.x.com/2/openapi.json, version 2.174: the eight webhook operations and their security blocks, CreateWebhooksRequest with url maxLength 200, WebhookConfigId pattern ^[0-9]{1,19}$, the four-value webhook_config.fields enum, ValidateWebhooksResponseData describing valid as whether the CRC check was triggered, CreateWebhookReplayJobRequest requiring webhook_id, from_date and to_date in yyyymmddhhmm, and GetWebhooksStreamLinksResponse declaring data as a flat array (accessed 9 November 2026)
- X developer docs, 'V2 Webhooks API': the five-row endpoint table, the blanket statement that all endpoints require an OAuth2 App Only Bearer Token, the URL requirements including no port and a 10-second response, the two signature headers and which secret keys each, and the webhook validation rules naming 3XX and SSL exceptions as immediate failures and 28 hours without validation as the transient-error cutoff (accessed 9 November 2026)
- X developer docs, 'Webhooks Quickstart': the CRC pseudocode keyed with the OAuth 2.0 client secret, the explicit warning not to use the App Only Bearer token, the hourly CRC claim, the four create-failure reasons including WebhookLimitExceeded, the xurl webhook start recommendation, and the duplicate-delivery warning for two apps sharing a URL (accessed 9 November 2026)
- X developer docs, 'Filtered Stream Webhooks' quickstart: the POST /2/tweets/search/webhooks/:webhook_id example carrying expansions and user.fields query parameters, the retrieval example published as GET /2/tweets/search/stream/webhooks, the response example nesting links inside data, and the comma-split fields array reading user.fields=username, name, id, expansions=author_id (accessed 9 November 2026)
- xurl issue #105, 'feat(webhook): support the OAuth2 client secret for CRC', opened 21 October 2026, and pull request #111 implementing it, opened 8 November 2026: both still open, both stating that an app holding only an OAuth 2.0 client ID and secret cannot start xurl's webhook receiver without an OAuth 1.0 consumer secret also stored (accessed 9 November 2026)
- X developer docs, 'X API Rate Limits': the Activity and Webhooks section listing 450/15min per app on the four /2/webhooks routes and 100/15min on /2/webhooks/replay, with no row for any /2/tweets/search/webhooks route and no row for any /2/account_activity route (accessed 9 November 2026)
Product designer and indie hacker. Runs the agent on his own X account every day and writes up what the data shows, including when it's inconvenient.
Follow on X →