WEBHOOKS
Twitter Webhook API: Real-Time Post Alerts, Free to Call
The Twitter Webhook API is a free feature: register an HTTPS delivery URL, then watch an X account. Once the two are linked, every new post from that handle arrives as an HMAC-signed POST within seconds of detection, bound by a shared ~60-second poll interval. Every management call is $0; a paid monitoring plan is only needed to watch more than one account at a time. No X developer account required.
Per our own spec, 24 of 121 endpoints are priced at $0, and every webhook management call sits in that group. The number is published in the OpenAPI document as an x-cost-usd field rather than only in prose, so "free" here is a value your client can read and assert on, not a marketing adjective that a later repricing could quietly contradict.
What is the Twitter Webhook API?
Two endpoints, both free. POST /twitter/webhook registers an https delivery URL and returns a signing secret shown exactly once. POST /twitter/monitor then watches an X handle. Point the monitor at your webhook's id and every new post from that account arrives as a tweet.created event, signed HMAC-SHA256 over the timestamp and raw body in an X-TwitterAPIs-Signature header. There is no backfill: only posts made after the monitor is created are delivered. An optional domain_filter narrows delivery to posts linking a given host, and include_replies (default true) can turn off reply delivery. Cost: $0 per call for every webhook and monitor endpoint. Watching a second account onward needs a monitoring plan.
What you can build with account webhooks
One webhook can receive events from every monitor you point at it. The same Bearer key and the same $0 call cost cover every pattern below.
| Use case | What it watches | Why it matters |
|---|---|---|
| Brand and mention alerting | Get pushed the moment a watched competitor or partner handle posts | React in minutes, not on your next poll |
| Trading / alpha signals | Watch a project or founder account for launch and listing posts | No polling loop to run or rate-limit against |
| Support and PR triage | Route a watched account's new posts straight into a queue | One webhook receiver instead of a cron job |
| AI agent event feed | Feed tweet.created events into an LLM classification step | Push-based, so the agent runs on new data, not a timer |
| Content and UGC pipelines | Catch a campaign or brand account's posts as they go live | Kick off downstream clipping or repost jobs immediately |
For a broader event set, mentions, replies, follows and DMs, not only new posts, see the Twitter account activity API, which covers the rest of that surface on the same polling architecture.
Pick the right endpoint for what you're monitoring
A webhook is the right tool when you already know which account you care about and want each new post the moment it is seen. When the thing you watch is a phrase, or posts by other people about an account, a polling read on the same key is the better fit. Pick the pattern before you write the receiver.
| You want | Use | Model |
|---|---|---|
| Every new post from a known account | Monitor + webhook (this page) | Push, $0 per call, first account free |
| Only that account's posts that link to your domain | Monitor with domain_filter | Push, same monitor slot |
| Posts that mention an account | Twitter mentions API | You poll, billed per call |
| A keyword, hashtag or cashtag across X | Twitter search API | You poll, billed per call |
| Mentions, replies, follows and DMs together | Twitter account activity API | Polling architecture over the wider event set |
Weighing push against a polling loop you run yourself? Our poll cadence cost breakdown for 50 accounts prices each interval, and the build vs buy guide for X monitoring covers what a homegrown watcher has to handle. To land deliveries in a workflow tool instead of your own server, see connecting X to n8n.
Register a webhook, create a monitor, verify the signature
Three calls to go live: create a webhook and save the secret it returns once, create a monitor pointed at that webhook's id, then verify the X-TwitterAPIs-Signature header on every delivery your endpoint receives.
Setup + a signature-verifying receiver
# 1. Register your delivery URL. Save "secret" now, it is never returned again.
curl -X POST "https://api.twitterapis.com/twitter/webhook" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/webhooks/twitterapis"}'
# -> { "id": "wh_9f2a1c", "secret": "whsec_...", "status": "active", ... }
# 2. Watch a handle and point it at the webhook you just made.
curl -X POST "https://api.twitterapis.com/twitter/monitor" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"handle": "jack", "webhook_id": "wh_9f2a1c"}'
# -> { "id": "mon_4b1e7a", "status": "active", "poll_interval_ms": ..., ... }
# 3. Fire a one-shot signed test delivery before you rely on it.
curl -X POST "https://api.twitterapis.com/twitter/webhook/wh_9f2a1c/test" \
-H "Authorization: Bearer YOUR_API_KEY"
# -> { "delivered": true, "status_code": 200, "error": null }import hmac
import hashlib
import time
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_..." # from the Create Webhook response, saved once
@app.post("/webhooks/twitterapis")
def receive():
# Header shape: t=<unix seconds>,v1=<hex HMAC-SHA256 of "{t}.{raw body}">
header = request.headers.get("X-TwitterAPIs-Signature", "")
parts = dict(kv.split("=", 1) for kv in header.split(",") if "=" in kv)
timestamp, signature = parts.get("t", ""), parts.get("v1", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
abort(401) # missing or stale: refuse replays older than 5 minutes
signed_payload = timestamp.encode() + b"." + request.get_data() # raw bytes
expected = hmac.new(
WEBHOOK_SECRET.encode(), signed_payload, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected.encode(), signature.encode()):
abort(401)
event = request.get_json()
if event.get("event") == "webhook.test":
return {"ok": True} # a diagnostic send, not a real post
# event["event"] == "tweet.created"
tweet = event["tweet"]
print("New post from", event["handle"], tweet["id"], tweet["text"][:80])
return {"ok": True}const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.raw({ type: "application/json" })); // need the RAW body to verify
const WEBHOOK_SECRET = "whsec_..."; // from the Create Webhook response, saved once
app.post("/webhooks/twitterapis", (req, res) => {
// Header shape: t=<unix seconds>,v1=<hex HMAC-SHA256 of "{t}.{raw body}">
const parts = Object.fromEntries(
(req.header("X-TwitterAPIs-Signature") || "")
.split(",")
.map((kv) => [kv.slice(0, kv.indexOf("=")), kv.slice(kv.indexOf("=") + 1)])
);
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) {
return res.status(401).end(); // missing or stale: refuse replays older than 5 minutes
}
const signedPayload = `${parts.t}.${req.body.toString("utf8")}`;
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(signedPayload)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 || "", "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
if (event.event === "webhook.test") return res.json({ ok: true });
// event.event === "tweet.created"
console.log("New post from", event.handle, event.tweet.id, event.tweet.text.slice(0, 80));
res.json({ ok: true });
});
app.listen(3000);Every call in that setup, the webhook, the monitor, and the test send, is $0. Read the full parameter list on the Monitoring reference docs.
What a delivery looks like: payload, headers and media
Every real delivery is a JSON body with four top-level fields. tweet is the full tweet, the same object the read endpoints return, and handle is the watched account lowercased without the @. A test send uses webhook.test and carries no tweet. Shape per the Webhook Delivery Payload reference (read 2026-10-15).
{
"event": "tweet.created",
"monitor_id": "mon_4b1e7a",
"handle": "jack",
"tweet": {
"id": "1899999999999999999",
"text": "...",
"media": [
{
"type": "video",
"media_url_https": "<poster frame image URL>",
"video_info": {
"duration_millis": 30066,
"variants": [
{ "content_type": "video/mp4", "url": "<mp4 file URL>", "bitrate": 2176000 }
]
}
}
]
}
}| Header | What it carries |
|---|---|
| x-twitterapis-event | tweet.created (webhook.test on a test send) |
| x-twitterapis-event-id | Stable per event, identical on every retry. Deduplicate on this. |
| x-twitterapis-delivery-id | Unique per HTTP attempt, changes on each retry. |
| x-twitterapis-attempt | Attempt number, starting at 1. |
| x-twitterapis-signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "{t}.{raw body}"> |
Media arrives as URLs, never bytes: photos carry media_url_https, video and animated GIFs add video_info.variants, and a text-only post has no media key at all. Register a Slack or Discord webhook URL instead and the post is sent in that platform's native message format with its images, minus the signature headers neither platform can check.
Retries, dead letters and replay: the delivery contract
What happens when your receiver is slow, down or gone. Each row links the reference page it comes from.
| Behaviour | Rule | Source |
|---|---|---|
| Delivery guarantee | At least once. A tweet can arrive twice, so make your handler idempotent. | Create Webhook notes |
| Retries | 8 attempts over 21 minutes with jitter when your endpoint does not answer 2xx. | Create Webhook notes |
| Dead letter | After the 8th failed attempt the delivery is held, not dropped, and is never sent again until you call redrive. | Replay Dead Deliveries |
| Replay | POST /twitter/webhook/{id}/redrive re-queues dead deliveries: max_age_hours default 24 (1 to 168), limit default 100 (1 to 1,000). | Replay Dead Deliveries |
| 410 Gone | Answering 410 disables the webhook at once. Dead deliveries from a 410 are reported as skipped_permanent, never replayed. | Replay Dead Deliveries |
| Ordering | Not guaranteed. Sort by tweet id if you need chronological order. | Create Webhook notes |
| Deduplication | x-twitterapis-event-id is stable across every retry of one tweet; x-twitterapis-delivery-id changes per attempt. | Webhook Delivery Payload |
| Destination URL | HTTPS on a public address. localhost, private, link-local and metadata addresses are refused at registration. | Create Webhook |
A test send is the exception: it is never queued, retried or dead-lettered, returns HTTP 200 when your endpoint answered 2xx and 502 when it did not, and still works on a webhook a 410 has disabled, so you can confirm a fix before you re-register.
Three free endpoints to audit delivery, not just trust it
A push architecture is only as good as its observability. Monitoring health is exposed as data, not a support ticket, and every read below is free.
GET /twitter/monitor/{id}/health
One monitor's cursor: last tweet id seen, last poll time, and a degraded flag if it is running behind the published interval.
GET /twitter/monitor/health
Account-wide rollup, no id needed: active vs paused monitor counts, plus pending, delivered and failed deliveries over the last 24 hours.
GET /twitter/monitor/deliveries
Your most recent delivery events with real per-event detected_lag_ms and delivery_lag_ms, not a marketing average.
A real measured event from the delivery log: detection took 33,984 ms (~34 s, bound by the shared poll interval) and delivery took 3,596 ms (~3.6 s) from detection to the webhook POST landing, for about 37.6 seconds end to end (event of 2026-08-13, published as the response example on the Monitor Deliveries reference). That is the honest number this feature ships with, not a best-case claim.
Pause, resume, repoint or remove a monitor
The rest of the lifecycle, every call $0. Resuming a paused monitor re-runs the same capacity check as creating one, so it can return 503 when monitoring capacity is full; an include_replies-only change never does, because it narrows delivery without adding polling load.
| Call | What it does |
|---|---|
| GET /twitter/webhook | List your webhooks. The signing secret is never returned again. |
| DELETE /twitter/webhook/{id} | Remove a webhook. Monitors pointed at it stop delivering until repointed. |
| GET /twitter/monitor | List monitors with status, degraded and events_possibly_missed flags. |
| POST /twitter/monitor/{id} | Pause, resume, repoint webhook_id, change domain_filter or include_replies, in one call. |
| DELETE /twitter/monitor/{id} | Stop watching an account. There is no undelete. |
What happens if I hit the rate limit?
There are three different limits here, and only one of them is a classic request rate limit. Each answers with its own status, so your client can tell them apart.
- Request rate (429). Management calls are free but not unmetered in time: a burst can return
429 rate_limited. Back off exponentially and retry; nothing is billed either way, per the Create Monitor reference. - Monitoring capacity (503). Creating or resuming a monitor returns
503 capacity_unavailablewhen monitoring capacity cannot take one more monitor at the published interval. It is a capacity signal, not a rejection of your key. Retry later. - Plan slots. The number of accounts you can watch at once is the slot count of your monitoring plan, from 1 on Free to 2,000 on Scale. Move to a smaller plan while watching more accounts than it allows and the newest monitors pause, they are not deleted, until you upgrade again.
What you never manage is X's own rate limit on the watched account: polling runs on our side, so your key spends nothing per poll however often a watched handle posts. The monitor response includes poll_interval_ms, which the docs mark as not yet a published SLA.
Every call is free. Plans cover watching more accounts.
This is the one part of TwitterAPIs that is not priced per call. Creating, updating, deleting and reading webhooks and monitors costs $0 regardless of plan. The first monitored account is free forever; watching more requires a flat monthly slot plan.
| Plan | Price | Monitored accounts | ~2 s delivery badge |
|---|---|---|---|
| Free | $0 | 1 account | No |
| Starter | $15/mo | 6 accounts | No |
| Growth | $47/mo | 20 accounts | Yes |
| Professional | $95/mo | 50 accounts | Yes |
| Enterprise | $199/mo | 150 accounts | Yes |
| Enterprise Plus | $399/mo | 500 accounts | Yes |
| Scale | $799/mo | 2,000 accounts | Yes |
Every new account also gets $0.50 free credit on signup, no credit card required, for the paid reads on the same key, such as the search or mentions polling the table above points to.
No API credits are ever deducted for monitoring. See live checkout and full plan detail on the pricing page's monitoring section, or explore the full API docs.
Webhook cost estimator
What watching 1 to 2,000 accounts costs a month. Monitoring is billed by how many accounts you watch, never by how many posts they make. Find the row that holds your list and read across: that plan covers every delivery for the month, whether the accounts post twice or two thousand times.
| Accounts you watch | Plan | Monthly | Per account |
|---|---|---|---|
| 1 | Free | $0 | $0.00 |
| 6 | Starter | $15 | $2.50 |
| 20 | Growth | $47 | $2.35 |
| 50 | Professional | $95 | $1.90 |
| 150 | Enterprise | $199 | $1.33 |
| 500 | Enterprise Plus | $399 | $0.80 |
| 2,000 | Scale | $799 | $0.40 |
The only per-call cost is what you choose to read on top of a delivery. Fetching the replies to a delivered post is one standard read at $0.0008, so enriching 1,000 deliveries a month adds about $0.80. Watching 7 accounts means the 20-account Growth plan.
Brand monitoring with webhooks
Your accounts, the replies they draw, and the posts that link to your site.
A monitor watches one account and delivers every post that account makes, so the first step in brand monitoring is one monitor for the brand account and one for each founder or spokesperson whose posts your team answers for.
Replies are included by default (include_replies is true), which matters for a brand: a support reply from your account is the post most likely to be quoted back at you. Set it to false on an account where you only want top-level announcements.
domain_filter narrows a monitor to posts that link a domain. It matches by hostname boundary, so example.com matches blog.example.com and never notexample.com. Point it at a partner, reseller or press account to hear only the posts that link your site.
A monitor delivers the posts its own account makes. For posts addressed to your handle, poll the mentions endpoint on a schedule, and run a search for the brand name for everything else. Both feed the same handler as your webhook deliveries.
Accounts per monitor and per plan
Each monitor takes one handle, so the number of monitors on a key equals the number of accounts on its plan: 1 on the free slot, then 6, 20, 50, 150, 500 or 2,000. One account per monitor keeps each account's status, pause and cursor separate, so pausing a noisy account never touches the others.
Create them with POST /twitter/monitor, one call per handle, at no cost. At your plan's cap the call returns 403 with an upgrade message. If monitoring capacity cannot serve one more monitor at the published interval it returns 503 capacity_unavailable, and the same call can succeed on a later retry. Only posts made after a monitor is created are delivered.
TwitterAPIs webhooks vs the official X API and self-run polling
| Option | Price | Auth | Coverage | Delivery |
|---|---|---|---|---|
| TwitterAPIs monitor + webhook | $0 per call, free 1-account tier, plans from $15/mo for more | Bearer token, 30 s signup | Shared 60 s poll interval; watch a handle or a domain-filtered subset | Signed POST within ~2 s of detection (Growth plan and up) |
| X V2 Webhooks API (official) | Billed on X's own developer plans | Approved X developer account, a Project and App, OAuth 2.0 App Only token | Account Activity, X Activity and filtered stream events, not only new posts | Push, but your server must answer CRC checks and reply within 10 s |
| Poll GET /twitter/user/mentions yourself | $0.0008 per call, no subscription | Bearer token, 30 s signup | You run the loop and pay per poll | As fast as you're willing to poll |
The official row reflects X's V2 Webhooks API introduction (read 2026-10-15): an approved developer account and App are prerequisites, the URL must be HTTPS with no port, your server must reply within 10 seconds, and it must answer a Challenge-Response Check on registration and periodically after that or the webhook is marked invalid. X signs each POST with a base64 HMAC-SHA256 of the raw body keyed with your app's client secret. Here the secret is per webhook, and the signature also covers a timestamp, so a receiver can refuse a replayed request older than five minutes.
Need the wider event set, mentions, replies, follows and DMs, not just new posts, see the Twitter account activity API, which runs the same polling architecture over that broader surface.
Wire webhook events into an AI agent via MCP
Install the TwitterAPIs MCP server (@twitterapis/mcp) and webhook and monitor management become native tool calls any MCP-compatible model can invoke, create a monitor, check its health, or read recent deliveries, all in a single tool turn. Already integrated against an x_user_stream-shaped API? The oapi/x_user_stream/add_user_to_monitor_tweet compat route accepts that same request shape and resolves to the identical underlying monitor, so a migration can move at your own pace. For keyword or hashtag tracking instead of an account handle, the Twitter search API runs on the same key. To poll an account's mentions instead of pushing its new posts, use the Twitter mentions API. Need a key? Get a Twitter API key in 30 seconds.
Twitter webhook API FAQ
Two free endpoints. POST /twitter/webhook registers an HTTPS URL you control and returns an HMAC signing secret, shown once, that you use to verify deliveries. POST /twitter/monitor then watches an X account and, once you point it at a webhook, POSTs a signed tweet.created event to that URL every time the watched handle publishes. There is no backfill: only posts made after the monitor was created are delivered. Both endpoints are zero-rated, they never consume API credits.
Two separate numbers matter. Detection is bound by the shared poll interval, currently around 60 seconds across every plan and not a published SLA, so a new post can take up to about a minute to be seen. Delivery, the time from us detecting the post to our POST landing on your endpoint, is typically a couple of seconds on the Growth plan and above; on a real production event measured after shipping this feature, detection took about 34 seconds and delivery took about 3.6 seconds, for roughly 38 seconds end to end. This is a fast-polling architecture, not a firehose, and it is not marketed as sub-second push.
Every delivery is signed HMAC-SHA256 over the timestamp and the raw request body, carried in an X-TwitterAPIs-Signature header. Recompute the signature on your end using the secret returned once at webhook creation and compare it to the header before trusting the payload. A one-shot test delivery (POST /twitter/webhook/{id}/test) sends a real signed event with event set to webhook.test rather than tweet.created, so you can confirm your receiver and signature check both work before relying on it.
Creating or resuming a monitor can return 503 capacity_unavailable if monitoring capacity cannot currently serve one more monitor at the published interval. Retry later; this is a capacity signal, not a rejection of your account or your webhook.
Each delivery gets 8 attempts over 21 minutes with jitter. If every attempt fails, the delivery is dead-lettered: we still hold the event, but it is not sent again until you call POST /twitter/webhook/{id}/redrive, which re-queues dead deliveries from the last 24 hours by default (up to 168) and up to 100 per call by default (up to 1,000). Delivery is at least once, so deduplicate on the x-twitterapis-event-id header, which stays the same across every retry of one tweet.
Yes. Register a Slack incoming webhook URL (hooks.slack.com) or a Discord webhook URL (discord.com/api/webhooks) and the delivery is sent in that platform's own message format, with the author, the text, a link to the post and its images. Neither platform verifies an HMAC, so the signature headers are omitted for those two destinations. Point the monitor at an endpoint you control if you want the full JSON payload.
X's V2 Webhooks API sends a Challenge-Response Check GET to your URL on registration and periodically after that, and your server must answer it with an HMAC of X's token or the webhook is marked invalid; X also requires a reply within 10 seconds and a URL with no port. TwitterAPIs has no CRC step: you prove the endpoint works with a one-shot POST /twitter/webhook/{id}/test, and you verify each delivery with the timestamped HMAC-SHA256 signature.
Every call to create, list, update or delete a webhook or a monitor is $0, including the health and delivery-log endpoints. What is not free is watching more than one account at a time: the first monitor on any account is a free slot, and watching more requires a monitoring plan, Starter at $15/mo for 6 slots up to Scale at $799/mo for 2,000, billed as a flat monthly subscription rather than the per-call model the rest of the API uses. See the full breakdown on the pricing page's monitoring section.
No. You need only a TwitterAPIs Bearer token, issued after a 30-second signup. There is no X developer application, no app review, and no enterprise sales process, which is the gate that keeps X's own first-party Account Activity webhooks out of reach for most builders.
Not yet through the webhook path. A monitor watches one X handle, optionally narrowed with a domain_filter (deliver only posts linking to a given host) or include_replies (turn reply delivery off). For keyword or hashtag tracking, run GET /twitter/search/tweet-advanced-search on a polling loop instead, using the same Bearer key at $0.0008 per call, see the Twitter search API.
Three read endpoints, all free. GET /twitter/monitor/{id}/health returns one monitor's cursor: its last seen tweet id, last poll time, and a degraded flag. GET /twitter/monitor/health is an account-wide rollup with no id needed, active and paused counts plus pending, delivered and failed deliveries over the last 24 hours. GET /twitter/monitor/deliveries lists your most recent delivery events with real measured detected_lag_ms and delivery_lag_ms per event, so you can audit actual latency rather than trust a published number.
Yes, as URLs, never as bytes. When a post carries media, the tweet object has both tweet.extended_entities.media and a top-level tweet.media alias holding the same array; a text-only post has neither key. Video and animated GIF entries add video_info.variants with an mp4 url and bitrate per variant, and media_url_https on those is the poster frame, not the video. Treat the URLs as untrusted input and validate the host before you fetch one.
No. The secret is returned only in the response to POST /twitter/webhook and no endpoint returns it again, including GET /twitter/webhook. Delete the webhook, create a new one to get a fresh secret, and repoint your monitors at the new webhook id with POST /twitter/monitor/{id}.
No. add_user_to_monitor_tweet, remove_user_to_monitor_tweet and get_user_to_monitor_tweet are compat drop-ins that translate an x_user_stream-shaped request onto the same underlying monitor system, so a migration can point at the compat routes first and move to monitor/webhook directly on your own schedule. Both paths bill the same, which is to say nothing.
Get pushed the moment an account posts
Every webhook and monitor call is $0. First account free, plans from $15/mo to watch more. No X developer account required.
Next read
Continue exploring related pages:
Twitter account activity API
Watch mentions, follows and DMs without enterprise webhooks.
Twitter search API
Real-time search with operators via the advanced_search endpoint, $0.0008 a call, $0.04 per 1,000 tweets on full 20-tweet pages.
TwitterAPIs pricing
Brand pricing page with endpoint-level costs and quick totals.