Docs
Account monitoring

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.

Video: Account monitoring: new tweets by webhook (2 min)

How it works

  1. You book a plan in the dashboard (Monitors). The monthly fee comes out of your prepaid balance.
  2. You add the accounts to watch with POST /monitors/accounts.
  3. You set a webhook URL with PUT /monitors/webhook, or read new tweets from GET /monitors/events, or both.
  4. 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

ClassGuaranteeTypical
StandardWe check at least every 60 s15–60 s
FastWe check at least every 5 s2–5 s
Real-timeWe check at least every 1 s0.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:

AccountsStandard (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": "..."}.

MethodPath
GET/monitorsPlan and webhook
GET/monitors/accountsWatched accounts and plan usage
POST/monitors/accountsWatch accounts
DELETE/monitors/accounts/{usernameOrId}Stop watching an account
GET/monitors/webhookCurrent webhook
PUT/monitors/webhookSet or change the webhook (POST works too)
DELETE/monitors/webhookRemove the webhook
POST/monitors/webhook/testSend a test event
GET/monitors/eventsPull API: events after a given event
GET/monitors/deliveriesThe 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"]}'

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"}'
{
  "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"
  }
}
  • tweet is a tweet in the simple format, the same object you get from the read endpoints with format=simple. A retweet has retweetOf, a reply has replyTo.
  • id is the event ID. It increases with every event and is what you pass as since to the pull API.
  • monitoredUserId is the watched account the event belongs to.

Headers:

Header
X-TwittrAPI-Eventtweet (or test for the test event)
X-TwittrAPI-DeliveryEvent ID, the same in every retry. Use it to drop duplicates.
X-TwittrAPI-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Content-Typeapplication/json
User-AgentTwittrAPI-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 "", 204

Go

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'
{
  "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
}
  • since is an event ID (exclusive) or an RFC 3339 time. Without it you get the oldest events still kept.
  • limit is 1–500, default 100. Events come oldest first.
  • Store nextSince and pass it as since on the next call. When hasMore is true, call again right away.
  • status is stored (no webhook set), pending (webhook delivery still being tried), delivered or failed.

Events are kept for 7 days, then deleted. We do not keep an archive of tweets beyond that.

Limits

Accounts per plan10, 50, 250, 1,000 or 5,000
Accounts per POST /monitors/accounts100
Accounts that can be watchedAny public account, no follower minimum (protected accounts return 422)
WebhooksOne per API account, https only, public host
Webhook timeout5 s
RetriesUp to 24 h per event, backoff up to 2 h
Auto-disableAfter 20 failed deliveries in a row
Event retention (pull API)7 days
Pull API page size1–500 events (default 100)
Request body64 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.