How do you track a tweet with an API?
Last updated August 24, 2026
Two shapes answer this. Re-reading tweet/detail on a timer costs $0.0008 each time and gives you fresh favorite_count and view_count numbers. A monitor is a standing watch on a handle that pushes new posts to your endpoint for nothing. Prove it is working through monitor/health, which reports deliveries_24h split into pending, delivered and failed.
Every rate here is the pricing TwitterAPIs publishes. The billed rate is $0.0008 per call; $0.04 per 1,000 tweets is derived from it at a full 20-tweet page, which is the default page size rather than a guaranteed yield (source: twitterapis.com/pricing).
Polling one post versus watching an account
These answer different questions. tweet/detail resolves one id into its object with favorite_count, retweet_count, reply_count and view_count as they stood at fetch time, so calling it repeatedly is how you chart a post's numbers moving. A monitor never revisits a post that already exists: it watches a handle, tells you when a new one appears, and stops caring about it after that. Charting a known post means polling, catching arrivals means a monitor, and a system that needs both runs both. They also differ in how they prove themselves. A poll proves itself by returning a body. A watch is silence by design, so it needs its own health routes before you can trust the quiet.
What one poll actually returns
tweet/detail takes a single required parameter, id, the numeric post id as a string. The response is small: a top-level id echoing what you asked for, and a tweet object holding text with URLs already expanded, created_at, a nested author, and the four engagement counters. Every counter is documented as a value at fetch time rather than a live figure, so two reads minutes apart can legitimately disagree and neither one is wrong. view_count is the field to treat as optional, since it is present only when the source exposes it. Store the moment of each read alongside the numbers, otherwise a genuinely flat series and a collector that quietly stopped look identical in your table later.
What a polling loop costs
A standard read is $0.0008, so the arithmetic is worth doing before you write the loop rather than after. Checking one post every minute is 1,440 calls a day, which comes to $1.15 a day and about $34.56 across thirty days for that single post. Ten posts on the same cadence is ten times that. The $0.50 of signup credit covers 625 calls, so a once-a-minute watch exhausts it in roughly ten hours. Dropping to once every five minutes takes the same job to 288 calls a day. Pick the interval from how fast the numbers you care about actually move, since most engagement curves stop repaying minute resolution after the first hour.
Creating a standing watch
A monitor takes handle, the X username without the @, plus the webhook_ids it should deliver to. The response echoes a normalized handle, lowercased with the @ stripped, along with id, status, domain_filter, include_replies, poll_interval_ms and created_at. The first poll after creation sets a baseline at that account's newest post, and there is no backfill, so watching an account with ten thousand existing posts sends zero webhooks for any of them. Only what the handle publishes afterwards is delivered. Creation can be refused with 503 capacity_unavailable when monitoring is at capacity and cannot take another watch at the published interval, which is a retry-later answer rather than a fault in your request.
Narrowing what gets delivered
Two optional switches shape delivery without changing what is polled. domain_filter restricts delivery to posts linking a given host and is normalized server-side: lowercased, with scheme, path, query, fragment, a leading www. and a trailing port all stripped, then rejected with 400 if what remains is not a valid hostname shape. Matching is by hostname boundary rather than substring, so example.com matches blog.example.com and never notexample.com. include_replies defaults to true and has to be a real boolean, because a string or a number is refused with 400 rather than coerced. Filtered-out posts still advance the cursor and count toward the account's tweets_domain_filtered metric, so they are excluded from delivery rather than lost.
Reading one watch's cursor
GET monitor/{id}/health returns id, status, degraded, poll_interval_ms, events_possibly_missed, last_tweet_id and last_poll_at. The two booleans are what you alert on. degraded means this watch is not being polled as expected, and events_possibly_missed means a gap in polling could have swallowed a post. last_tweet_id and last_poll_at are both null until the first poll tick has run, so a freshly created watch legitimately looks empty for a moment and your alerting should not read that as a fault. Compare last_poll_at against poll_interval_ms to judge how stale a cursor really is, rather than trusting the interval figure on its own. The id on the response is only an echo of the one you passed in the path, so it tells you nothing new.
The account-wide rollup
GET monitor/health answers for the whole key with no path parameter, which also means it never returns 404. The body has three parts: status, either operational or degraded when any active watch is being served slower than the published interval, a monitors object counting active, paused and total, and a deliveries_24h object counting pending, delivered and failed attempts over the last day. A key with no watches gets 200 with every count at zero rather than an error, so it is safe to hit from a dashboard on a fixed timer. failed climbing while delivered stays flat points at your receiver, not at the polling side. pending is the bucket to read next, because a queue that keeps growing means attempts are being made and not landing.
Measuring real lag instead of assuming it
GET monitor/deliveries lists recent events newest first, with limit accepting 1 to 200 and defaulting to 50. Each event carries id, monitor_id, tweet_id, status, tweet_created_at, detected_at, delivered_at and three integers. detected_lag_ms is X's own post timestamp subtracted from the moment the scheduler queued the send, delivery_lag_ms is queue to POST, and total_lag_ms adds the two. A null in any of them means one timestamp was unavailable for that specific event rather than an error. The documented sample event shows 33984, 3596 and 37580 respectively, which tells you the shape of the answer rather than promising anything about yours. The feed is scoped to your own key, so nothing belonging to another account ever appears in it.
Why poll_interval_ms is not a guarantee
The field reporting the interval says so itself: it is documented as not a published SLA yet, subject to change as real X search-index ingestion lag gets measured. The deliveries route repeats the point, describing detected_lag_ms as the honest number for how fast detection currently is while noting it is bound by the current poll interval rather than by any commitment. So do not derive an alert threshold from the interval. Pull a few hundred real events, read your own detected_lag_ms distribution, and set the threshold from that, then keep watching it, because the figure moves with overall monitoring load rather than with anything on your account. Alert on a change in that distribution rather than on any absolute millisecond number. $0.0008 a call, per the rates we publish.
The API key, and which tools are free
Everything on this page runs on one API key, issued when you sign up and sent as Authorization: Bearer on every request, with no developer account or OAuth app behind it. The same value can be sent under an x-api-key header instead if your HTTP client prefers that. What makes tracking cheap is which tools on that key are free. Creating, listing, updating, pausing and deleting monitors costs nothing. So do both health routes, the per-watch cursor and the account-wide rollup, and the deliveries feed that reports measured lag. Registering a webhook, sending it a signed test event and replaying deliveries that died after their retries ran out are free as well, and account/me reports your balance at no charge. The billed tools are the reads: tweet/detail at $0.0008 when you re-read a post's counters, tweet/replies and tweet/quotes at the same rate when you want the conversation around it, and tweet/thread at $0.004 when you need the whole chain at once. A tracker built on that split spends money only on questions a push cannot answer, which is how a standing watch on an account ends up costing nothing for as long as it runs.
Paying only for the data you get
A tracking job reads the same objects again and again, so the way a meter counts matters more here than almost anywhere. Here you pay per call, and a call that returns nothing new costs the same $0.0008 as one that returns a change. That makes the polling interval the main dial. Charting one post's counters every minute is 1,440 calls a day, about $1.15; every fifteen minutes it is 96 calls, under eight cents. The free monitor removes the other large cost, detecting new posts, entirely. Refused requests are not billed, so a backoff after a 429 costs time rather than money. The fields and compact parameters let you shrink each response to the counters you actually chart, which does not lower the price but does cut what you store. On a per-resource meter the trade looks different, because each returned post is billed every time it is read, so re-reading the same post a thousand times is a thousand post reads. Choosing the interval from how fast the numbers really move, rather than from how fresh you would like them to look, is the habit that keeps a tracker cheap on either meter.
What you can build on one watch
A single monitor on one account supports more than an alert. Pointed at a brand account, it feeds a launch tracker: each new post arrives at your webhook, you record its id, then re-read tweet/detail at widening intervals to chart its first day of likes, reposts, replies and views. Pointed at a competitor with domain_filter set to their site, it delivers only the posts that link to their own pages, which makes a clean feed of their announcements. Pointed at an executive's account with include_replies off, it becomes a monitor for original statements only. Pointed at your own support account, it gives an audit trail of every public reply the team sent, with delivery lag measured per event. Each of those is the same free watch with different switches, and each adds billed reads only where you decide a delivered post deserves follow-up. Because a monitor never backfills, the history before its creation is a separate, one-time read job against the timeline routes. Build that once, then let the watch keep the dataset current from that point forward without another line on the bill.
Real-time streams versus polling
Real-time access to X data comes in two shapes, and only one of them is a true stream. The official platform sells a filtered stream, a persistent connection that delivers matching posts as they are published, and it belongs to X's own product line. What most trackers need is narrower: new posts from known accounts, and changing counters on known posts. Here the first is a push rather than a stream. A monitor polls the watched account on a shared interval and posts each new item to your webhook, signed, with the lag it measured recorded on every delivery. The second is a poll, because counters change continuously and no push fires when a like count moves. In practice that split covers most real-time tracking. The push side reacts within the poll interval and costs nothing; the poll side reacts as fast as you are willing to pay for. Where you genuinely need every matching post across all of X the moment it appears, the official filtered stream is the tool built for that job, and knowing so before a launch is cheaper than discovering it during one.
How current is the data you track?
Every number you track is a snapshot, and its age is part of the data. Counters on tweet/detail are documented as values at fetch time, so a read is exactly as current as the moment it ran, and two reads minutes apart can disagree without either being wrong. view_count appears only where the source exposes it, so treat its absence as missing rather than as zero. On the push side, currency is measured rather than promised. Each delivery in monitor/deliveries carries detected_lag_ms, the gap between the post's own timestamp and the moment it was queued, delivery_lag_ms from queue to your endpoint, and total_lag_ms, and the poll interval behind them is documented as not yet a published SLA. The limitation worth stating plainly is that nothing is backdated. A monitor delivers posts made after it was created, and a poll knows only what was true when it ran. Store the fetch time beside every row and the detected time beside every delivery, and your dashboard can say how old each number is instead of implying that every figure on screen is live.
Can I use this API in production?
Yes, and the tracking routes expose the failure cases a production system needs to see. Deliveries are HMAC-SHA256 signed with a secret shown once at webhook creation, so your receiver can reject anything it cannot verify. Failed deliveries are retried, eight attempts over 21 minutes, and a delivery that exhausts them is held rather than lost until you replay it with the free redrive route; deliveries that failed for a permanent reason, such as a 410 from your endpoint, are reported as skipped rather than resent. The two health routes give you degraded and events_possibly_missed flags to alert on, and a 503 on creation means monitoring capacity is full rather than that your request was wrong. On the read side, every key is paced at 600 requests a minute with 20 in flight, refused requests are not billed, and a 402 tells you the balance is empty before anything silently stops. Put account/me on a free timer and alert well before zero. The one discipline the API cannot supply is yours: dedupe on tweet_id, because a retried delivery can arrive twice, and give production its own key rather than sharing one with experiments.
Brand monitoring with a standing watch
Brand monitoring usually means two feeds: what your own accounts post, and what other people say about you. The first is a monitor per handle, which delivers every new post from that account to your webhook, free, with domain_filter available to keep only posts that link to your site. The second is a search: run tweet/advanced_search on your brand name and product names, with operators such as -from:yourhandle to exclude your own posts and lang:en to stay in one language, on a schedule, and keep the highest post id you have seen so each pass only processes new mentions. At $0.0008 a page of about 20 posts, a brand with a few hundred mentions a day costs well under a dollar a month to watch. Then re-read the posts that matter with tweet/detail to track how their engagement grows.
Quick integration in three requests
The first working watch is three requests. One: POST /twitter/webhook with your HTTPS url, and store the signing secret it returns, because it is shown only once. Two: POST /twitter/monitor with the handle to watch, and optionally webhook_ids, include_replies and domain_filter. Three: GET /twitter/monitor/health to confirm the watch is active, then POST /twitter/webhook/<id>/test to send a test delivery and check your endpoint verifies the HMAC signature. All of these are free account calls. From then on new posts arrive as signed POSTs to your url, with no polling code on your side. For a single post rather than an account, the integration is even shorter: one GET /twitter/tweet/detail on a timer, reading favorite_count, retweet_count and view_count each time.
Polling a post against running a monitor
| What you are asking | Poll tweet/detail | Run a monitor |
|---|---|---|
| What it answers | How this post's counters changed | That the account posted something new |
| Cost per check | $0.0008 | $0 |
| Reach into older posts | Any id you already hold | None, the baseline starts at creation |
| Who sets the timing | You, by choosing the interval | The poll interval, reported as poll_interval_ms |
| Filtering available | None, you fetch then discard | domain_filter and include_replies |
| How you confirm it works | The response body arrives | monitor/health and monitor/deliveries |
| What makes it stop | Your scheduler or a spent balance | A deleted webhook or a paused status |
Filtered Stream prioritizes data hydration and delivery, with approximately 6-7 seconds of P99 latency.
Questions and answers
- Can I be notified when a specific tweet gets replies?
- Not with a monitor. A monitor is keyed to a handle and delivers that account's new posts, never activity on an existing one. To track replies to a particular id you re-read tweet/replies on an interval and diff the result against what you already stored. Each of those pages is an ordinary billed read, so the cadence you pick is a spending decision as much as a freshness one.
- How do I know a watch is still delivering?
- Call monitor/health. It needs no path parameter, is scoped by your key and never 404s, so a key with nothing configured answers 200 with every counter at zero. Read the deliveries_24h object rather than the status string alone: failed rising while delivered stays flat is a receiver problem, and both sitting at zero for an account that posts is worth checking per watch.
- Does creating a monitor pull in the account's old posts?
- No, and that is deliberate. The opening poll records the newest existing post as a baseline and delivers nothing for it, so an account with a decade of history sends you nothing at all until it posts again. If you want that history it is a separate read job against the timeline endpoints, billed per page like any other read and completely independent of the watch.
- What does degraded actually mean?
- It says this watch is currently being polled more slowly than its published interval. The monitor keeps running and this is not an outage, but paired with events_possibly_missed it tells you a posting window may have slipped past unseen. Check last_poll_at on the same response to see how stale the cursor is, then check the account-wide status field to see whether the condition is yours alone.
- Can I pause a watch instead of deleting it?
- Yes. POST to monitor/{id} with status set to paused, and the same route resumes it with active. Resuming re-runs the capacity and per-account cap checks that creation runs, because it adds this watch back against monitoring capacity, so a resume can be refused where a create would be. Sending anything other than exactly active or paused in that field is a 400 rather than a quiet no-op.
- What does watching an account cost per month?
- Nothing at all. Every monitoring and webhook route is zero-rated, so a standing watch adds no line to your balance however many posts it forwards. The comparison worth making is against the loop it replaces: checking one account once a minute is 1,440 billed reads a day, and the watch delivering that same information costs nothing and reacts faster than the interval you would have picked.
- Can I turn off replies without recreating the monitor?
- Send include_replies set to false in a POST to monitor/{id}. Replies keep being polled and keep advancing the cursor, they simply stop being delivered to you. An update carrying only that field never re-runs the capacity or per-account cap checks, because narrowing delivery changes neither poll cadence nor the monitoring capacity you take, which makes it safe on a live watch.
- What is a 503 capacity_unavailable telling me?
- That monitoring is currently at capacity and cannot serve one more watch at the published interval. It describes that capacity at that moment rather than anything malformed in your request, so the documented response is to retry later rather than to change parameters. The same check runs when you resume a paused watch, which is why a resume can fail on a request that succeeded when you first created it.
- Can I move a monitor to a different account?
- No. A monitor's watched handle cannot be changed, so switching accounts means deleting it and creating a new one, which also resets the baseline to the new account's newest post. Everything else is editable in place: status, webhook_ids, domain_filter and include_replies can all change in one call, and sending domain_filter as null clears an existing filter entirely. Recreating also resets the cursor, so the replacement delivers nothing for posts made before it existed.
- What latency should I expect from a monitor?
- Measure it rather than assume it. The field reporting the poll interval is explicitly documented as not a published SLA, and detected_lag_ms is described as the honest figure for how fast detection currently is, bound by the shared interval. Pull a few hundred events from the deliveries route, look at detected_lag_ms and delivery_lag_ms separately, and set alerting from the distribution you actually observe.
Keep reading
Start with $0.50 in free credits
No credit card. Roughly 12,500 tweets to test every endpoint.