Twitter webhooks: tweets as they post.
X does send webhooks. Register an HTTPS URL with its Webhooks API, then subscribe through the X Activity API to receive an account’s new posts at $0.005 each. Webhooks for keyword rules are Enterprise only.
Without a developer account, poll a search instead and forward each new tweet to your own URL. Below: what each X product delivers and costs, a Python poller, and a calculator for which is cheaper at your volume.
What X delivers in real time.
Three X API products push data as it happens. Pick by what you watch: one account’s activity, or every public post that matches a rule.
| Product | Watches | Access and delivery | Price |
|---|---|---|---|
| X Activity API | One account per subscription. Any public account’s posts, deletes, profile changes and live broadcasts. Replies, mentions, likes and DMs only for users who authorized your app. | Pay-per-use, up to 1,500 subscriptions. Delivered by webhook or a persistent HTTP stream. | $0.005 per post event, $0.010 per follow or DM received. Deletes and sent DMs are not billed. |
| Account Activity API | Everything that happens to accounts that authorized your app: posts, mentions, replies, likes, follows, DMs. | Pay-per-use: 3 accounts and 1 webhook. X is deprecating it in favour of the X Activity API. | Delivered posts count toward the monthly Post cap. |
| Filtered stream | Public posts that match your rules: keywords, hashtags, accounts, languages. | Pay-per-use: 1,000 rules over one persistent HTTP connection, about 4 to 5 seconds P99 latency. Webhook delivery is Enterprise only. | $0.005 per post, within the monthly Post cap. |
Source: X’s developer documentation and pay-per-usage pricing page. Pay-per-use accounts can read up to 3 million posts a month across all endpoints.
What an X webhook needs.
The keys come from the X Developer Console; how to get them. The CRC answer is a few lines in any language.
- 1.A developer account, a Project and App in the Developer Console, and prepaid credits.
- 2.A public HTTPS URL with no port number in it.
- 3.A GET handler for the Challenge-Response Check (CRC): X sends a crc_token and expects an HMAC-SHA256 of it, signed with your app’s client secret. X repeats the check, and a webhook that fails it stops receiving events.
- 4.A POST handler that returns 200 within 10 seconds.
- 5.Signature checks on every POST: the X-Twitter-Webhooks-Signature-OAuth2 header holds an HMAC-SHA256 of the raw body, made with the same secret.
import base64, hashlib, hmac
def crc_response(crc_token: str, client_secret: str) -> dict:
digest = hmac.new(client_secret.encode(), crc_token.encode(), hashlib.sha256).digest()
return {"response_token": "sha256=" + base64.b64encode(digest).decode()}No developer account? Poll and forward.
The script runs a search every minute and posts each tweet it has not seen to your webhook URL, oldest first. A normal poll is one request; it reads further pages only when more new tweets arrived than one page of 20 holds.
One query can watch many accounts (from:a OR from:b) or any keyword, and the price is per request, not per account. On its first run it only records the current tweets, so your endpoint is not flooded; seen.json lets it resume after a restart.
- 1. Get an API key and set it as
CHEAPXAPI_KEY. - 2. Set
WEBHOOK_URLto the endpoint that should receive tweets. - 3.
pip install requests, change QUERY and run it.
import json, os, time, requests
API = "https://api.cheapxapi.com/twitter/tweet/advanced_search"
HEADERS = {"x-api-key": os.environ["CHEAPXAPI_KEY"]}
WEBHOOK_URL = os.environ["WEBHOOK_URL"]
QUERY = "from:NASA OR from:SpaceX"
INTERVAL = 60 # seconds between polls
MAX_PAGES = 5 # caps what one burst of new tweets can cost
STATE = "seen.json"
def search(cursor=""):
params = {"query": QUERY, "queryType": "Latest"}
if cursor:
params["cursor"] = cursor
res = requests.get(API, headers=HEADERS, params=params, timeout=75)
res.raise_for_status()
return res.json()
def new_tweets(seen):
fresh, cursor = {}, ""
for _ in range(MAX_PAGES):
page = search(cursor)
tweets = page.get("tweets", [])
for t in tweets:
if t["id"] not in seen:
fresh[t["id"]] = t
# A page that reaches a known tweet means everything older was seen.
if any(t["id"] in seen for t in tweets) or not page.get("has_next_page") or not page.get("next_cursor"):
break
cursor = page["next_cursor"]
return sorted(fresh.values(), key=lambda t: int(t["id"])) # oldest first
def save(seen):
with open(STATE, "w") as f:
json.dump(seen[-2000:], f)
if os.path.exists(STATE):
with open(STATE) as f:
seen = json.load(f)
else:
seen = [t["id"] for t in search().get("tweets", [])]
save(seen)
print(f"Watching {QUERY!r}, skipped {len(seen)} existing tweets", flush=True)
while True:
try:
for t in new_tweets(set(seen)):
requests.post(WEBHOOK_URL, timeout=10, json={
"id": t["id"], "url": t["url"], "author": t["author"]["userName"],
"created_at": t["createdAt"], "text": t["text"],
}).raise_for_status()
seen.append(t["id"])
save(seen)
except requests.RequestException as err:
print("Will retry next poll:", err, flush=True)
time.sleep(INTERVAL)Endpoint reference: Advanced search. Query syntax: search operators.
Polling or X: which costs less.
Polling costs a fixed amount per poll until a poll needs a second page. X charges per post delivered. For a quiet account X is cheaper; for a busy keyword or many accounts in one query, polling is.
| Every | Requests a day | 30 days |
|---|---|---|
| 15 seconds | 5,760 | $103.68 |
| 30 seconds | 2,880 | $51.84 |
| 1 minute | 1,440 | $25.92 |
| 5 minutes | 288 | $5.18 |
| 15 minutes | 96 | $1.73 |
When fewer than 20 new tweets arrive per interval.
| Provider | Bar | Value |
|---|---|---|
| Polling cheapxapiPolls every minute | $25.92 | |
| X API stream or webhook$0.005 per post delivered | $45.00 |
Polling every minute costs $25.92 over 30 days even when nothing is posted, and only more when one poll needs a second page. X’s per-post price is lower below about 173 tweets a day at this interval.
Which to use.
- Seconds matter, or you need DMs: use X’s own webhooks or filtered stream. A search cannot read DMs, and polling is never faster than its interval.
- A few quiet accounts: the X Activity API at $0.005 a post usually costs less than polling every minute.
- Busy keywords or many accounts, a minute’s delay is fine: poll one search with the script above. No developer account, no public endpoint for X to check, and a burst of tweets adds a request per page, not a charge per tweet.
Questions.
Does Twitter (X) have webhooks?
Yes. You register an HTTPS URL with X’s V2 Webhooks API, pass its CRC check, and then attach a product that sends events to it: the X Activity API for events on specific accounts, the older Account Activity API for accounts that authorized your app, or filtered stream webhooks for keyword rules on Enterprise plans.
Can I get a webhook when a specific account tweets?
Yes. Subscribe to the post.create event in the X Activity API with that account’s user ID; it works for any public account and costs $0.005 per post delivered. Without a developer account, poll a from: search and forward new tweets to your URL, as the script on this page does.
Can I get a webhook for a keyword or hashtag?
Through X, keyword webhooks are Enterprise only. Pay-per-use accounts get the same rules through the filtered stream, which you read over a persistent HTTP connection instead of a webhook. The alternative is polling a search for the keyword.
Does cheapxapi send webhooks?
No. cheapxapi answers requests; it does not push data. The script on this page runs on your machine, polls a search and posts each new tweet to the webhook URL you choose.
How fast does a polled tweet arrive?
Within one polling interval plus the time the tweet takes to show up in X search. X documents about 4 to 5 seconds P99 latency for its filtered stream; when seconds matter, use that.