Account monitoring
Watch any public X account, with no follower minimum, and get its new tweets pushed to a signed webhook or pulled from an event feed within 60 s, 5 s or 1 s depending on the plan. Monthly plans, managed under /monitors.
Account monitoring watches X accounts for you and sends every new tweet they post to your webhook as soon as it is detected. You add the accounts once; there is no polling on your side and no charge per request. Each plan is a flat monthly fee for a number of watched accounts and a speed class.
How it works
- You book a plan in the dashboard (Monitors). The monthly fee comes out of your prepaid balance.
- You add the accounts to watch with
POST /monitors/accounts. - You set a webhook URL with
PUT /monitors/webhook, or read new tweets fromGET /monitors/events, or both. - We check the accounts' timelines at the interval of your speed class. Every new tweet becomes one event, signed and sent to your webhook, and kept for 7 days in the event feed.
Any public account can be watched, whatever its size: there is no follower minimum, and an account with 50 followers is checked as often as one with 5 million. Tweets that already existed when an account was added are not sent; delivery starts with the first tweet after monitoring begins. An account watched by several customers is checked once and fanned out to all of them, so a popular account is not slower than a niche one.
Speed classes
| Class | Guarantee | Typical |
|---|---|---|
| Standard | We check at least every 60 s | 15–60 s |
| Fast | We check at least every 5 s | 2–5 s |
| Real-time | We check at least every 1 s | 0.5–1 s |
The guarantee is about how often we look: an account in the Real-time class is never checked less often than once per second, even when that costs us more than the plan brings in. When there is spare capacity we check more often than the guarantee.
Detection latency is the time from the tweet being posted to the event being created. It has two parts:
- X's own delay: the time X takes before a new tweet shows up in its timelines. It is usually short, but it is outside our control and we cannot guarantee it.
- Our polling interval: at most the guarantee of your class (60 s, 5 s or 1 s), on average about half of it.
Webhook delivery adds the round trip to your server. Every event carries tweetCreatedAt (from X) and detectedAt (when we saw it), so you can measure the latency yourself.
Plans and prices
Monthly prices in USD, charged from your balance:
| Accounts | Standard (60 s) | Fast (5 s) | Real-time (1 s) |
|---|---|---|---|
| 10 | $5 | $9 | $19 |
| 50 | $15 | $29 | $59 |
| 250 | $49 | $99 | $199 |
| 1,000 | $149 | $299 | $599 |
| 5,000 | $499 | $999 | $1,999 |
- The plan renews every month from your balance. If the balance does not cover the renewal, the plan is paused: your watched accounts are kept, no events are created and adding accounts returns
402. Top up and renew in the dashboard to resume. - An upgrade (more accounts or a faster class) applies at once; you pay the prorated difference for the rest of the period. A downgrade applies at the end of the period.
- Requests to
/monitors/*are not billed per call. They count toward your normal rate limit.
API
Base URL https://api.twittrapi.com, authenticated with your normal X-API-KEY header. Request and response bodies are JSON. Errors have the form {"error": "..."}.
| Method | Path | |
|---|---|---|
GET | /monitors | Plan and webhook |
GET | /monitors/accounts | Watched accounts and plan usage |
POST | /monitors/accounts | Watch accounts |
DELETE | /monitors/accounts/{usernameOrId} | Stop watching an account |
GET | /monitors/webhook | Current webhook |
PUT | /monitors/webhook | Set or change the webhook (POST works too) |
DELETE | /monitors/webhook | Remove the webhook |
POST | /monitors/webhook/test | Send a test event |
GET | /monitors/events | Pull API: events after a given event |
GET | /monitors/deliveries | The latest events with their delivery status (limit up to 200, default 50) |
Watch accounts
curl -X POST 'https://api.twittrapi.com/monitors/accounts' \
--header 'X-API-KEY: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{"usernames": ["XDevelopers", "elonmusk"]}'curl -X POST 'https://twitter-aio.p.rapidapi.com/monitors/accounts' \
--header 'x-rapidapi-host: twitter-aio.p.rapidapi.com' \
--header 'x-rapidapi-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{"usernames": ["XDevelopers", "elonmusk"]}'The body takes username, userId, usernames or userIds (with or without @), up to 100 accounts per request. Each account gets its own result:
{
"data": [
{ "input": "XDevelopers", "userId": "2244994945", "username": "XDevelopers", "status": "added" },
{ "input": "elonmusk", "userId": "44196397", "username": "elonmusk", "status": "already_watching" }
],
"plan": {
"planId": "m50-realtime",
"maxAccounts": 50,
"usedAccounts": 12,
"latencyClass": "realtime",
"status": "active"
}
}A failed account has an error instead of a status: not found on X, protected, not a valid username, or the plan's account limit reached. With a single account the HTTP status reflects the result (201 added, 404, 422 protected, 402 limit reached); with several it is 200 and you read the results one by one. Without a plan, or with a paused plan, the whole request returns 402.
GET /monitors/accounts lists the watched accounts with userId, username, state, latencyClass, subscribedAt and monitoringSince. A new account has the state pending until monitoring of it starts, then active; monitoringSince is set from then on.
Set the webhook
curl -X PUT 'https://api.twittrapi.com/monitors/webhook' \
--header 'X-API-KEY: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{"url": "https://example.com/hooks/twittrapi"}'curl -X PUT 'https://twitter-aio.p.rapidapi.com/monitors/webhook' \
--header 'x-rapidapi-host: twitter-aio.p.rapidapi.com' \
--header 'x-rapidapi-key: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{"url": "https://example.com/hooks/twittrapi"}'{
"webhook": {
"id": 7,
"url": "https://example.com/hooks/twittrapi",
"secret": "whsec_3f1c...",
"active": true,
"failureCount": 0,
"disabledAt": null,
"disabledReason": null,
"lastSuccessAt": null,
"lastFailureAt": null
}
}The first PUT creates a signing secret (whsec_ followed by 48 hex characters). Changing the URL keeps the secret; send "rotateSecret": true to get a new one. Setting the webhook again also re-enables it after it was disabled.
The URL must be https, without credentials, on a public host: private, loopback and link-local addresses (and host names that resolve to them) are refused, and redirects are not followed.
POST /monitors/webhook/test sends a signed test event and answers with {"ok": true, "statusCode": 200, "latencyMs": 84} (or ok: false and an error).
Webhook events
Each new tweet is one POST to your URL with this JSON body:
{
"event": "tweet",
"id": "1849203",
"monitoredUserId": "2244994945",
"tweetId": "2019881223666233717",
"tweetCreatedAt": "2026-02-06T21:09:45Z",
"detectedAt": "2026-02-06T21:09:46.212Z",
"tweet": {
"id": "2019881223666233717",
"url": "https://x.com/XDevelopers/status/2019881223666233717",
"text": "Officially launching X API Pay-Per-Use …",
"createdAt": "2026-02-06T21:09:45Z",
"lang": "en",
"author": { "id": "2244994945", "username": "XDevelopers", "name": "Developers", "...": "..." },
"counts": { "likes": 0, "retweets": 0, "replies": 0, "quotes": 0, "bookmarks": 0, "views": 0 },
"media": [],
"replyTo": null,
"quoted": null,
"retweetOf": null,
"conversationId": "2019881223666233717"
}
}tweetis a tweet in the simple format, the same object you get from the read endpoints withformat=simple. A retweet hasretweetOf, a reply hasreplyTo.idis the event ID. It increases with every event and is what you pass assinceto the pull API.monitoredUserIdis the watched account the event belongs to.
Headers:
| Header | |
|---|---|
X-TwittrAPI-Event | tweet (or test for the test event) |
X-TwittrAPI-Delivery | Event ID, the same in every retry. Use it to drop duplicates. |
X-TwittrAPI-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Content-Type | application/json |
User-Agent | TwittrAPI-Monitor/1.0 |
Answer with any 2xx status within 5 seconds. Do the work after answering (put the event on a queue), not before.
Verify the signature
v1 is HMAC-SHA256 with your webhook secret as key over the string <t>.<raw body>: the timestamp from the header, a dot, and the request body exactly as received. Compute it on the raw bytes, before any JSON parsing, compare it in constant time, and reject timestamps more than 5 minutes away from your clock so a captured request cannot be replayed.
Node.js
import crypto from "node:crypto";
import express from "express";
const secret = process.env.TWITTRAPI_WEBHOOK_SECRET;
const app = express();
function verify(header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=", 2)),
);
const t = Number(parts.t);
if (!t || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.`)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/hooks/twittrapi", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.get("X-TwittrAPI-Signature") ?? "", req.body)) {
return res.sendStatus(401);
}
res.sendStatus(204);
const event = JSON.parse(req.body);
// queue event.tweet for processing; dedupe on req.get("X-TwittrAPI-Delivery")
});
app.listen(3000);Python
import hashlib, hmac, os, time
from flask import Flask, abort, request
SECRET = os.environ["TWITTRAPI_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
def verify(header: str, raw_body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t, sig = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or not sig or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(SECRET, t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
@app.post("/hooks/twittrapi")
def hook():
if not verify(request.headers.get("X-TwittrAPI-Signature", ""), request.get_data()):
abort(401)
event = request.get_json()
# queue event["tweet"]; dedupe on request.headers["X-TwittrAPI-Delivery"]
return "", 204Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
var secret = []byte(os.Getenv("TWITTRAPI_WEBHOOK_SECRET"))
func verify(header string, body []byte, tolerance time.Duration) bool {
var t, sig string
for _, part := range strings.Split(header, ",") {
k, v, _ := strings.Cut(strings.TrimSpace(part), "=")
switch k {
case "t":
t = v
case "v1":
sig = v
}
}
ts, err := strconv.ParseInt(t, 10, 64)
if err != nil || sig == "" {
return false
}
if d := time.Since(time.Unix(ts, 0)); d > tolerance || d < -tolerance {
return false
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(t + "."))
mac.Write(body)
return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(sig))
}
func main() {
http.HandleFunc("POST /hooks/twittrapi", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil || !verify(r.Header.Get("X-TwittrAPI-Signature"), body, 5*time.Minute) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusNoContent)
// queue body for processing; dedupe on r.Header.Get("X-TwittrAPI-Delivery")
})
http.ListenAndServe(":3000", nil)
}Retries and auto-disable
A delivery fails when your server does not answer with 2xx within 5 seconds (timeouts, connection errors, 3xx, 4xx and 5xx all count). Failed deliveries are retried with exponential backoff: after 10 s, 20 s, 40 s and so on, doubling up to at most 2 hours between attempts, with ±20 % jitter. A delivery that has not succeeded 24 hours after the tweet was detected is given up and marked failed.
Retries can arrive out of order and, rarely, more than once. Use X-TwittrAPI-Delivery to drop duplicates and tweetCreatedAt to order.
After 20 failed deliveries in a row the webhook is disabled (active: false, with disabledAt and disabledReason) and we email you. Monitoring continues: new events are still created and stay available through the pull API. Fix your endpoint, then send PUT /monitors/webhook again to re-enable it.
Pull API
If you cannot receive webhooks, or to catch up after downtime, read the events directly:
curl 'https://api.twittrapi.com/monitors/events?since=1849203&limit=100' \
--header 'X-API-KEY: YOUR_API_KEY'curl 'https://twitter-aio.p.rapidapi.com/monitors/events?since=1849203&limit=100' \
--header 'x-rapidapi-host: twitter-aio.p.rapidapi.com' \
--header 'x-rapidapi-key: YOUR_API_KEY'{
"data": [
{
"event": "tweet",
"id": "1849204",
"monitoredUserId": "2244994945",
"tweetId": "2019881223666233718",
"tweetCreatedAt": "2026-02-06T21:12:03Z",
"detectedAt": "2026-02-06T21:12:04.017Z",
"tweet": { "id": "2019881223666233718", "...": "..." },
"status": "delivered",
"attempts": 1,
"deliveredAt": "2026-02-06T21:12:04.231Z",
"lastError": null
}
],
"nextSince": "1849204",
"hasMore": false
}sinceis an event ID (exclusive) or an RFC 3339 time. Without it you get the oldest events still kept.limitis 1–500, default 100. Events come oldest first.- Store
nextSinceand pass it assinceon the next call. WhenhasMoreistrue, call again right away. statusisstored(no webhook set),pending(webhook delivery still being tried),deliveredorfailed.
Events are kept for 7 days, then deleted. We do not keep an archive of tweets beyond that.
Limits
| Accounts per plan | 10, 50, 250, 1,000 or 5,000 |
Accounts per POST /monitors/accounts | 100 |
| Accounts that can be watched | Any public account, no follower minimum (protected accounts return 422) |
| Webhooks | One per API account, https only, public host |
| Webhook timeout | 5 s |
| Retries | Up to 24 h per event, backoff up to 2 h |
| Auto-disable | After 20 failed deliveries in a row |
| Event retention (pull API) | 7 days |
| Pull API page size | 1–500 events (default 100) |
| Request body | 64 KB |
Requests to /monitors/* | Your normal rate limit, no per-call charge |
FAQ
Are replies and retweets included?
We do not filter: every tweet of the account that X returns to us is sent. Retweets carry retweetOf, replies replyTo and quotes quoted, so you can filter on your side if you only want original tweets.
Do small accounts get the same speed? Yes. There is no follower threshold and no separate slow lane: every public account you add is checked at the guaranteed interval of your class, whether it has 50 followers or 5 million.
Can I watch protected accounts? No. Only public accounts can be watched.
Do I get old tweets when I add an account? No. Delivery starts with the first tweet after monitoring of the account begins. Use the user tweets endpoint for history.
What happens to deleted tweets? An event is sent when the tweet is detected. Deletions are not sent as separate events.
Is the 1 second a guarantee of end-to-end latency?
No. It guarantees how often we check (at least once per second for Real-time). The time X needs before a new tweet appears is added on top and is outside our control. Compare tweetCreatedAt and detectedAt to see the actual latency.
What if my balance runs out? The plan is paused at renewal: watched accounts are kept, no events are created. Top up and renew in the dashboard and monitoring continues.
Does the same account cost more if many people watch it? No. Price depends only on the number of accounts in your plan and the speed class.
How do I test my endpoint?
POST /monitors/webhook/test sends a signed test event with X-TwittrAPI-Event: test. Your handler should verify and accept it like a real one.