Automate X (Twitter) analytics reports using GET /2/users/{id}/tweets with tweet.fields=public_metrics on the Twitter API v2. Requires OAuth 2.0 with offline.access scope for token refresh. Cost warning: reading your own tweets costs $0.001 per read on pay-per-use; Basic tier ($200/month) includes 15,000 reads/month. Note: follower/following counts were removed from self-serve tiers in April 2026.
API Quick Reference
OAuth 2.0
15,000 reads/month (Basic) or $0.001/read pay-per-use
JSON
Available
Understanding the X (Twitter) API v2
X API v2 is a REST JSON API available at https://api.twitter.com/2/. Since February 2026, X has moved to pay-per-use pricing in addition to monthly tiers: reading your own tweets costs $0.001 per read, reading others' tweets costs $0.005 per read, and posting costs $0.015–$0.20 depending on whether the tweet contains a link. The Basic tier ($200/month, doubled from $100 in January 2025) includes 15,000 tweet reads per month.
For analytics automation, the primary endpoint is GET /2/users/{id}/tweets which returns a user's recent tweets with optional metrics fields. Public metrics (impressions, likes, retweets, replies, quotes) are available with any authentication. Non-public and organic metrics require user-context OAuth 2.0 with the tweet author's access token. The API returns up to 100 tweets per request with cursor-based pagination.
Important breaking changes: follower counts and following counts were removed from all self-serve tiers in April 2026. Engagement rate calculations based on audience size are no longer possible without the Pro tier ($5,000/month). Official docs: https://developer.twitter.com/en/docs/twitter-api
https://api.twitter.com/2Setting Up X (Twitter) API Authentication
X API v2 uses OAuth 2.0 with PKCE for user-context access. You redirect the user to Twitter's authorization page, they approve your app, and you exchange the code for an access token and refresh token. The refresh token is only issued if you include offline.access in the scope. Access tokens expire after 2 hours — always implement refresh logic.
- 1Go to https://developer.twitter.com/en/portal/dashboard and create a new project and app
- 2In the app settings, enable OAuth 2.0 under 'User authentication settings'
- 3Set the app permissions to 'Read' (tweet.read, users.read)
- 4Add a callback URL (e.g. http://localhost:3000/callback or your production URL)
- 5Copy the Client ID (OAuth 2.0 Client ID, not the API Key) from the app dashboard
- 6If your app is confidential (server-side), also copy the Client Secret
- 7Build the authorization URL: https://twitter.com/i/oauth2/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=tweet.read%20users.read%20offline.access&state=STATE&code_challenge=CODE_CHALLENGE&code_challenge_method=S256
- 8After user authorizes, exchange the code at https://api.twitter.com/2/oauth2/token for access_token and refresh_token
1import os2import secrets3import hashlib4import base645import requests67CLIENT_ID = os.environ['TWITTER_CLIENT_ID']8CLIENT_SECRET = os.environ['TWITTER_CLIENT_SECRET'] # For confidential apps9REDIRECT_URI = os.environ['TWITTER_REDIRECT_URI']1011def generate_pkce():12 code_verifier = secrets.token_urlsafe(50)13 code_challenge = base64.urlsafe_b64encode(14 hashlib.sha256(code_verifier.encode()).digest()15 ).decode().rstrip('=')16 return code_verifier, code_challenge1718def refresh_access_token(refresh_token):19 resp = requests.post(20 'https://api.twitter.com/2/oauth2/token',21 auth=(CLIENT_ID, CLIENT_SECRET),22 data={23 'grant_type': 'refresh_token',24 'refresh_token': refresh_token,25 'client_id': CLIENT_ID,26 }27 )28 resp.raise_for_status()29 return resp.json() # Contains new access_token and refresh_tokenSecurity notes
- •Store CLIENT_ID, CLIENT_SECRET, and tokens in environment variables — never hardcode them
- •The offline.access scope is required to get a refresh token — without it, users must re-authorize every 2 hours
- •Always use PKCE (code_challenge/code_verifier) — never use the implicit flow
- •When you get a new refresh_token after refreshing, store it immediately — the old one is invalidated
- •Never expose your Client Secret in frontend code — OAuth 2.0 token exchange must happen server-side
Key endpoints
/2/users/{id}/tweetsReturns the most recent tweets for a user. Add tweet.fields=public_metrics,non_public_metrics,organic_metrics to get engagement data. This is the primary endpoint for analytics reports.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | The user ID (not username) of the account to fetch tweets for |
tweet.fields | string | optional | Comma-separated fields: public_metrics,non_public_metrics,organic_metrics,created_at,entities |
max_results | number | optional | Tweets per page: 5-100, default 10 |
pagination_token | string | optional | Cursor token from meta.next_token for pagination |
start_time | string | optional | ISO 8601 start date filter, e.g. 2026-05-13T00:00:00Z |
Response
1{"data": [{"id": "1234567890", "text": "Just shipped a new feature!", "public_metrics": {"impression_count": 12847, "like_count": 342, "retweet_count": 89, "reply_count": 23, "quote_count": 14}, "organic_metrics": {"impression_count": 12847, "like_count": 342, "retweet_count": 89, "reply_count": 23}, "created_at": "2026-05-20T14:30:00.000Z"}], "meta": {"next_token": "7140dibdnow9c7btw3w29n4v4idp", "result_count": 10, "newest_id": "1234567890", "oldest_id": "1234567800"}}/2/tweets/{id}Returns a single tweet with optional metrics fields. Use for enriching individual tweet data or verifying specific post performance.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Tweet ID to look up |
tweet.fields | string | optional | Fields to include: public_metrics, created_at, entities, context_annotations |
Response
1{"data": {"id": "1234567890", "text": "Product launch tweet", "public_metrics": {"impression_count": 45231, "like_count": 1247, "retweet_count": 389, "reply_count": 124, "quote_count": 67}, "created_at": "2026-05-15T09:00:00.000Z"}}/2/users/meReturns the authenticated user's profile data. Use to get the user ID needed for the tweets endpoint, and to fetch basic profile metrics.
| Parameter | Type | Required | Description |
|---|---|---|---|
user.fields | string | optional | Fields to include: public_metrics, created_at, description, verified |
Response
1{"data": {"id": "987654321", "name": "Your Company", "username": "yourcompany", "public_metrics": {"tweet_count": 1247, "listed_count": 89}}}Step-by-step automation
Get the Authenticated User's ID
Why: The tweets endpoint requires a user ID, not a username — GET /2/users/me gives you the correct ID for the authenticated account.
Call GET /2/users/me with user.fields=public_metrics to get the user ID and current profile stats. Store the ID for subsequent calls — it never changes. Note that follower_count was removed from the public_metrics response for self-serve tiers in April 2026.
1curl 'https://api.twitter.com/2/users/me?user.fields=public_metrics,created_at' \2 -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'Pro tip: Cache the user ID — it never changes and calling /2/users/me on every run wastes your monthly read quota
Expected result: User object with id, username, and public_metrics (tweet_count, listed_count). Follower/following counts NOT available on Basic tier as of April 2026.
Fetch Recent Tweets with Metrics
Why: The /users/{id}/tweets endpoint with tweet.fields=public_metrics,organic_metrics gives you impression counts and engagement data for the past 7 days.
GET /2/users/{id}/tweets with max_results=100 and tweet.fields=public_metrics,organic_metrics,created_at. Public metrics (impressions, likes, retweets, replies) are available to all. Organic metrics (same data broken out for organic vs promoted) require user-context auth. Note: this costs $0.001 per tweet read on pay-per-use, or counts toward the 15,000/month Basic tier cap.
1curl 'https://api.twitter.com/2/users/YOUR_USER_ID/tweets?max_results=100&tweet.fields=public_metrics,organic_metrics,created_at' \2 -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'Pro tip: Use start_time parameter to limit to the last 7 days: start_time=2026-05-13T00:00:00Z — this reduces read costs and speeds up the request
Expected result: Array of tweet objects each with id, text, created_at, and public_metrics containing impression_count, like_count, retweet_count, reply_count, quote_count
Calculate Engagement Metrics
Why: Raw counts are not enough — you need engagement rate, best performing content, and week-over-week trends to make the report actionable.
Calculate engagement rate as (likes + retweets + replies + quotes) / impressions * 100 for each tweet. Sort by total engagement to identify top content. Group tweets by day or week to calculate trend direction. Without follower counts (removed April 2026), use impression-based engagement rates rather than follower-based ones.
1# No direct API call for this step — calculation is done in code2# Fetch the tweets first, then calculate engagement metrics locallyPro tip: Impression-based engagement rate is more meaningful than follower-based since April 2026 — an account with 10k followers but bad reach will show honest engagement rates this way
Expected result: Array of tweet objects enriched with total_engagement and engagement_rate, sorted by performance for inclusion in the report
Generate and Deliver the Weekly Report
Why: Turning raw numbers into a formatted summary delivered to email or Slack is what makes this automation genuinely useful rather than just a data pull.
Aggregate all tweet metrics for the week: total impressions, total engagements, average engagement rate, top tweet, and period-over-period comparison if you store historical data. Format as a Slack Block Kit message or plain text email. For Slack delivery, use the Incoming Webhooks URL from your Slack app settings.
1# Send weekly report to Slack2curl -X POST YOUR_SLACK_WEBHOOK_URL \3 -H 'Content-Type: application/json' \4 -d '{5 "text": "*X (Twitter) Weekly Report — Week of May 19-26, 2026*",6 "attachments": [7 {8 "fields": [9 {"title": "Total Impressions", "value": "142,831", "short": true},10 {"title": "Total Engagement", "value": "3,847", "short": true},11 {"title": "Avg Engagement Rate", "value": "2.69%", "short": true},12 {"title": "Top Tweet", "value": "Just shipped a new feature! (1,247 likes)", "short": false}13 ]14 }15 ]16 }'Pro tip: Store weekly summary data to a database or spreadsheet — the X API only returns the last 7 days for Basic tier, so you need to build your own historical record
Expected result: Formatted Slack message delivered with total impressions, engagement totals, average engagement rate, tweet count, and top performing tweet for the week
Complete working code
This script fetches the last 7 days of tweets for the authenticated user, calculates engagement metrics, identifies top performing content, and delivers a weekly report to Slack. It handles OAuth2 token refresh automatically and tracks read costs.
1#!/usr/bin/env python32import os3import logging4import requests5from datetime import datetime, timedelta, timezone67logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')89ACCESS_TOKEN = os.environ['TWITTER_ACCESS_TOKEN']10REFRESH_TOKEN = os.environ['TWITTER_REFRESH_TOKEN']11CLIENT_ID = os.environ['TWITTER_CLIENT_ID']12CLIENT_SECRET = os.environ['TWITTER_CLIENT_SECRET']13SLACK_WEBHOOK = os.environ['SLACK_WEBHOOK_URL']1415def refresh_token():16 global ACCESS_TOKEN, REFRESH_TOKEN17 resp = requests.post(18 'https://api.twitter.com/2/oauth2/token',19 auth=(CLIENT_ID, CLIENT_SECRET),20 data={'grant_type': 'refresh_token', 'refresh_token': REFRESH_TOKEN, 'client_id': CLIENT_ID}21 )22 resp.raise_for_status()23 data = resp.json()24 ACCESS_TOKEN = data['access_token']25 if 'refresh_token' in data:26 REFRESH_TOKEN = data['refresh_token'] # Store new refresh token!27 logging.info('Token refreshed successfully')2829def twitter_get(path, params=None):30 resp = requests.get(31 f'https://api.twitter.com/2{path}',32 params=params,33 headers={'Authorization': f'Bearer {ACCESS_TOKEN}'}34 )35 if resp.status_code == 401:36 refresh_token()37 resp = requests.get(38 f'https://api.twitter.com/2{path}',39 params=params,40 headers={'Authorization': f'Bearer {ACCESS_TOKEN}'}41 )42 resp.raise_for_status()43 return resp.json()4445def get_user_id():46 data = twitter_get('/users/me', {'user.fields': 'public_metrics'})47 return data['data']['id']4849def get_recent_tweets(user_id):50 start_time = (datetime.now(timezone.utc) - timedelta(days=7)).strftime('%Y-%m-%dT%H:%M:%SZ')51 tweets = []52 pagination_token = None53 while True:54 params = {55 'max_results': 100,56 'tweet.fields': 'public_metrics,organic_metrics,created_at',57 'start_time': start_time58 }59 if pagination_token:60 params['pagination_token'] = pagination_token61 data = twitter_get(f'/users/{user_id}/tweets', params)62 tweets.extend(data.get('data', []))63 pagination_token = data.get('meta', {}).get('next_token')64 if not pagination_token:65 break66 logging.info(f'Fetched {len(tweets)} tweets (cost: ~${len(tweets) * 0.001:.4f} on pay-per-use)')67 return tweets6869def analyze_tweets(tweets):70 results = []71 for t in tweets:72 m = t.get('public_metrics', {})73 impressions = m.get('impression_count', 0)74 total_eng = sum(m.get(k, 0) for k in ['like_count', 'retweet_count', 'reply_count', 'quote_count'])75 results.append({76 'id': t['id'], 'text': t['text'][:100],77 'impressions': impressions, 'total_engagement': total_eng,78 'engagement_rate': round(total_eng / impressions * 100, 2) if impressions > 0 else 0,79 'created_at': t.get('created_at', '')80 })81 return sorted(results, key=lambda x: x['total_engagement'], reverse=True)8283def send_slack_report(analyzed):84 total_imp = sum(t['impressions'] for t in analyzed)85 total_eng = sum(t['total_engagement'] for t in analyzed)86 avg_rate = total_eng / total_imp * 100 if total_imp > 0 else 087 top = analyzed[0] if analyzed else {}88 payload = {89 'text': '*X (Twitter) Weekly Analytics Report*',90 'attachments': [{91 'color': '#1DA1F2',92 'fields': [93 {'title': 'Impressions (7d)', 'value': f"{total_imp:,}", 'short': True},94 {'title': 'Engagements (7d)', 'value': f"{total_eng:,}", 'short': True},95 {'title': 'Avg Eng Rate', 'value': f"{avg_rate:.2f}%", 'short': True},96 {'title': 'Tweets Published', 'value': str(len(analyzed)), 'short': True},97 {'title': 'Top Tweet', 'value': top.get('text', 'N/A'), 'short': False}98 ]99 }]100 }101 requests.post(SLACK_WEBHOOK, json=payload)102 logging.info('Slack report sent')103104if __name__ == '__main__':105 user_id = get_user_id()106 tweets = get_recent_tweets(user_id)107 if tweets:108 analyzed = analyze_tweets(tweets)109 send_slack_report(analyzed)110 else:111 logging.info('No tweets in the last 7 days')Error handling
{"title": "Unauthorized", "status": 401, "detail": "Unauthorized"}Access token has expired (expires after 2 hours) or is invalid. Missing Authorization header also triggers this.
Use the refresh_token to get a new access_token via POST /2/oauth2/token with grant_type=refresh_token. The new refresh_token in the response must be stored immediately — the old one is invalidated.
Refresh token immediately, then retry the original request once
{"title": "Forbidden", "detail": "non_public_metrics and organic_metrics are not available for tweets that are not from the authenticated user"}Attempting to read non_public_metrics or organic_metrics on tweets that don't belong to the authenticated user.
Only request non_public_metrics and organic_metrics for your own tweets. Use public_metrics only for tweets by other users.
Remove non_public_metrics from the request and retry with public_metrics only
{"title": "Too Many Requests", "status": 429, "detail": "Too Many Requests"}You have exceeded the monthly read cap (15,000 on Basic tier) or hit a short-term rate limit (typically 15 requests per 15 minutes on the tweets endpoint).
Check the x-rate-limit-remaining and x-rate-limit-reset headers to know when to retry. For monthly cap exhaustion, you will need to wait until the monthly reset or upgrade your tier.
Read x-rate-limit-reset header (Unix timestamp) and sleep until then; for monthly cap, stop automation and send an alert
{"errors": [{"message": "Invalid value for \"tweet.fields\" field"}]}Requesting tweet fields not available on your tier, or using an invalid field name.
Check that all fields in tweet.fields are valid for your access tier. Remove non_public_metrics if you don't have user-context OAuth2 auth with the tweet author.
Fix the request parameters and retry immediately
Rate Limits for X (Twitter) API
| Scope | Limit | Window |
|---|---|---|
| Tweets read (Basic tier) | 15,000 tweet reads | per month |
| GET /users/{id}/tweets | 1,500 requests | per 15 minutes (user auth) |
| Pay-per-use (own tweets) | $0.001 | per tweet read |
1import time2import requests34def twitter_get_with_retry(url, headers, params=None, max_retries=3):5 for attempt in range(max_retries):6 resp = requests.get(url, headers=headers, params=params)7 if resp.status_code == 429:8 reset_time = int(resp.headers.get('x-rate-limit-reset', time.time() + 900))9 wait = max(reset_time - time.time(), 0) + 510 print(f'Rate limited. Waiting {wait:.0f}s...')11 time.sleep(wait)12 continue13 resp.raise_for_status()14 return resp.json()15 raise Exception('Max retries exceeded')- Cache the user ID and store it persistently — calling /users/me on every run wastes read quota
- Use the start_time parameter to limit to the last 7 days — fetching all-time tweets is expensive and unnecessary for weekly reports
- Request max_results=100 per page to minimize the number of API calls for pagination
- Implement token refresh proactively (check expiry time) rather than reactively (on 401 error) to avoid failed report runs
- Monitor your monthly read consumption using the x-rate-limit-remaining header and alert when approaching the 15,000 cap
Security checklist
- Store access_token and refresh_token in environment variables or a secrets manager — never hardcode them
- When you receive a new refresh_token after a token refresh, store it immediately — the previous one is permanently invalidated
- Use PKCE (code_challenge + code_verifier) for all OAuth 2.0 flows — never use the implicit flow
- Only request the minimum scopes needed: tweet.read and users.read for analytics (add offline.access for refresh token)
- Never expose your Client Secret in frontend code — all token exchanges must happen server-side
- Log API call counts and costs so you can track pay-per-use spending before your monthly bill arrives
- Validate that you are only fetching your own tweets for non_public_metrics — requesting these for other users' tweets triggers 403 errors
Automation use cases
Weekly Performance Email Digest
intermediateAutomatically send a formatted email every Monday with last week's impressions, engagement rate, top tweet, and week-over-week comparison.
Best-Time-to-Post Analysis
intermediateAnalyze historical tweet performance by hour-of-day and day-of-week to identify when your audience engages most.
Content Type Performance Breakdown
advancedCategorize tweets by type (plain text, thread, link, media) and compare average engagement rates to identify what content format works best.
Google Sheets Analytics Dashboard
intermediateAppend weekly tweet metrics to a Google Sheet automatically for tracking historical trends and sharing with stakeholders.
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 (Twitter) integration can trigger on new tweets and log metrics to spreadsheets, though real-time tweet.fields metrics are limited.
- + No code required
- + Easy Sheets/email integration
- + Reliable scheduling
- - Limited access to impression/organic metrics
- - Cannot aggregate across multiple tweets
- - Costly for high-volume tweet tracking
Make
Free tier available; paid from $9/monthMake has an X (Twitter) module supporting tweet lookup with metrics, enabling scheduled report builds with data transformation.
- + More flexible data transformation than Zapier
- + Lower cost per operation
- + Can aggregate metrics across tweet sets
- - Limited to Make's X module capabilities
- - No native spike detection
- - Requires paid plan for scheduled automation
n8n
Self-hosted free; cloud from €20/monthn8n's Twitter node supports fetching user tweets with metrics; combined with Code nodes, you can build full report logic including engagement rate calculation.
- + Self-hostable — no per-operation costs
- + Full code flexibility with Code nodes
- + Can output to any destination including email, Slack, Notion
- - Requires self-hosting setup
- - Twitter node may lag behind API v2 changes
- - No built-in cost tracking for pay-per-use
Best practices
- Include offline.access in your OAuth scopes — without it you cannot get a refresh token and users must re-authorize every 2 hours
- Track your monthly read consumption proactively — on pay-per-use, 100 tweets fetched daily costs ~$3/month, while Basic tier at $200/month includes 15,000 reads
- Store user_id persistently after the first lookup — /users/me costs a read each time and the ID never changes
- Use impression-based engagement rates now that follower counts are unavailable on self-serve tiers (removed April 2026)
- Always save historical report data locally — X API Basic tier only returns the last 7 days, so once the window passes, the data is gone
- Check x-rate-limit-remaining on every response and back off proactively when below 20% remaining
- Build token refresh into every API call function rather than as a separate step — token expiry can happen mid-run on long reports
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Twitter/X analytics report automation using the X API v2 in Python. I use GET /2/users/{id}/tweets with tweet.fields=public_metrics,organic_metrics to fetch recent tweets. My OAuth 2.0 access tokens expire every 2 hours and I need to use refresh_token to get a new one. The issue is that when I refresh the token, I get back a NEW refresh_token that replaces the old one, and I need to store it somewhere persistent. Can you help me write a token management class that stores access and refresh tokens to a file and automatically refreshes before expiry?
Build an X (Twitter) analytics dashboard UI. It should display a weekly metrics summary: total impressions, total engagements, average engagement rate, and number of tweets. Show a bar chart of daily impressions for the past 7 days. Include a table of recent tweets sorted by engagement rate with columns: tweet text (truncated), impressions, likes, retweets, replies, and engagement rate percentage. Add a note that follower counts are not available on the Basic tier. The data comes from a Python script that polls the X API weekly — design the frontend with mock data that matches the expected API schema.
Frequently asked questions
Is the X (Twitter) API free for analytics reports?
The Free tier is effectively unusable for analytics — it provides zero tweet reads. You need Basic ($200/month, 15,000 reads/month) or pay-per-use ($0.001/read for own tweets). For a weekly report fetching 100 tweets weekly, that is 400 reads/month on pay-per-use, costing about $0.40/month — well under the Basic tier cost.
Why can't I get follower count data anymore?
X removed follower_count and following_count from the public_metrics response for all self-serve API tiers (Free, Basic, and Pro's lower access levels) in April 2026. This data is only available on the Enterprise tier ($42,000+/year). Use impression-based engagement rates instead — they are more accurate anyway since they reflect actual content reach.
What happens when I hit the rate limit?
The API returns HTTP 429 with x-rate-limit-reset (Unix timestamp) and x-rate-limit-remaining (0) headers. Sleep until the reset time plus a 5-second buffer, then retry. For the monthly cap (15,000 reads on Basic), there is no retry — the cap resets on your billing cycle date.
My access token expired and I got a 401 error. How do I refresh it?
POST to https://api.twitter.com/2/oauth2/token with grant_type=refresh_token and your current refresh_token. You will get a new access_token (and usually a new refresh_token). Critical: immediately replace your stored refresh_token with the new one — the old token is permanently invalidated and cannot be used again.
Can I get analytics data for other users' tweets?
Only public_metrics (likes, retweets, replies, quotes) are available for other users' tweets. Non-public and organic metrics require the tweet author's OAuth token. Each other-user tweet read costs $0.005 on pay-per-use (vs $0.001 for your own tweets). Competitor analytics requires significant API budget.
How far back can I fetch tweet history?
Basic tier: GET /2/users/{id}/tweets returns the 3,200 most recent tweets. You can use start_time to filter by date. Full archive search (all tweets ever) requires the Academic Research product or the Pro/Enterprise tiers. For your own tweet history, the 3,200 tweet cap is usually sufficient for analytics.
Can RapidDev build a custom X analytics dashboard for my brand?
Yes. RapidDev has built 600+ integrations including social media analytics platforms. We can build production-grade X analytics automation with cost monitoring, multi-account support, and custom reporting. 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