Monitor X (Twitter) trends by combining GET /2/tweets/search/recent for keyword volume tracking and GET /2/tweets/counts/recent for aggregate counts. Requires OAuth 2.0 Bearer token or user context. Cost: $0.005 per tweet read on pay-per-use; Basic tier ($200/month) gives 15,000 reads/month. The filtered stream (GET /2/tweets/search/stream) is more cost-efficient for continuous monitoring but requires Basic tier.
API Quick Reference
OAuth 2.0 / Bearer Token
180 requests/15 minutes (search/recent, Basic)
JSON
Available
Understanding the X (Twitter) API v2
X API v2 provides REST endpoints at https://api.twitter.com/2/ for searching tweets and monitoring keyword volumes. For trend monitoring, the two key endpoints are GET /2/tweets/search/recent (returns up to 100 tweets per request matching a query, covering the past 7 days on Basic tier) and GET /2/tweets/counts/recent (returns bucketed tweet counts for a query without returning tweet bodies — much cheaper for volume tracking).
Since February 2026, X uses pay-per-use pricing: each tweet read costs $0.005 for others' tweets, making high-frequency search.list polling expensive. At 1,000 polls per day reading 100 tweets each, you would spend $500/day. The tweet counts endpoint is cost-efficient because it returns aggregate counts without returning individual tweet objects, but rate limits still apply. The filtered stream (GET /2/tweets/search/stream) is the most efficient approach for real-time monitoring — it pushes tweets matching your rules as they are posted, eliminating polling entirely, but requires Basic tier.
The Basic tier ($200/month) includes 15,000 tweet reads per month and access to the filtered stream. The free tier has effectively zero read access for trend monitoring purposes. Official docs: https://developer.twitter.com/en/docs/twitter-api
https://api.twitter.com/2Setting Up X (Twitter) API Authentication
For trend monitoring with public tweets, App-Only Bearer Token authentication is the simplest approach — it requires no user interaction and no refresh logic. Get a Bearer token by base64-encoding your API Key and API Key Secret and POSTing to https://api.twitter.com/oauth2/token. This token never expires until you invalidate it.
- 1Go to https://developer.twitter.com/en/portal/dashboard and create a project and app
- 2In the app settings, find 'Keys and Tokens'
- 3Copy the 'API Key' (Consumer Key) and 'API Key Secret' (Consumer Secret)
- 4Base64-encode 'API_KEY:API_KEY_SECRET' and POST to https://api.twitter.com/oauth2/token
- 5Store the returned bearer_token securely — this never expires until you invalidate it
- 6For Basic tier, also sign up for a paid plan at https://developer.twitter.com/en/portal/products
- 7Add the Bearer token to your environment variables as TWITTER_BEARER_TOKEN
1import os2import base643import requests45API_KEY = os.environ['TWITTER_API_KEY']6API_KEY_SECRET = os.environ['TWITTER_API_KEY_SECRET']78def get_bearer_token():9 credentials = base64.b64encode(f'{API_KEY}:{API_KEY_SECRET}'.encode()).decode()10 resp = requests.post(11 'https://api.twitter.com/oauth2/token',12 headers={13 'Authorization': f'Basic {credentials}',14 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'15 },16 data='grant_type=client_credentials'17 )18 resp.raise_for_status()19 return resp.json()['access_token']2021# Or use the bearer_token directly from your developer portal22BEARER_TOKEN = os.environ.get('TWITTER_BEARER_TOKEN') or get_bearer_token()Security notes
- •Store API_KEY, API_KEY_SECRET, and BEARER_TOKEN in environment variables — never hardcode them
- •The Bearer token has no expiry, so treat it as a long-lived secret — rotate it if compromised
- •App-only Bearer token gives access to all public tweets — it does NOT have access to protected tweets or user-specific data
- •For personalized trends endpoint, you need OAuth 2.0 user context — Bearer token is not sufficient
- •Never commit credentials to version control — use a .env file locally and proper secrets management in production
Key endpoints
/2/tweets/search/recentReturns tweets from the last 7 days matching a query. Each tweet returned counts as one read ($0.005 on pay-per-use). Best for sampling what is being said about a topic, not for volume counts.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | required | Search query using X's search operators (AND, OR, -term, from:, lang:, etc.) |
tweet.fields | string | optional | Fields to include: public_metrics,created_at,entities,author_id |
max_results | number | optional | Results per page: 10-100, default 10 |
next_token | string | optional | Pagination cursor from meta.next_token |
start_time | string | optional | ISO 8601 start time, e.g. 2026-05-15T00:00:00Z (max 7 days ago on Basic) |
Response
1{"data": [{"id": "1234567890", "text": "Breaking: GPT-5 just dropped and it's incredible", "public_metrics": {"impression_count": 48291, "like_count": 2847, "retweet_count": 987}, "created_at": "2026-05-22T14:30:00.000Z"}], "meta": {"newest_id": "1234567890", "oldest_id": "1234567800", "result_count": 10, "next_token": "7140dibdnow9c7btw3w29n4v4idp"}}/2/tweets/counts/recentReturns aggregate tweet counts for a query bucketed by time (day, hour, or minute) without returning individual tweets. Cost-efficient for volume trend tracking — counts don't consume your tweet read quota.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | required | Search query using X's operators |
granularity | string | optional | Bucket size: minute, hour (default), or day |
start_time | string | optional | ISO 8601 start of the count window |
end_time | string | optional | ISO 8601 end of the count window |
Response
1{"data": [{"start": "2026-05-22T13:00:00.000Z", "end": "2026-05-22T14:00:00.000Z", "tweet_count": 1247}, {"start": "2026-05-22T14:00:00.000Z", "end": "2026-05-22T15:00:00.000Z", "tweet_count": 3891}], "meta": {"total_tweet_count": 28493}}/2/tweets/search/streamPersistent filtered stream that pushes tweets matching your rules in real-time. Requires Basic+ tier. More cost-efficient than polling for continuous monitoring — tweets are pushed rather than pulled.
| Parameter | Type | Required | Description |
|---|---|---|---|
tweet.fields | string | optional | Fields to include in streamed tweets |
expansions | string | optional | Expand referenced objects: author_id, attachments.media_keys |
Response
1{"data": {"id": "1234567891", "text": "New AI breakthrough announced today"}, "matching_rules": [{"id": "123456", "tag": "AI-monitoring"}]}Step-by-step automation
Get an App-Only Bearer Token
Why: Public tweet search for trend monitoring only requires App-Only authentication — no user authorization flow needed.
POST to https://api.twitter.com/oauth2/token with your API Key and Secret base64-encoded in the Authorization header and grant_type=client_credentials in the body. Store the returned access_token as your BEARER_TOKEN. Alternatively, copy the Bearer Token directly from your developer portal under 'Keys and Tokens'.
1# Encode API_KEY:API_KEY_SECRET in base64 first2curl -X POST 'https://api.twitter.com/oauth2/token' \3 -H 'Authorization: Basic BASE64_ENCODED_CREDENTIALS' \4 -H 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8' \5 -d 'grant_type=client_credentials'Pro tip: The Bearer Token in your developer portal is the same thing — copy it directly from there to skip this step entirely
Expected result: JSON response with token_type: 'bearer' and access_token — a long string starting with 'AAAA' for App-only tokens
Get Tweet Volume Counts for a Keyword
Why: The counts/recent endpoint returns aggregate counts by time bucket — no individual tweet bodies, so it does not consume your read quota while still showing volume trends.
GET /2/tweets/counts/recent with granularity=hour gives you hourly tweet counts for any keyword over the past 7 days. This is the most cost-efficient way to track volume trends. Compare the latest hour's count to the previous 24-hour average to detect spikes.
1curl -G 'https://api.twitter.com/2/tweets/counts/recent' \2 -H 'Authorization: Bearer YOUR_BEARER_TOKEN' \3 --data-urlencode 'query=GPT lang:en -is:retweet' \4 -d 'granularity=hour'Pro tip: Add -is:retweet to your query to track original tweets only — retweets inflate volume counts without representing unique conversations
Expected result: Array of hourly buckets with tweet_count, plus meta.total_tweet_count. No individual tweet reads consumed.
Fetch Top Tweets for Spike Context
Why: Volume counts tell you a spike is happening; fetching the top tweets tells you why — what specific content or narrative is driving the trend.
When your spike detector triggers, call GET /2/tweets/search/recent with the same query and sort_order=recency or sort_order=relevancy. Fetch only when needed (not on every poll) to minimize read costs. Each tweet read costs $0.005 on pay-per-use, so limit to 10-20 tweets for context.
1curl -G 'https://api.twitter.com/2/tweets/search/recent' \2 -H 'Authorization: Bearer YOUR_BEARER_TOKEN' \3 --data-urlencode 'query=GPT lang:en -is:retweet' \4 -d 'tweet.fields=public_metrics,created_at,author_id' \5 -d 'sort_order=relevancy' \6 -d 'max_results=10'Pro tip: Use sort_order=relevancy rather than recency when fetching context for alerts — relevancy surfaces the most engaging tweets, not just the newest ones
Expected result: Array of up to 10 tweet objects sorted by relevance, each with text, public_metrics, and created_at — used as context for the alert message
Detect Spikes and Send Alerts
Why: Volume counts only become actionable when they cross a threshold relative to the baseline — automated comparison and alerting is what makes this a real monitoring system.
Compare the latest hourly tweet count to the 24-hour rolling average for your tracked keywords. When the ratio exceeds your threshold (e.g. 3x), send an alert to Slack or email with the current count, baseline, spike ratio, and 2-3 example tweets for context. Use a seen-spike set to avoid re-alerting on the same trend for multiple consecutive hours.
1# Trigger Slack alert on spike detection2curl -X POST YOUR_SLACK_WEBHOOK_URL \3 -H 'Content-Type: application/json' \4 -d '{5 "text": "*X Trend Alert* \u2191 \"GPT\" is spiking — 4,892 tweets/hour (baseline: 1,247) — 3.9x spike",6 "attachments": [7 {8 "title": "Top tweet: GPT-5 just dropped and benchmarks are insane",9 "text": "Impressions: 48,291 | Likes: 2,847"10 }11 ]12 }'Pro tip: Add a 1-hour cooldown per keyword to avoid alert fatigue when a trend stays elevated — you want a single alert, not one every hour
Expected result: Slack alert sent when spike ratio exceeds threshold, with hourly count, baseline comparison, spike ratio, and top tweet for context
Complete working code
This script monitors configured keywords every 30 minutes by fetching tweet counts (zero read cost), detects volume spikes by comparing to a 24-hour rolling average, and sends Slack alerts with context tweets only when a spike is confirmed. It tracks alert cooldowns per keyword to prevent spam.
1#!/usr/bin/env python32import os3import time4import logging5import requests6from collections import deque78logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')910BEARER = os.environ['TWITTER_BEARER_TOKEN']11SLACK_WEBHOOK = os.environ['SLACK_WEBHOOK_URL']1213KEYWORDS = ['GPT', 'Claude AI', 'OpenAI', 'LLM']14SPIKE_THRESHOLD = 3.015POLL_INTERVAL = 1800 # 30 minutes16ALERT_COOLDOWN = 3600 # 1 hour per keyword1718headers = {'Authorization': f'Bearer {BEARER}'}19hourly_history = {kw: deque(maxlen=24) for kw in KEYWORDS}20alert_times = {kw: 0 for kw in KEYWORDS}2122def get_counts(query):23 resp = requests.get(24 'https://api.twitter.com/2/tweets/counts/recent',25 headers=headers,26 params={'query': query, 'granularity': 'hour'},27 timeout=1528 )29 if resp.status_code == 429:30 reset = int(resp.headers.get('x-rate-limit-reset', time.time() + 900))31 wait = max(reset - time.time(), 0) + 532 logging.warning(f'Rate limited on counts. Waiting {wait:.0f}s')33 time.sleep(wait)34 return get_counts(query)35 resp.raise_for_status()36 return resp.json()['data']3738def get_context(query, n=3):39 resp = requests.get(40 'https://api.twitter.com/2/tweets/search/recent',41 headers=headers,42 params={43 'query': query,44 'tweet.fields': 'public_metrics,created_at',45 'sort_order': 'relevancy',46 'max_results': n47 },48 timeout=1549 )50 resp.raise_for_status()51 tweets = resp.json().get('data', [])52 logging.info(f'Context tweets fetched: {len(tweets)} (~${len(tweets)*0.005:.3f} cost)')53 return tweets5455def send_alert(keyword, last_hour, baseline, ratio, context):56 top = context[0]['text'][:140] if context else 'No context'57 payload = {58 'text': f'*X Trend Alert* \u2191 "{keyword}" spiking: {last_hour:,}/hr (avg {baseline:.0f}) = {ratio:.1f}x',59 'attachments': [{60 'color': '#ff4444',61 'fields': [62 {'title': 'Current (last hour)', 'value': f'{last_hour:,} tweets', 'short': True},63 {'title': '24h Baseline', 'value': f'{baseline:.0f} tweets/hr', 'short': True},64 ],65 'text': f'Top tweet: {top}'66 }]67 }68 requests.post(SLACK_WEBHOOK, json=payload, timeout=5)69 logging.info(f'Alert sent: "{keyword}" at {ratio:.1f}x spike')7071def run_check():72 for kw in KEYWORDS:73 query = f'{kw} lang:en -is:retweet'74 try:75 counts = get_counts(query)76 if len(counts) < 25:77 continue78 last_hour = counts[-1]['tweet_count']79 baseline = sum(c['tweet_count'] for c in counts[-25:-1]) / 2480 ratio = last_hour / baseline if baseline > 0 else 081 hourly_history[kw].append(last_hour)82 logging.info(f'"{kw}": {last_hour}/hr, baseline {baseline:.0f}, ratio {ratio:.1f}x')83 if ratio >= SPIKE_THRESHOLD and (time.time() - alert_times[kw]) >= ALERT_COOLDOWN:84 context = get_context(query, n=3)85 send_alert(kw, last_hour, baseline, ratio, context)86 alert_times[kw] = time.time()87 time.sleep(2) # Small delay between keywords88 except Exception as e:89 logging.error(f'Error checking "{kw}": {e}')9091if __name__ == '__main__':92 logging.info(f'Starting trend monitor for: {KEYWORDS}')93 while True:94 run_check()95 logging.info(f'Sleeping {POLL_INTERVAL}s...')96 time.sleep(POLL_INTERVAL)Error handling
{"title": "Too Many Requests", "status": 429, "detail": "Too Many Requests"}Exceeded the rate limit for the endpoint: 180 requests per 15 minutes for search/recent (Basic), 300 requests per 15 minutes for counts/recent.
Read the x-rate-limit-reset header (Unix timestamp) and sleep until that time. Never implement a fixed sleep — use the header value.
Sleep until x-rate-limit-reset + 5 seconds, then retry once
{"title": "Unauthorized", "status": 401}Invalid or missing Bearer token. App-only Bearer tokens don't expire, so this usually means the token is malformed or has been invalidated.
Verify the BEARER_TOKEN environment variable is set correctly. If it was invalidated, generate a new one from the developer portal or via the oauth2/token endpoint.
Do not retry — fix the authentication issue first
{"errors": [{"message": "Sorry, you are not authorized to access 'tweet.fields'"}]}Requesting fields or features not available on your current tier. Free tier has extremely limited read access.
Check your API tier at developer.twitter.com. Upgrade to Basic ($200/month) for search access with public_metrics. Remove fields that require higher-tier access.
Remove unsupported fields and retry immediately
{"title": "Forbidden", "detail": "You are not allowed to use this endpoint with your current access level"}Attempting to use the filtered stream without Basic tier, or accessing Pro-only endpoints on Basic.
Check which endpoints are available at your tier: https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api. Upgrade your plan or use alternative endpoints.
Do not retry — upgrade plan or switch to an available endpoint
Rate Limits for X (Twitter) API
| Scope | Limit | Window |
|---|---|---|
| GET /2/tweets/search/recent (Basic) | 180 requests | per 15 minutes |
| GET /2/tweets/counts/recent (Basic) | 300 requests | per 15 minutes |
| Monthly read cap (Basic tier) | 15,000 tweet reads | per month |
1import time2import requests34def twitter_request(url, bearer_token, params=None, max_retries=3):5 headers = {'Authorization': f'Bearer {bearer_token}'}6 for attempt in range(max_retries):7 resp = requests.get(url, headers=headers, params=params, timeout=15)8 if resp.status_code == 429:9 reset_ts = int(resp.headers.get('x-rate-limit-reset', time.time() + 900))10 wait = max(reset_ts - time.time(), 0) + 511 print(f'Rate limited. Sleeping {wait:.0f}s (attempt {attempt + 1}/{max_retries})')12 time.sleep(wait)13 continue14 resp.raise_for_status()15 return resp.json()16 raise Exception('Max retries exceeded')- Use /2/tweets/counts/recent instead of /2/tweets/search/recent for volume tracking — counts don't consume your read quota
- Only fetch actual tweet objects when you need context for an alert — not on every poll
- Poll every 30 minutes rather than every minute — hourly tweet counts don't change meaningfully in shorter intervals
- Implement a 1-hour per-keyword alert cooldown to prevent Slack spam when a trend stays elevated
- Monitor your monthly read cap: $0.005/tweet read on pay-per-use means 3,000 tweets per day costs $15/day = $450/month
Security checklist
- Store TWITTER_BEARER_TOKEN in environment variables — never hardcode in source code
- App-only Bearer tokens have no expiry but can be invalidated — store them in a secrets manager and rotate if compromised
- Never commit .env files or credential files to version control — use .gitignore
- For sensitive monitoring use cases, use user-context OAuth 2.0 rather than App-only auth for better access control
- Log all API calls with rate limit remaining values to track usage and detect unexpected cost spikes
- Implement request timeouts (15 seconds) to prevent hanging connections from blocking your monitoring loop
Automation use cases
Brand Mention Spike Alerting
intermediateMonitor your brand name and product terms for sudden volume spikes that may indicate viral mentions, PR crises, or competitor activity.
Industry News Early Warning
beginnerTrack industry keywords and get alerted when a news event causes a volume spike, letting you respond with relevant content before competitors.
Competitor Launch Detection
intermediateMonitor competitor product names and detect announcement spikes to inform your own marketing and product team in real time.
Hashtag Campaign Performance Tracker
intermediateTrack hourly tweet volumes for campaign hashtags and measure engagement velocity to understand campaign reach over time.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier available; paid from $19.99/monthZapier's X integration can trigger on keyword mentions and route them to Slack, Sheets, or email — though volume spike detection requires custom logic.
- + No code required
- + Easy Slack/email integration
- + Reliable managed infrastructure
- - Cannot do volume counting or spike detection natively
- - Limited to individual tweet triggers, not aggregate counts
- - Costly for high-volume keyword monitoring
Make
Free tier available; paid from $9/monthMake's X module supports searching recent tweets on a schedule; combined with data aggregation modules, you can build basic volume tracking.
- + More flexible aggregation than Zapier
- + Lower cost per operation
- + Can route to multiple destinations
- - No native tweet counts endpoint support
- - Spike detection requires complex scenario logic
- - Requires paid plan for scheduled automation
n8n
Self-hosted free; cloud from €20/monthn8n's HTTP Request node can call the counts/recent endpoint; combined with Code nodes, you can implement full spike detection with rolling averages.
- + Self-hostable — no per-operation pricing
- + Full flexibility with Code nodes for spike logic
- + Can store historical counts in a database
- - Requires self-hosting for full control
- - No native X API node for counts endpoint
- - Setup requires more technical knowledge
Best practices
- Use /2/tweets/counts/recent as your primary polling endpoint — it returns volume data without consuming tweet read quota
- Only call /2/tweets/search/recent to fetch actual tweets when you need context for an alert — this is the expensive endpoint
- Build your baseline from at least 24 hourly data points before firing spike alerts — with less data, you will get many false positives
- Add -is:retweet to all monitoring queries — retweets inflate volume counts and represent amplification, not unique conversations
- Implement per-keyword alert cooldowns (1 hour minimum) to prevent alert fatigue when a trend remains elevated
- Track your monthly read costs explicitly — at $0.005/tweet, a monitoring run that reads 500 context tweets daily costs $75/month
- Use the lang:en filter for English-language monitoring — without it, multilingual spikes can mask or inflate your baseline
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building an X (Twitter) trend monitoring system using the API v2 in Python. I use GET /2/tweets/counts/recent to get hourly tweet volumes (no read cost) and GET /2/tweets/search/recent only when I detect a spike (costs $0.005/tweet). My spike detector compares the latest hour to the 24-hour rolling average. The problem is false positives — the baseline gets contaminated when a previous spike is included in the 24-hour window. Can you help me implement a more robust baseline calculation that excludes previous spike hours from the baseline average?
Build an X (Twitter) trend monitoring dashboard UI. Show a list of tracked keywords with their current tweet volume per hour, 24-hour baseline, and spike ratio. Add a line chart showing hourly tweet volume for each keyword over the past 7 days (with spike events highlighted in red). Include a table of recent alerts with keyword, spike ratio, timestamp, and example tweet. Add a configuration panel to add/remove keywords and set the spike threshold multiplier. The data comes from a Python script polling the X API — design with mock data that shows one current spike.
Frequently asked questions
Is the X (Twitter) API free for trend monitoring?
The Free tier has effectively zero tweet read access, making it unusable for trend monitoring. Basic tier at $200/month includes 15,000 tweet reads and access to search/recent. If you use the counts/recent endpoint (aggregate volumes with no tweet bodies), it does not count against your read quota — making it the cost-efficient choice for volume tracking.
What is the difference between counts/recent and search/recent for trend monitoring?
GET /2/tweets/counts/recent returns only aggregate tweet counts bucketed by hour — no individual tweets, no read cost. GET /2/tweets/search/recent returns actual tweet objects, consuming $0.005 each on pay-per-use. For volume trend detection, use counts/recent as your primary endpoint and only call search/recent when you need context tweets for an alert.
What happens when I hit the rate limit?
HTTP 429 is returned with x-rate-limit-reset (Unix timestamp) and x-rate-limit-remaining: 0 headers. Sleep until the reset timestamp plus 5 seconds buffer, then retry. The counts/recent endpoint has 300 requests per 15 minutes (Basic), so polling every 30 minutes for 10 keywords uses about 20 requests per 15 minutes — well under the limit.
Can I monitor trends in real-time without polling?
Yes — the filtered stream (GET /2/tweets/search/stream) pushes matching tweets as they are posted, eliminating the need to poll. It requires Basic tier and uses a separate streaming connection. You set filter rules (keywords, phrases, hashtags) and the stream sends matching tweets continuously. This is more efficient than polling for high-volume keywords.
How do I write effective search queries for monitoring?
Use X's search operators: -is:retweet to exclude retweets (reduce noise), lang:en for English only, -is:reply to exclude replies, (keyword1 OR keyword2) for multiple terms. Example: '("GPT-5" OR "GPT 5") lang:en -is:retweet -is:reply'. Quotes enforce exact phrase matching. Test your query in the Twitter search UI first before building the automation.
How do I avoid false positive spike alerts?
Build your baseline from at least 24 consecutive hourly data points before firing any alerts. Use a minimum baseline threshold (e.g. at least 100 tweets/hour) to avoid 10x spikes on tiny baselines (e.g. 1 tweet to 10 tweets). Implement a 1-hour per-keyword cooldown to avoid alerting multiple consecutive hours during a sustained trend. Exclude previous spike hours from the baseline calculation.
Can RapidDev build a custom X trend monitoring platform for my business?
Yes. RapidDev has built 600+ integrations including real-time social monitoring systems. We can build production-grade X trend monitoring with multi-platform comparison, sentiment analysis, custom alert routing, and historical dashboards. Book a free consultation at rapidevelopers.com.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation