# Audience Geography Source: https://docs.sorsa.io/Audience-Geography How to retrieve the country associated with any public X account and aggregate audience geography data from a follower list. > **Note:** For a fuller walkthrough with worked examples and country-distribution analysis, see [Twitter Audience Geography API: Analyze Followers by Country](https://api.sorsa.io/blog/twitter-audience-geography-api) on the blog. *** ## The `/about` Endpoint Returns "About" section metadata for any public X account: country, username change history, X Premium (Blue) status and start date, account source, and organization affiliation. ### Request ```bash theme={null} curl "https://api.sorsa.io/v3/about?username=elonmusk" \ -H "ApiKey: YOUR_API_KEY" ``` ```python theme={null} import requests resp = requests.get( "https://api.sorsa.io/v3/about", headers={"ApiKey": "YOUR_API_KEY"}, params={"username": "elonmusk"}, ) print(resp.json()) ``` ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `username` | string | One of three | Handle without @. | | `user_id` | string | One of three | Numeric user ID. | | `user_link` | string | One of three | Full profile URL. | ### Response ```json theme={null} { "country": "United States", "username_change_count": 1, "last_username_change_at": "2021-01-01T00:00:00Z", "premium_start_at": "2026-03-14T18:30:35Z", "is_blue_verified": true, "source": "US App Store", "affiliate_username": null } ``` ### Response fields | Field | Type | Description | | :- | :- | :- | | `country` | string | Country associated with the account, derived from platform-level signals (not the bio "Location" field). Returns `"Unknown"` when the platform has insufficient data. | | `username_change_count` | integer | Total number of handle changes on the account. | | `last_username_change_at` | string (ISO 8601) or null | Timestamp of the most recent handle change. `null` if the handle has never changed. | | `premium_start_at` | string (ISO 8601) or null | Date the account's X Premium (Blue) subscription started. `null` if the account is not on X Premium. | | `is_blue_verified` | boolean | Whether the account has a Blue (X Premium) verified badge. | | `source` | string | Source shown in the About section, such as the app store region or client the account registered from (for example, `"US App Store"`). | | `affiliate_username` | string or null | Handle of the affiliated organization or parent account. `null` when there is no affiliation. | For audience geography you only need `country`. The remaining fields come back on the same call and are documented here for completeness. *** ## Audience Geography Workflow To build a country distribution for any account's audience: 1. Extract the follower list with `GET /v3/followers`. 2. Call `GET /v3/about` for each follower using their `user_id`. 3. Aggregate the `country` values into a distribution. ```python theme={null} import requests import time from collections import Counter API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.sorsa.io/v3" HEADERS = {"ApiKey": API_KEY} def get_followers(username, max_pages=10): followers, cursor = [], None for _ in range(max_pages): params = {"username": username} if cursor: params["next_cursor"] = cursor resp = requests.get(f"{BASE_URL}/followers", headers=HEADERS, params=params, timeout=30) resp.raise_for_status() data = resp.json() followers.extend(data.get("users", [])) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.1) return followers def get_country(user_id): resp = requests.get( f"{BASE_URL}/about", headers=HEADERS, params={"user_id": user_id}, timeout=15, ) resp.raise_for_status() return resp.json().get("country") or "Unknown" def audience_geography(username, max_follower_pages=5): followers = get_followers(username, max_pages=max_follower_pages) countries = Counter() for user in followers: countries[get_country(user["id"])] += 1 time.sleep(0.05) # stay within 20 req/s return countries, len(followers) ``` ### Cost Each `/about` call counts as one request against your quota. For a 1,000-follower sample, that is roughly 1,005 requests: 1,000 country lookups and 5 follower-list pages (200 followers per page). Every new account starts with 100 free requests (no card required), enough to test a smaller geography sample before choosing a plan. See [Pricing](https://api.sorsa.io/pricing) for per-plan rates. *** ## Exporting to CSV ```python theme={null} import csv def export_to_csv(countries, total, path="geography.csv"): with open(path, "w", newline="") as f: w = csv.writer(f) w.writerow(["country", "count", "percentage"]) for country, count in countries.most_common(): w.writerow([country, count, round(count / total * 100, 2)]) ``` *** ## Data Accuracy `country` is the account country returned by `/about`; `location` on a profile is a separate free-text field. Report the returned country as an account-level label, not a verified residence or precise location. * Keep `"Unknown"` as its own category and report its share of the retrieved sample. Do not substitute a country or treat failed requests as unknown-country results. * The first follower pages are an ordered sample. State the sample size and collection date; do not assume they represent the full audience. * Compare distributions collected with the same sampling method. A larger sample alone does not remove selection bias. *** ## Related * [Followers & Following](https://docs.sorsa.io/followers-and-following) * [Target Audience Discovery](https://docs.sorsa.io/target-audiences-Discovery) * [Competitor Analysis](https://docs.sorsa.io/Competitor-Analysis) * [Marketing Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification) * [API Reference: Account About Info](https://docs.sorsa.io/api-reference/users-data/account-about-info) # Competitor Analysis Source: https://docs.sorsa.io/Competitor-Analysis # How to Analyze Competitors on Twitter Using the API This guide describes a complete workflow for analyzing competitors on X (formerly Twitter) using Sorsa API: profile benchmarking, content strategy decomposition, audience composition, public sentiment, and share of voice. Each phase maps to a specific endpoint, and the phases compose into a single weekly report you can run on a schedule. All examples use Python 3.8+ with `requests`. Replace `YOUR_API_KEY` with your actual key in every snippet. The complete consolidated script with all helper functions is at the bottom of this page. > **Free to start:** Every endpoint in this guide works on your first 100 requests: one-time, no credit card, no expiry. That is enough to trial the full workflow at shallow depth before choosing a plan. > **Note:** For a narrative walkthrough with strategy context and worked examples, see [Twitter Competitor Analysis: A Developer's Guide](https://api.sorsa.io/blog/twitter-competitor-analysis) on the blog. > **No-code option:** For ad-hoc side-by-side comparison without writing code, use the [Profile Comparison Tool](https://api.sorsa.io/playground/compare-users). It returns followers, engagement rate, average likes and retweets per tweet, posting frequency, and account age for any two handles. The [Engagement Calculator](https://api.sorsa.io/playground/engagement-calculator) covers per-tweet engagement-rate math for a single account. *** ## Setup ```python theme={null} import requests import time import csv from pathlib import Path from datetime import date, datetime, timedelta, timezone API_KEY = "YOUR_API_KEY" BASE = "https://api.sorsa.io/v3" HEADERS = {"ApiKey": API_KEY} JSON_HEADERS = {**HEADERS, "Content-Type": "application/json"} ``` The `ApiKey` header authenticates every request. See [Authentication](https://docs.sorsa.io/authentication) for details. *** ## Phase 1: Profile Benchmarking **Endpoints:** `GET /v3/info`, `GET /v3/info-batch` Establish the baseline: follower count, tweet volume, account age, bio, verified status. Use [`/info-batch`](https://docs.sorsa.io/api-reference/users-data/user-profile-batch) to fetch up to 100 profiles in a single request, which counts as one request against your quota. ### Snapshot script ```python theme={null} def get_profiles(usernames): """Fetch profiles for up to 100 accounts in a single API call.""" resp = requests.get( f"{BASE}/info-batch", headers=HEADERS, params=[("usernames", u) for u in usernames], timeout=30, ) resp.raise_for_status() return resp.json().get("users", []) competitors = ["stripe", "wise", "revolutapp"] profiles = get_profiles(competitors) print(f"{'Handle':<18} {'Followers':>12} {'Tweets':>10} {'Following':>10} {'Verified':>10}") print("-" * 64) for p in profiles: print( f"@{p['username']:<17} " f"{p['followers_count']:>12,} " f"{p['tweets_count']:>10,} " f"{p['followings_count']:>10,} " f"{str(p.get('verified', False)):>10}" ) ``` ### Tracking growth over time A single snapshot gives a baseline; measuring growth requires at least two dated observations. Log snapshots on a daily or weekly schedule (cron, GitHub Actions, etc.) and compute deltas: ```python theme={null} def log_snapshot(profiles, output_file="snapshots.csv"): """Append today's snapshot to a running CSV log.""" file_exists = Path(output_file).exists() today = date.today().isoformat() with open(output_file, "a", newline="") as f: writer = csv.writer(f) if not file_exists: writer.writerow(["date", "username", "followers", "tweets", "following"]) for p in profiles: writer.writerow([ today, p["username"], p["followers_count"], p["tweets_count"], p["followings_count"], ]) def compute_growth(csv_file, username, days=7): with open(csv_file, encoding="utf-8") as f: rows = [r for r in csv.DictReader(f) if r["username"].lower() == username.lower()] if len(rows) < 2: return None rows.sort(key=lambda row: row["date"]) latest_row = rows[-1] cutoff = date.fromisoformat(latest_row["date"]) - timedelta(days=days) earlier_rows = [r for r in rows if date.fromisoformat(r["date"]) <= cutoff] if not earlier_rows: return None latest = int(latest_row["followers"]) earlier = int(earlier_rows[-1]["followers"]) return ((latest - earlier) / earlier) * 100 if earlier else None log_snapshot(profiles) for handle in competitors: g = compute_growth("snapshots.csv", handle, days=7) if g is not None: print(f"@{handle}: {g:+.2f}% weekly follower growth") ``` The helper compares the latest snapshot with the most recent observation on or before the requested cutoff. With irregular snapshots, the actual interval can be longer than `days`; log daily if you need closer alignment. Store user IDs alongside handles in a production history so renames do not split one account's records. The growth formula: ```text theme={null} Growth Rate % = ((Followers Today - Followers N Days Ago) / Followers N Days Ago) * 100 ``` ### Bio and positioning changes `/info` returns `description`, `location`, `bio_urls`, and `created_at`. Diff these across snapshots to detect positioning shifts (bio changes, link destination changes) at zero additional cost. *** ## Phase 2: Content Strategy **Endpoints:** `POST /v3/user-tweets`, `POST /v3/search-tweets` Pull a competitor's recent tweets and decompose their content mix: original posts vs. replies vs. quotes vs. retweets, average engagement, top-performing posts. ### Fetching recent tweets [`/user-tweets`](https://docs.sorsa.io/api-reference/tweets/user-tweets) returns up to 20 tweets per page. Unlike the official X API's timeline endpoints, it is not hard-capped at 3,200 tweets, so you can paginate deeper into an account's history. For retrieval far back in time, `/search-tweets` with `since:`/`until:` operators (see below) is the more reliable approach. ```python theme={null} def fetch_user_tweets(username, max_pages=10): """Pull a competitor's recent tweets via pagination.""" all_tweets = [] cursor = None for _ in range(max_pages): body = {"username": username} if cursor: body["next_cursor"] = cursor resp = requests.post( f"{BASE}/user-tweets", headers=JSON_HEADERS, json=body, timeout=30, ) resp.raise_for_status() data = resp.json() all_tweets.extend(data.get("tweets", [])) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.1) return all_tweets ``` ### Decomposing the content mix ```python theme={null} def analyze_content(tweets, username): if not tweets: return None total = len(tweets) likes = [t.get("likes_count", 0) for t in tweets] retweets = [t.get("retweet_count", 0) for t in tweets] replies = [t.get("reply_count", 0) for t in tweets] original = sum(1 for t in tweets if not t.get("is_reply") and not t.get("retweeted_status")) reply_count = sum(1 for t in tweets if t.get("is_reply")) quote_count = sum(1 for t in tweets if t.get("is_quote_status")) with_media = sum(1 for t in tweets if t.get("entities")) top_tweet = max(tweets, key=lambda t: t.get("likes_count", 0)) return { "username": username, "sample_size": total, "avg_likes": sum(likes) / total, "avg_retweets": sum(retweets) / total, "avg_replies": sum(replies) / total, "original_pct": original / total * 100, "reply_pct": reply_count / total * 100, "quote_pct": quote_count / total * 100, "media_pct": with_media / total * 100, "top_tweet_likes": top_tweet.get("likes_count", 0), "top_tweet_text": top_tweet.get("full_text", "")[:200], } for handle in competitors: tweets = fetch_user_tweets(handle, max_pages=10) result = analyze_content(tweets, handle) if result: print(f"\n@{result['username']} (n={result['sample_size']})") print(f" Avg likes/tweet: {result['avg_likes']:.1f}") print(f" Avg retweets/tweet: {result['avg_retweets']:.1f}") print(f" Content mix: {result['original_pct']:.0f}% original / " f"{result['reply_pct']:.0f}% replies / {result['quote_pct']:.0f}% quotes / " f"{result['media_pct']:.0f}% with media") print(f" Top tweet: ({result['top_tweet_likes']} likes) {result['top_tweet_text']}") ``` Note that these categories overlap (a tweet with media is also an original post), so the percentages are independent shares rather than a partition that sums to 100. ### Historical comparison To compare two time windows for the same account (e.g., Q1 vs Q4), switch from `/user-tweets` to [`/search-tweets`](https://docs.sorsa.io/search-tweets) and use the `since:` and `until:` operators. See [Search Operators](https://docs.sorsa.io/search-operators) for the full syntax and [Historical Data](https://docs.sorsa.io/historical-data) for backfill patterns. ```python theme={null} def fetch_tweets_in_range(username, since_date, until_date): query = f"from:{username} since:{since_date} until:{until_date}" resp = requests.post( f"{BASE}/search-tweets", headers=JSON_HEADERS, json={"query": query, "order": "latest"}, timeout=30, ) resp.raise_for_status() return resp.json().get("tweets", []) # First page only; paginate for a full period. q1_tweets = fetch_tweets_in_range("stripe", "2026-01-01", "2026-04-01") q4_tweets = fetch_tweets_in_range("stripe", "2025-10-01", "2026-01-01") ``` *** ## Phase 3: Audience Composition **Endpoints:** `GET /v3/followers`, `GET /v3/verified-followers`, `GET /v3/followers-stats` A competitor's follower list reveals who their audience is. There are two approaches with different cost profiles. ### Verified followers (low cost) [`/verified-followers`](https://docs.sorsa.io/api-reference/users-data/verified-followers) returns only verified accounts following a handle. This is the highest-signal slice of any audience and is dramatically cheaper than pulling the full follower graph. ```python theme={null} def fetch_verified_followers(username, max_pages=10): all_users = [] cursor = None for _ in range(max_pages): params = {"username": username} if cursor: params["next_cursor"] = cursor resp = requests.get( f"{BASE}/verified-followers", headers=HEADERS, params=params, timeout=30, ) resp.raise_for_status() data = resp.json() all_users.extend(data.get("users", [])) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.1) return all_users for handle in competitors: verified = fetch_verified_followers(handle, max_pages=5) top = sorted(verified, key=lambda u: u.get("followers_count", 0), reverse=True)[:10] print(f"\n@{handle}: {len(verified)} verified followers fetched") for u in top: print(f" @{u['username']:<25} {u['followers_count']:>10,} followers") ``` Diff verified-follower lists across snapshots to detect new high-authority followers per competitor. Journalist follow events often precede coverage by 2-4 weeks. ### Full follower extraction (high cost) [`/followers`](https://docs.sorsa.io/api-reference/users-data/followers) returns up to 200 profiles per page. For an account with 1M followers, full extraction is roughly 5,000 requests. Plan accordingly: see [Pricing](https://api.sorsa.io/pricing) for plan limits and [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) for batch and budget patterns. ```python theme={null} def fetch_all_followers(username, max_pages=200): """Pull all followers via pagination. Cost scales with account size.""" all_users = [] cursor = None for _ in range(max_pages): params = {"username": username} if cursor: params["next_cursor"] = cursor resp = requests.get( f"{BASE}/followers", headers=HEADERS, params=params, timeout=30, ) resp.raise_for_status() data = resp.json() all_users.extend(data.get("users", [])) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.1) return all_users ``` ### Audience overlap Once you have follower lists for two accounts, compute overlap with set intersection on user IDs: ```python theme={null} followers_a = {u["id"] for u in fetch_all_followers("competitor_a")} followers_b = {u["id"] for u in fetch_all_followers("competitor_b")} overlap = followers_a & followers_b only_a = followers_a - followers_b only_b = followers_b - followers_a print(f"Shared audience: {len(overlap):,}") print(f"Unique to @competitor_a: {len(only_a):,}") print(f"Unique to @competitor_b: {len(only_b):,}") denominator = min(len(followers_a), len(followers_b)) overlap_ratio = len(overlap) / denominator if denominator else 0 print(f"Overlap ratio: {overlap_ratio:.1%}") ``` For a deeper dive on follower extraction patterns, see [Followers and Following](https://docs.sorsa.io/followers-and-following). ### Crypto and Web3 follower breakdown For accounts in Sorsa's crypto database, [`/followers-stats`](https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/follower-category-stats) returns a categorical breakdown: influencers, projects, VCs. See [Sorsa Score and Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) for context. ```python theme={null} def get_follower_breakdown(username): resp = requests.get( f"{BASE}/followers-stats", headers=HEADERS, params={"username": username}, timeout=30, ) resp.raise_for_status() return resp.json() for handle in ["VitalikButerin", "saylor"]: stats = get_follower_breakdown(handle) print(f"\n@{handle}:") print(f" Tracked followers: {stats['followers_count']}") print(f" Influencers: {stats['influencers_count']}") print(f" Projects: {stats['projects_count']}") print(f" VCs: {stats['venture_capitals_count']}") ``` Counts only include accounts already tracked in the Sorsa crypto database. *** ## Phase 4: Public Sentiment and Mentions **Endpoint:** `POST /v3/mentions` [`/mentions`](https://docs.sorsa.io/api-reference/search/search-mentions) supports filtering by minimum likes, retweets, replies, and date ranges. Filtering by `min_likes` cuts low-signal noise (bot replies, auto-tags) from the mention stream. See [Track Mentions](https://docs.sorsa.io/search-mentions) for the full filter set. ### Pulling high-engagement mentions ```python theme={null} def fetch_mentions(handle, min_likes=10, since_date=None, until_date=None, max_pages=5): all_mentions = [] cursor = None for _ in range(max_pages): body = {"query": handle, "order": "popular", "min_likes": min_likes} if since_date: body["since_date"] = since_date if until_date: body["until_date"] = until_date if cursor: body["next_cursor"] = cursor resp = requests.post( f"{BASE}/mentions", headers=JSON_HEADERS, json=body, timeout=30, ) resp.raise_for_status() data = resp.json() all_mentions.extend(data.get("tweets", [])) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.1) return all_mentions ``` ### Sentiment classification with VADER VADER is an open-source sentiment library tuned for social media text. It runs locally with no per-call cost and handles negation, intensifiers, and emoji reasonably well. ```python theme={null} # pip install vaderSentiment from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer analyzer = SentimentIntensityAnalyzer() def classify_sentiment(mentions): results = {"positive": [], "negative": [], "neutral": []} for m in mentions: score = analyzer.polarity_scores(m["full_text"])["compound"] if score >= 0.05: results["positive"].append((score, m)) elif score <= -0.05: results["negative"].append((score, m)) else: results["neutral"].append((score, m)) return results for handle in competitors: mentions = fetch_mentions(handle, min_likes=10, max_pages=5) s = classify_sentiment(mentions) print(f"\n@{handle}: {len(mentions)} mentions analyzed") print(f" Positive: {len(s['positive'])} Negative: {len(s['negative'])} Neutral: {len(s['neutral'])}") if s["negative"]: worst = min(s["negative"], key=lambda x: x[0]) text = worst[1]["full_text"][:150].replace("\n", " ") print(f" Sharpest negative: {text}...") ``` For higher accuracy on sarcasm, technical complaints, or mixed sentiment, pipe `full_text` into an LLM API (OpenAI, Anthropic). A hybrid approach (VADER for filtering, LLM only on flagged or high-engagement mentions) keeps cost predictable. *** ## Phase 5: Share of Voice Share of voice (SOV) measures one brand's mention volume against the category total. The formula: ```text theme={null} SOV = (your mentions in period) / (your mentions + sum of competitor mentions in period) ``` Implementation: ```python theme={null} def count_mentions(handle, days=7, min_likes=0): until = datetime.now(timezone.utc).date().isoformat() since = (datetime.now(timezone.utc) - timedelta(days=days)).date().isoformat() mentions = fetch_mentions( handle, min_likes=min_likes, since_date=since, until_date=until, max_pages=20, ) return len(mentions) brand = "your_handle" your_mentions = count_mentions(brand, days=7, min_likes=5) competitor_mentions = {h: count_mentions(h, days=7, min_likes=5) for h in competitors} total = your_mentions + sum(competitor_mentions.values()) print(f"\nShare of voice, last 7 days (min 5 likes):") print(f" @{brand:<20} {your_mentions:>5} ({(your_mentions/total*100 if total else 0):.1f}%)") for h, n in sorted(competitor_mentions.items(), key=lambda x: -x[1]): print(f" @{h:<20} {n:>5} ({(n/total*100 if total else 0):.1f}%)") ``` Notes: * Filter by minimum engagement (`min_likes=5` is a reasonable floor) to exclude bot and spam noise. * Track week-over-week deltas, not absolute snapshots. Category-level events skew absolute numbers in ways that obscure your own movement. * These counts are capped by `max_pages`, so they describe the retrieved sample. For comparable shares, use the same dates and filters and verify that pagination is exhausted for every brand; otherwise label the report as sampled. * To compute category-keyword SOV (e.g., "embedded finance" rather than brand mentions), replace `/mentions` with `/search-tweets` and use the keyword as the denominator query. *** ## Consolidated Weekly Report Script This script combines all five phases. Drop it into a cron job, GitHub Actions schedule, or any task runner. ```python theme={null} import requests import csv import time from datetime import date, datetime, timedelta, timezone from pathlib import Path from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer API_KEY = "YOUR_API_KEY" BASE = "https://api.sorsa.io/v3" HEADERS = {"ApiKey": API_KEY} JSON_HEADERS = {**HEADERS, "Content-Type": "application/json"} BRAND = "your_handle" COMPETITORS = ["competitor1", "competitor2", "competitor3"] SNAPSHOT_FILE = "snapshots.csv" analyzer = SentimentIntensityAnalyzer() def get_profiles(usernames): resp = requests.get( f"{BASE}/info-batch", headers=HEADERS, params=[("usernames", u) for u in usernames], timeout=30, ) resp.raise_for_status() return resp.json().get("users", []) def log_snapshot(profiles, output_file=SNAPSHOT_FILE): file_exists = Path(output_file).exists() today = date.today().isoformat() with open(output_file, "a", newline="") as f: writer = csv.writer(f) if not file_exists: writer.writerow(["date", "username", "followers", "tweets", "following"]) for p in profiles: writer.writerow([ today, p["username"], p["followers_count"], p["tweets_count"], p["followings_count"], ]) def compute_growth(csv_file, username, days=7): with open(csv_file, encoding="utf-8") as f: rows = [r for r in csv.DictReader(f) if r["username"].lower() == username.lower()] if len(rows) < 2: return None rows.sort(key=lambda row: row["date"]) latest_row = rows[-1] cutoff = date.fromisoformat(latest_row["date"]) - timedelta(days=days) earlier_rows = [r for r in rows if date.fromisoformat(r["date"]) <= cutoff] if not earlier_rows: return None latest = int(latest_row["followers"]) earlier = int(earlier_rows[-1]["followers"]) return ((latest - earlier) / earlier) * 100 if earlier else None def fetch_user_tweets(username, max_pages=5): all_tweets = [] cursor = None for _ in range(max_pages): body = {"username": username} if cursor: body["next_cursor"] = cursor resp = requests.post(f"{BASE}/user-tweets", headers=JSON_HEADERS, json=body, timeout=30) resp.raise_for_status() data = resp.json() all_tweets.extend(data.get("tweets", [])) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.1) return all_tweets def analyze_content(tweets, username): if not tweets: return None total = len(tweets) likes = [t.get("likes_count", 0) for t in tweets] original = sum(1 for t in tweets if not t.get("is_reply") and not t.get("retweeted_status")) with_media = sum(1 for t in tweets if t.get("entities")) return { "username": username, "sample_size": total, "avg_likes": sum(likes) / total, "original_pct": original / total * 100, "media_pct": with_media / total * 100, } def fetch_verified_followers(username, max_pages=3): all_users = [] cursor = None for _ in range(max_pages): params = {"username": username} if cursor: params["next_cursor"] = cursor resp = requests.get(f"{BASE}/verified-followers", headers=HEADERS, params=params, timeout=30) resp.raise_for_status() data = resp.json() all_users.extend(data.get("users", [])) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.1) return all_users def fetch_mentions(handle, min_likes=10, since_date=None, until_date=None, max_pages=5): all_mentions = [] cursor = None for _ in range(max_pages): body = {"query": handle, "order": "popular", "min_likes": min_likes} if since_date: body["since_date"] = since_date if until_date: body["until_date"] = until_date if cursor: body["next_cursor"] = cursor resp = requests.post(f"{BASE}/mentions", headers=JSON_HEADERS, json=body, timeout=30) resp.raise_for_status() data = resp.json() all_mentions.extend(data.get("tweets", [])) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.1) return all_mentions def classify_sentiment(mentions): results = {"positive": [], "negative": [], "neutral": []} for m in mentions: score = analyzer.polarity_scores(m["full_text"])["compound"] if score >= 0.05: results["positive"].append((score, m)) elif score <= -0.05: results["negative"].append((score, m)) else: results["neutral"].append((score, m)) return results def count_mentions(handle, days=7, min_likes=0): until = datetime.now(timezone.utc).date().isoformat() since = (datetime.now(timezone.utc) - timedelta(days=days)).date().isoformat() return len(fetch_mentions(handle, min_likes=min_likes, since_date=since, until_date=until, max_pages=20)) def header(text): line = "=" * 64 print(f"\n{line}\n{text}\n{line}") def run_weekly_report(): header("PHASE 1: PROFILE BENCHMARKS") profiles = get_profiles(COMPETITORS + [BRAND]) print(f"{'Handle':<18} {'Followers':>12} {'Tweets':>10} {'Verified':>10}") for p in profiles: print(f"@{p['username']:<17} {p['followers_count']:>12,} " f"{p['tweets_count']:>10,} {str(p.get('verified', False)):>10}") log_snapshot(profiles) for h in COMPETITORS + [BRAND]: g = compute_growth(SNAPSHOT_FILE, h, days=7) if g is not None: print(f" @{h}: {g:+.2f}% weekly follower growth") header("PHASE 2: CONTENT STRATEGY") for handle in COMPETITORS: tweets = fetch_user_tweets(handle, max_pages=5) result = analyze_content(tweets, handle) if result: print(f"@{result['username']}: avg {result['avg_likes']:.0f} likes/tweet, " f"{result['original_pct']:.0f}% original, " f"{result['media_pct']:.0f}% with media") header("PHASE 3: VERIFIED FOLLOWERS") for handle in COMPETITORS: verified = fetch_verified_followers(handle, max_pages=3) print(f"@{handle}: {len(verified)} verified followers in top pages") header("PHASE 4: SENTIMENT") for handle in COMPETITORS: mentions = fetch_mentions(handle, min_likes=10, max_pages=3) s = classify_sentiment(mentions) print(f"@{handle}: {len(s['positive'])} pos / {len(s['negative'])} neg " f"/ {len(s['neutral'])} neutral (n={len(mentions)})") header("PHASE 5: SHARE OF VOICE (7d)") your_n = count_mentions(BRAND, days=7, min_likes=5) comp_n = {h: count_mentions(h, days=7, min_likes=5) for h in COMPETITORS} total = your_n + sum(comp_n.values()) if total: print(f" @{BRAND}: {your_n} ({your_n/total*100:.1f}%)") for h, n in sorted(comp_n.items(), key=lambda x: -x[1]): print(f" @{h}: {n} ({(n/total*100 if total else 0):.1f}%)") if __name__ == "__main__": run_weekly_report() ``` At the default page depths in this script, a full run for three competitors is on the order of 100 requests (a little more for very active brands where mention pagination runs deeper, a little less otherwise). Run weekly, that is a few hundred requests a month, well inside the Starter plan (10,000 requests per month). The free 100 requests are enough to try the workflow first at shallow depth, on one or two competitors, before choosing a plan. See [Pricing](https://api.sorsa.io/pricing) for plan details. *** ## Next Steps * [Search Tweets](https://docs.sorsa.io/search-tweets): keyword and operator search across all of X. * [Track Mentions](https://docs.sorsa.io/search-mentions): deeper coverage of the `/mentions` endpoint with all filter combinations. * [Followers and Following](https://docs.sorsa.io/followers-and-following): full patterns for follower extraction, pagination, and CSV export. * [Historical Data](https://docs.sorsa.io/historical-data): backfill strategies for long time-window analysis. * [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring): polling patterns for detecting new tweets within seconds. * [Target Audience Discovery](https://docs.sorsa.io/target-audiences-Discovery): extract and segment competitor follower lists for lead generation. * [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage): batch endpoints, cursor handling, and request budgeting. * [API Reference](https://docs.sorsa.io/api-reference-guide): full specification for every endpoint used in this guide. # ID Conversion Source: https://docs.sorsa.io/ID-Conversion Convert between X (formerly Twitter) usernames, numeric user IDs, and profile URLs. Three lightweight utility endpoints, one request each. Every new account includes 100 free requests (no card required), so you can start converting right away. > **Note:** For a fuller explainer on usernames, user IDs, and profile links, see [Twitter ID Converter: Username, User ID, and Profile Link](https://api.sorsa.io/blog/twitter-id-converter) on the blog. > **No-code option:** for one-off conversions, use the free [Sorsa ID Converter](https://api.sorsa.io/playground/id-converter) web tool. Paste a username, ID, or profile URL and get the result instantly, no API key required. *** ## Why User IDs Matter A username (handle) can be changed at any time, and once released, can be claimed by someone else. A numeric user ID is assigned at account creation and never changes. If you are building anything that stores or references X accounts, work with user IDs: * **Handle changes do not break your system.** The ID points to the same account regardless of how many times the user renames. * **Preserve the full ID.** Keep IDs as strings in JSON and JavaScript. If your database uses an integer column, verify that its range supports the IDs without precision loss. * **Cross-time joins are clean.** When matching accounts across datasets collected at different times, IDs are the only safe key. * **Some endpoints are ID-based.** `/info-batch` takes a `user_ids` array (it also accepts `usernames`), and list and community endpoints reference the target list or community by its numeric ID. ## How User IDs Are Formatted X uses Snowflake IDs: 64-bit integers that pack a timestamp, a machine ID, and a sequence number into a single value. Tweet IDs have used this format since 2010. User IDs are different. X kept assigning sequential integer IDs to accounts for years after Snowflake launched, and only migrated user IDs to the Snowflake format around 2020. This is why older accounts have short IDs (Jack Dorsey's account is `12`) while accounts created after 2020 have 19-digit IDs. The practical consequence: you cannot decode a creation timestamp from older user IDs. If you need a registration date for an older account, fetch the profile and read `created_at`. *** ## Endpoint 1: Username to User ID ```http theme={null} GET /v3/username-to-id/{user_handle} ``` Converts a handle (without `@`) into the permanent numeric user ID. ```bash theme={null} curl "https://api.sorsa.io/v3/username-to-id/elonmusk" \ -H "ApiKey: YOUR_API_KEY" ``` ```json theme={null} {"id": "44196397"} ``` ```python theme={null} import requests API_KEY = "YOUR_API_KEY" def username_to_id(handle: str) -> str: resp = requests.get( f"https://api.sorsa.io/v3/username-to-id/{handle}", headers={"ApiKey": API_KEY}, ) resp.raise_for_status() return resp.json()["id"] print(username_to_id("elonmusk")) # "44196397" ``` ```javascript theme={null} async function usernameToId(handle) { const resp = await fetch( `https://api.sorsa.io/v3/username-to-id/${handle}`, { headers: { "ApiKey": "YOUR_API_KEY" } } ); return (await resp.json()).id; } ``` ## Endpoint 2: User ID to Username ```http theme={null} GET /v3/id-to-username/{user_id} ``` Resolves a numeric user ID back to the account's current handle. Useful for making stored IDs human-readable, or for detecting that an account has been renamed since you last looked. ```bash theme={null} curl "https://api.sorsa.io/v3/id-to-username/44196397" \ -H "ApiKey: YOUR_API_KEY" ``` ```json theme={null} {"handle": "elonmusk"} ``` ```python theme={null} def id_to_username(user_id: str) -> str: resp = requests.get( f"https://api.sorsa.io/v3/id-to-username/{user_id}", headers={"ApiKey": API_KEY}, ) resp.raise_for_status() return resp.json()["handle"] ``` ## Endpoint 3: Profile Link to User ID ```http theme={null} GET /v3/link-to-id?link={profile_url} ``` Extracts the permanent user ID from a full profile URL. Useful when processing links from spreadsheets, bookmarks, or scraped pages and you need to normalize them into IDs. ```bash theme={null} curl "https://api.sorsa.io/v3/link-to-id?link=https://x.com/elonmusk" \ -H "ApiKey: YOUR_API_KEY" ``` ```json theme={null} {"id": "44196397"} ``` ```python theme={null} def link_to_id(profile_url: str) -> str: resp = requests.get( "https://api.sorsa.io/v3/link-to-id", headers={"ApiKey": API_KEY}, params={"link": profile_url}, ) resp.raise_for_status() return resp.json()["id"] ``` > **Tip:** if you need both the user ID and the full profile, skip the conversion step and call [`/info`](https://docs.sorsa.io/api-reference/users-data/user-profile) with the `username` parameter. It returns the complete profile (including `id`) in a single request. See [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) for more patterns like this. *** ## Common Patterns ### Batch Conversion When you have a list of handles (CRM export, competitor list, spreadsheet) and need to convert them all to IDs: ```python theme={null} import requests import time API_KEY = "YOUR_API_KEY" BASE = "https://api.sorsa.io/v3" HEADERS = {"ApiKey": API_KEY} def batch_username_to_id(handles, pause=0.05): """ Resolve a list of handles to user IDs. Returns: dict mapping handle -> id (or None if lookup failed). """ results = {} for handle in handles: handle = handle.strip().lstrip("@") try: resp = requests.get(f"{BASE}/username-to-id/{handle}", headers=HEADERS, timeout=10) if resp.status_code == 200: results[handle] = resp.json()["id"] elif resp.status_code == 404: results[handle] = None # Account does not exist or is suspended elif resp.status_code == 429: time.sleep(1) retry = requests.get(f"{BASE}/username-to-id/{handle}", headers=HEADERS, timeout=10) results[handle] = retry.json()["id"] if retry.status_code == 200 else None else: results[handle] = None except requests.RequestException: results[handle] = None time.sleep(pause) return results handles = ["NASA", "SpaceX", "Tesla", "OpenAI", "stripe"] id_map = batch_username_to_id(handles) for handle, uid in id_map.items(): print(f"@{handle} -> {uid or '(not found)'}") ``` The reverse direction works the same way: swap the URL for `/id-to-username/{user_id}` and read the `handle` field from the response. This is useful for refreshing a database that may have stale display names. ### Normalizing Mixed Input When users submit account references in different formats (some as handles, some as URLs, some as IDs), normalize everything to user IDs. The check below skips the API call entirely when the input is already an ID: ```python theme={null} def normalize_to_id(value: str) -> str: """ Accepts a handle, an @handle, a profile URL, or a numeric ID. Returns the numeric user ID. """ value = value.strip().lstrip("@") if value.isdigit(): return value if "x.com/" in value or "twitter.com/" in value: return link_to_id(value) return username_to_id(value) # All four return the same ID for source in ["elonmusk", "@elonmusk", "https://x.com/elonmusk", "44196397"]: print(normalize_to_id(source)) ``` Use this as the first step in any ingestion pipeline so downstream logic always works with the stable identifier. ### Detecting Handle Changes If you stored both the user ID and the handle at collection time, you can periodically re-resolve the IDs and detect which accounts have renamed: ```python theme={null} def detect_renames(records): """ records: list of {"user_id": str, "stored_handle": str} Returns: list of accounts that have renamed. """ changes = [] for record in records: try: current = id_to_username(record["user_id"]) except requests.HTTPError: continue # Deleted, suspended, or transient error if current and current.lower() != record["stored_handle"].lower(): changes.append({ "user_id": record["user_id"], "old_handle": record["stored_handle"], "new_handle": current, }) time.sleep(0.05) return changes ``` For a richer audit, the [`/about`](https://docs.sorsa.io/api-reference/users-data/account-about-info) endpoint returns `username_change_count` and `last_username_change_at`. That tells you not just the current handle but how many times the account has renamed and when the most recent change happened. *** ## Next Steps * [Followers & Following](https://docs.sorsa.io/followers-and-following): most follower-extraction workflows start with ID conversion. * [Audience Geography](https://docs.sorsa.io/Audience-Geography): the `/about` endpoint accepts `user_id` and returns country data plus username change history. * [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage): avoid unnecessary conversion calls by using `/info` directly when you need the full profile. * [API Reference](https://docs.sorsa.io/api-reference-guide): full specification for `/username-to-id`, `/id-to-username`, `/link-to-id`, and every other Sorsa endpoint. # Marketing Campaign Verification Source: https://docs.sorsa.io/Marketing-Campaign-Verification # How to Verify Twitter Engagement Actions via API: Follows, Retweets, Comments, Quotes Reward campaigns on X (formerly Twitter) ask users to complete actions: follow an account, retweet a post, leave a comment, join a community. To hand out rewards fairly, you need to verify that each participant actually did what they claimed. Doing this manually does not scale past a handful of users, and honor-system checkboxes invite bots and fraud. Sorsa API provides a dedicated set of verification endpoints that answer simple questions: did this user follow that account? Did they retweet this tweet? Did they comment on it? Did they join this community? Each check is a single API call that returns a clear yes/no (or status) result, so you can build automated quest systems, giveaway platforms, referral programs, and engagement campaigns on top of verifiable, auditable data. This guide covers every verification endpoint with working code, then shows how to combine them into a complete campaign verification pipeline. Every new account includes 100 free requests (no card required) that work across all endpoints, so you can prototype the full flow before picking a plan. > **Note:** For a fuller walkthrough with extra workflows and end-to-end examples, see [Twitter Engagement Verification API: Full Campaign Guide](https://api.sorsa.io/blog/twitter-engagement-verification-api) on the blog. *** ## Available Verification Checks Here is what you can verify, which endpoint to use, and what you cannot check: | Action | Endpoint | Method | What it returns | | :- | :- | :- | :- | | User follows an account | `/check-follow` | POST | `{"follow": true/false}` | | User retweeted a tweet | `/check-retweet` | POST | `{"retweet": true/false}` | | User quoted a tweet | `/check-quoted` | POST | `{"status": "quoted" / "retweet" / "not_found"}` | | User commented on a tweet | `/check-comment` | GET | `{"commented": true/false}` | | User is a community member | `/check-community-member` | POST | `{"is_member": true/false}` | **What you cannot verify:** Likes. X made likes private in 2024, so no API (including the official one) can check whether a specific user liked a specific tweet. Design your campaigns around the five actions above. *** ## Check 1: Did the User Follow an Account? The most common campaign task. "Follow @YourBrand to enter the giveaway." **Endpoint:** `POST /v3/check-follow` The endpoint answers "does user\_2 follow user\_1?". Set `user_1` to the brand (the account being followed) and `user_2` to the participant. ### Simplest Example ```bash theme={null} curl -X POST https://api.sorsa.io/v3/check-follow \ -H "ApiKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "username_1": "YourBrand", "username_2": "participant_handle" }' ``` Response: ```json theme={null} { "follow": true, "user_protected": false } ``` ### Parameters Provide exactly one identifier for each side: | Side | Options (provide one) | | :- | :- | | Brand account (`user_1`, the account being followed) | `username_1`, `user_link_1`, or `user_id_1` | | Participant (`user_2`) | `username_2`, `user_link_2`, or `user_id_2` | ### Python ```python theme={null} import requests API_KEY = "YOUR_API_KEY" BASE = "https://api.sorsa.io/v3" HEADERS = {"ApiKey": API_KEY, "Content-Type": "application/json"} def check_follow(brand_handle: str, participant_handle: str) -> dict: resp = requests.post( f"{BASE}/check-follow", headers=HEADERS, json={"username_1": brand_handle, "username_2": participant_handle}, timeout=15, ) resp.raise_for_status() return resp.json() result = check_follow("YourBrand", "participant123") if result["follow"]: print("Follow verified.") elif result.get("user_protected"): print("Account is private; follow cannot be confirmed.") else: print("Not following.") ``` If `user_protected` is `true`, the participant's account is private and their follow relationships cannot be verified. *** ## Check 2: Did the User Retweet a Tweet? "Retweet this post to enter." The endpoint scans up to 100 retweets per request and paginates for tweets with more retweets than that. **Endpoint:** `POST /v3/check-retweet` ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `tweet_link` | string | Yes | URL or ID of the tweet to verify. | | `username` / `user_link` / `user_id` | string | Yes (one) | The participant. Provide exactly one. | | `next_cursor` | string | No | Pagination cursor for tweets with more than 100 retweets. | ### Python ```python theme={null} def check_retweet(tweet_link: str, participant_handle: str) -> bool: cursor = None for _ in range(5): # check up to 500 retweets total body = {"tweet_link": tweet_link, "username": participant_handle} if cursor: body["next_cursor"] = cursor resp = requests.post(f"{BASE}/check-retweet", headers=HEADERS, json=body, timeout=15) resp.raise_for_status() data = resp.json() if data["retweet"]: return True cursor = data.get("next_cursor") if not cursor: return False raise RuntimeError("Retweet verification incomplete: page limit reached") ``` Each call scans the most recent 100 retweets. For most campaigns one request is enough, because users tend to retweet shortly after a campaign starts, so their retweet lands in the most recent batch. For popular tweets where the user retweeted early, page through `next_cursor`. *** ## Check 3: Did the User Quote a Tweet? "Quote tweet this post with your thoughts." The `/check-quoted` endpoint distinguishes a quote tweet from a plain retweet and returns a status string. **Endpoint:** `POST /v3/check-quoted` ### Python ```python theme={null} def check_quoted(tweet_link: str, participant_handle: str) -> dict: resp = requests.post( f"{BASE}/check-quoted", headers=HEADERS, json={"tweet_link": tweet_link, "username": participant_handle}, timeout=15, ) resp.raise_for_status() return resp.json() data = check_quoted("https://x.com/YourBrand/status/1234567890", "participant123") if data["status"] == "quoted": print(f"Quote verified! They wrote: {data['text']}") elif data["status"] == "retweet": print("They retweeted but did not quote.") else: print("No quote or retweet found.") ``` ### Response ```json theme={null} { "status": "quoted", "date": "2026-03-10 14:22:09", "text": "This is amazing, everyone should check this out!", "user_protected": false } ``` The `status` field returns one of three values: `"quoted"` (user posted a quote tweet), `"retweet"` (user retweeted without adding text), or `"not_found"` (neither action detected). When a quote exists, the response also includes its date and text, which you can use for content quality checks (minimum length, required hashtag, profanity filter). ```python theme={null} def quote_is_acceptable(quote_text: str, min_length: int = 30, required_hashtag: str = None) -> bool: if len(quote_text.strip()) < min_length: return False if required_hashtag and required_hashtag.lower() not in quote_text.lower(): return False return True ``` *** ## Check 4: Did the User Comment on a Tweet? "Leave a comment under this post." This is the only verification endpoint that uses GET instead of POST. **Endpoint:** `GET /v3/check-comment` ### Parameters (query string) | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `tweet_link` | string | Yes | URL or ID of the tweet. | | `username` / `user_link` / `user_id` | string | Yes (one) | The participant. Provide exactly one. | ### Python ```python theme={null} def check_comment(tweet_link: str, participant_handle: str) -> dict: resp = requests.get( f"{BASE}/check-comment", headers={"ApiKey": API_KEY}, params={"tweet_link": tweet_link, "username": participant_handle}, timeout=15, ) resp.raise_for_status() return resp.json() data = check_comment("https://x.com/YourBrand/status/1234567890", "participant123") if data["commented"]: print(f"Comment verified: {data['tweet']['full_text'][:100]}") else: print("No comment found.") ``` When `commented` is `true`, the response includes the full `tweet` object of the comment itself, with text, engagement metrics, and timestamp. Use it to enforce comment quality (minimum length, required hashtag, no emoji-only replies) beyond just checking that a reply exists. ```python theme={null} def comment_is_acceptable(comment: dict, min_length: int = 20, required_keyword: str = None) -> bool: text = comment.get("full_text", "").strip() if len(text) < min_length: return False if required_keyword and required_keyword.lower() not in text.lower(): return False if len(text.split()) < 3: return False return True ``` *** ## Check 5: Is the User a Community Member? Confirm current Community data availability with [support](https://docs.sorsa.io/support) before making this a campaign requirement. See the availability note in [Lists and Communities](https://docs.sorsa.io/lists-and-communities). "Join our X Community to participate." Useful for campaigns that require community membership as a prerequisite. **Endpoint:** `POST /v3/check-community-member` ### Python ```python theme={null} def check_community_member(community_id: str, participant_handle: str) -> bool: resp = requests.post( f"{BASE}/check-community-member", headers=HEADERS, json={"community_id": community_id, "username": participant_handle}, timeout=15, ) resp.raise_for_status() return resp.json().get("is_member", False) is_member = check_community_member("1966045657589813686", "participant123") print("Member" if is_member else "Not a member") ``` The community ID is the numeric string in the community URL (`x.com/i/communities/`). *** ## Building a Campaign Verification Pipeline In a real campaign, users complete multiple tasks. The pattern below runs all five checks for a single participant, returns a structured result, and applies quality rules to the comment and quote. ```python theme={null} from dataclasses import dataclass, field @dataclass class CampaignConfig: brand_handle: str tweet_to_retweet: str tweet_to_quote: str tweet_to_comment: str community_id: str required_hashtag: str = "" min_quote_length: int = 30 min_comment_length: int = 20 @dataclass class ParticipantResult: username: str follow: bool = False retweet: bool = False quote: bool = False quote_text: str = "" comment: bool = False comment_text: str = "" community: bool = False completed: int = field(init=False, default=0) def total(self) -> int: return sum([self.follow, self.retweet, self.quote, self.comment, self.community]) def verify_participant(username: str, cfg: CampaignConfig) -> ParticipantResult: r = ParticipantResult(username=username) r.follow = check_follow(cfg.brand_handle, username)["follow"] r.retweet = check_retweet(cfg.tweet_to_retweet, username) quote_data = check_quoted(cfg.tweet_to_quote, username) if quote_data["status"] == "quoted": r.quote_text = quote_data.get("text", "") r.quote = quote_is_acceptable(r.quote_text, cfg.min_quote_length, cfg.required_hashtag) comment_data = check_comment(cfg.tweet_to_comment, username) if comment_data.get("commented"): r.comment_text = comment_data["tweet"].get("full_text", "") r.comment = comment_is_acceptable(comment_data["tweet"], cfg.min_comment_length) r.community = check_community_member(cfg.community_id, username) r.completed = r.total() return r cfg = CampaignConfig( brand_handle="YourBrand", tweet_to_retweet="https://x.com/YourBrand/status/111111111", tweet_to_quote="https://x.com/YourBrand/status/222222222", tweet_to_comment="https://x.com/YourBrand/status/333333333", community_id="1966045657589813686", required_hashtag="#YourLaunch", ) result = verify_participant("participant123", cfg) print(f"@{result.username}: {result.completed}/5 tasks done") ``` The first page of each of the five checks uses five requests. Retweet pagination and retries add requests, so five is a baseline rather than a fixed cost. A page-budget exception is an incomplete verification, not evidence that the participant failed the task. *** ## Verifying Participants in Bulk When a campaign has thousands of participants, verify them in batch. The pattern below respects the rate limit, writes results to CSV, and is resumable: it writes a row after each participant, so a crash does not lose progress. ```python theme={null} import csv import time from pathlib import Path def verify_campaign_batch(usernames: list[str], cfg: CampaignConfig, output_file: str) -> None: fields = ["username", "follow", "retweet", "quote", "comment", "community", "completed", "quote_text", "comment_text"] already_done = set() out_path = Path(output_file) if out_path.exists(): with out_path.open() as f: already_done = {row["username"] for row in csv.DictReader(f)} mode = "a" if out_path.exists() else "w" with out_path.open(mode, newline="") as f: writer = csv.DictWriter(f, fieldnames=fields) if mode == "w": writer.writeheader() for i, username in enumerate(usernames): if username in already_done: continue for attempt in range(3): try: r = verify_participant(username, cfg) writer.writerow({ "username": r.username, "follow": r.follow, "retweet": r.retweet, "quote": r.quote, "comment": r.comment, "community": r.community, "completed": r.completed, "quote_text": r.quote_text, "comment_text": r.comment_text, }) f.flush() already_done.add(username) print(f"[{i+1}/{len(usernames)}] @{username}: {r.completed}/5") break except RuntimeError as e: print(f"[{i+1}] @{username}: INCOMPLETE {e}") break except requests.HTTPError as e: if e.response.status_code == 429: time.sleep(5) continue # retry the same participant print(f"[{i+1}] @{username}: ERROR {e}") break time.sleep(0.25) participants = open("entries.txt").read().splitlines() verify_campaign_batch(participants, cfg, "campaign_results.csv") ``` The loop waits between participants and retries a `429` up to three times, but retweet pagination can add calls within a participant. Use a shared request limiter for a production worker pool. Actual throughput depends on page depth and response latency. Incomplete or failed participants must remain eligible for retry rather than being recorded as a negative result. *** ## Account Ownership Verification Before a user joins a campaign, you may want to confirm they actually own the X handle they provided. A common pattern: 1. Generate a unique code (for example, `VERIFY-a8f3b2`) and show it to the user. 2. Ask them to post a tweet containing that code. 3. Use `/user-tweets` to fetch their recent tweets and check whether the code appears. ```python theme={null} import secrets def generate_verification_code() -> str: return f"VERIFY-{secrets.token_hex(4)}" def verify_account_ownership(username: str, expected_code: str) -> bool: resp = requests.post( f"{BASE}/user-tweets", headers=HEADERS, json={"username": username}, timeout=15, ) resp.raise_for_status() tweets = resp.json().get("tweets", []) for tweet in tweets: author = tweet.get("user") or {} if (author.get("username", "").lower() == username.lstrip("@").lower() and not tweet.get("retweeted_status") and expected_code in (tweet.get("full_text") or "")): return True return False code = generate_verification_code() print(f"Ask the user to tweet: {code}") # ... after the user tweets ... if verify_account_ownership("participant123", code): print("Account ownership confirmed.") ``` Bind each challenge to the signed-in participant and intended X account, give it a short expiration, and consume it once. Verify the matching post's author and creation time against that challenge. The example checks author and text; your application must implement challenge storage, expiry, and one-time use. The participant can delete the tweet after verification. *** ## Anti-Fraud Considerations Use the checks below as configurable eligibility or review criteria. Profile age and counts do not prove whether an account is legitimate: * **Minimum account age.** Fetch the participant's profile via `/info` and check `created_at`. Reject accounts created in the last 30 days, since most bot farms use fresh accounts. * **Minimum activity.** Check `tweets_count` and `followers_count`. Low counts can justify additional review, but do not establish that an account is a bot. * **Comment quality.** When verifying comments via `/check-comment`, the response includes the full tweet text. Check for minimum length, required keywords or hashtags, and reject single-character or emoji-only replies. * **Quote quality.** The `/check-quoted` response includes the quote text. Apply the same quality checks as for comments. * **Rate of completion.** Fast completion is a review signal, not proof of automation. Log timestamps and flag suspiciously fast completions. ```python theme={null} from datetime import datetime, timezone def is_legitimate_account( username: str, min_age_days: int = 30, min_tweets: int = 10, min_followers: int = 5, ) -> tuple[bool, dict]: resp = requests.get( f"{BASE}/info", headers={"ApiKey": API_KEY}, params={"username": username}, timeout=15, ) resp.raise_for_status() profile = resp.json() created = datetime.fromisoformat(profile["created_at"].replace("Z", "+00:00")) age_days = (datetime.now(timezone.utc) - created).days checks = { "account_age_ok": age_days >= min_age_days, "has_tweets": profile.get("tweets_count", 0) >= min_tweets, "has_followers": profile.get("followers_count", 0) >= min_followers, "not_protected": not profile.get("protected", False), } return all(checks.values()), checks ``` Run this before the five verification checks. If `is_legitimate_account` returns `False`, you skip 5 verification requests on a participant you would have rejected anyway. *** ## Scoring Participants by Influence Not all participants have equal reach. A retweet from an account with 50,000 followers is worth more to a campaign than one from an account with 50. Use `/info` to fetch the participant's profile and weight their reward by follower count. ```python theme={null} import math BASE_POINTS = {"follow": 10, "retweet": 15, "quote": 25, "comment": 20, "community": 10} def get_follower_count(username: str) -> int: resp = requests.get( f"{BASE}/info", headers={"ApiKey": API_KEY}, params={"username": username}, timeout=15, ) resp.raise_for_status() return resp.json().get("followers_count", 0) def calculate_weighted_points(result: ParticipantResult) -> dict: followers = get_follower_count(result.username) # log scaling: 100 followers -> 2x, 10K -> 4x, 1M -> 6x multiplier = max(1.0, math.log10(followers + 1)) total = 0 breakdown = {} for task, base in BASE_POINTS.items(): if getattr(result, task): points = round(base * multiplier) breakdown[task] = points total += points return {"followers": followers, "multiplier": round(multiplier, 2), "breakdown": breakdown, "total": total} ``` For crypto-focused campaigns, replace the follower-count multiplier with the [Sorsa Score](https://docs.sorsa.io/sorsa-score-and-crypto-analytics), which measures recognition among crypto KOLs, projects, and VCs. *** ## A Note on Likes X (Twitter) made likes private in 2024. The platform no longer exposes which users liked a specific tweet through any public API: not Sorsa, not the official X API, not any third-party tool. If a campaign previously included a "Like this tweet" task, replace it with a retweet or comment requirement, both of which remain fully verifiable. *** ## Next Steps * [Search Tweets](https://docs.sorsa.io/search-tweets): find campaign-related tweets by keyword for broader monitoring. * [Track Mentions](https://docs.sorsa.io/search-mentions): track organic mentions of your brand alongside campaign-driven mentions. * [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring): verify tasks in near real time by polling for new activity. * [Followers & Following](https://docs.sorsa.io/followers-and-following): extract your own follower list to cross-reference with campaign participants. * [Pricing](https://api.sorsa.io/pricing): estimate campaign costs (5 requests per participant for full verification). * [API Reference](https://docs.sorsa.io/api-reference-guide): full specification for all verification endpoints. # Welcome to Sorsa API Source: https://docs.sorsa.io/Welcome-to-Sorsa-API Sorsa API gives you fast, structured access to public X (formerly Twitter) data through a simple REST interface. It covers user profiles, tweets, search, follower graphs, list and community content, and crypto-related audience analytics, all through a single API key, with no OAuth, no app approval, and no rate limit surprises. Think of it as a reliable X data source you do not have to build or maintain yourself: a managed alternative to the official Twitter API, designed for developers, data engineers, analysts, researchers, growth teams, and AI product builders who want to extract, monitor, and analyze public X content at scale. > **AI Docs / LLM-ready documentation.** Building with an LLM, AI agent, or coding assistant? Point it at our machine-readable docs so it has full context before generating any request: the [full plain-text API documentation](https://api.sorsa.io/v3/static/sorsa_api_v3_documentation_en.txt). *** ## Who is Sorsa API for? Sorsa is built for anyone who needs programmatic access to X platform data. You do not need an official X developer account, Twitter API approval, or complex OAuth setup to get started. Common profiles include backend developers building social data pipelines, data scientists running sentiment analysis, growth marketers tracking brand mentions, Web3 teams verifying community engagement, no-code builders connecting X data to automation tools like Make or Zapier, and AI engineers collecting training datasets from public conversations. *** ## Quick start **Base URL** ```text theme={null} https://api.sorsa.io/v3 ``` **Authentication** Include your API key in the `ApiKey` header with every request. No OAuth flows, no bearer tokens, no callback URLs. **Response format** All endpoints return JSON. **Example request** ```bash theme={null} curl --request GET \ --url "https://api.sorsa.io/v3/info?username=elonmusk" \ --header "ApiKey: YOUR_API_KEY" ``` New here? Follow the [Quickstart guide](https://docs.sorsa.io/quickstart) to get your API key and send your first request in under three minutes. **Free to start.** Every new account comes with 100 free requests: no credit card required, no expiry, and access to all available endpoints. Use them to try any endpoint before choosing a plan. *** ## Building with AI? Sorsa is designed to be easy to wire into LLM-powered agents, AI coding assistants, and automated workflows. Instead of pasting endpoints by hand, feed your model the full documentation context directly: * [AI Docs page](https://docs.sorsa.io/ai-docs) - an overview of how to consume Sorsa documentation programmatically. * [Documentation index (llms.txt)](https://docs.sorsa.io/llms.txt) - a compact map of every available page, so an agent can discover the docs before exploring further. * [Full plain-text API reference](https://api.sorsa.io/v3/static/sorsa_api_v3_documentation_en.txt) - the complete, LLM-readable specification in a single file. Drop it into your prompt, IDE, or retrieval index. *** ## API capabilities A high-level overview of what you can retrieve. For complete schemas, parameters, and response examples, see the [API Reference](https://docs.sorsa.io/api-reference-guide). ### Users Retrieve user profiles, audience metrics, account history, and relationship graphs, including followers, followings, and verified followers. Profiles can be fetched one at a time or in batches of up to 100. Key endpoints: `GET /info`, `GET /info-batch`, `GET /about`, `GET /followers`, `GET /follows`, `GET /verified-followers` ### Tweets Access tweet content, engagement metrics, threads, replies, quotes, retweeters, user timelines, long-form X Articles, and trending topics by location. Fetch a single tweet or up to 100 at once. Key endpoints: `POST /tweet-info`, `POST /tweet-info-bulk`, `POST /user-tweets`, `POST /comments`, `POST /quotes`, `POST /retweeters`, `POST /article`, `GET /trends` ### Search Run query-based search across tweets and users with full support for X Advanced Search syntax (`from:`, `to:`, `since:`, `until:`, hashtags, and more), plus dedicated mention tracking and Spaces lookups. Build queries visually with the free [Search Builder](https://api.sorsa.io/playground/search-builder), and see the full [Search Operators](https://docs.sorsa.io/search-operators) reference. Key endpoints: `POST /search-tweets`, `POST /mentions`, `POST /search-users`, `GET /spaces` ### Verification Validate whether specific social actions have occurred: follow, comment, quote, retweet, or community membership. Useful for access gating and marketing campaign verification. Key endpoints: `POST /check-follow`, `GET /check-comment`, `POST /check-quoted`, `POST /check-retweet`, `POST /check-community-member` ### Lists Retrieve members, followers, and tweet feeds from public X Lists. Key endpoints: `GET /list-members`, `GET /list-followers`, `GET /list-tweets` ### Communities Access X Communities content, membership data, and in-community search. Key endpoints: `POST /community-tweets`, `POST /community-search-tweets`, `POST /community-members` ### Crypto and Web3 analytics Track an account's Sorsa Score (popularity among influencers, projects, and VCs), score changes over time, follower category breakdowns, top followers and followings by score, and new follow activity over the last 7 days. Key endpoints: `GET /score`, `GET /score-changes`, `GET /followers-stats`, `GET /top-followers`, `GET /top-following`, `GET /new-followers-7d`, `GET /new-following-7d` ### Technical utilities Resolve usernames to user IDs and back, extract IDs from profile links, and monitor your API quota usage. You can also convert IDs in the browser with the free [ID Converter](https://api.sorsa.io/playground/id-converter). Key endpoints: `GET /username-to-id`, `GET /id-to-username`, `GET /link-to-id`, `GET /key-usage-info` *** ## Typical use cases **Brand and media monitoring.** Track mentions, keywords, and sentiment around your brand or competitors in real time. **Data pipelines.** Export structured X data into BigQuery, PostgreSQL, S3, or any downstream data warehouse. **Audience analysis.** Map follower graphs, identify high-value connections, and analyze creator growth over time. **Access gating and social verification.** Confirm that users have completed social tasks (follow, retweet, comment, quote) before granting access to campaigns, token drops, or gated content. **AI and LLM training data.** Collect high-quality public conversational data for model fine-tuning, sentiment analysis, or research datasets. **No-code and low-code automation.** Connect Sorsa API to tools like Make (Integromat), Zapier, n8n, or Pipedream via HTTP request modules. Monitor accounts, trigger workflows on new mentions, or sync X data to spreadsheets and CRMs without writing code. For detailed implementation patterns and workflow examples, see the [Use Cases Guide](https://docs.sorsa.io/use-cases-overview). *** ## Try it without code You do not need to write a single line of code to explore Sorsa. The [API Playground](https://api.sorsa.io/playground) lets you call any endpoint from the browser, view live responses, and experiment with parameters before integrating. We also offer a set of free, ready-to-use tools built on top of the API: * [Search Builder](https://api.sorsa.io/playground/search-builder) - construct advanced tweet search queries visually. * [ID Converter](https://api.sorsa.io/playground/id-converter) - convert between username, user ID, and profile link. * [Shadowban Check](https://api.sorsa.io/playground/shadowban-check) - test whether an account is shadowbanned. * [Media Downloader](https://api.sorsa.io/playground/media-downloader) - download photos and videos from tweets. * [Compare Users](https://api.sorsa.io/playground/compare-users) - compare two accounts side by side on key metrics. * [Recent Followers](https://api.sorsa.io/playground/recent-followers) - view an account's latest followers. * [Engagement Calculator](https://api.sorsa.io/playground/engagement-calculator) - estimate engagement rate from recent tweets. * [Follow Check](https://api.sorsa.io/playground/follow-check) - check whether two accounts follow each other. * [Location Check](https://api.sorsa.io/playground/location-check) - find which country an X account is based in. * [Account Age Check](https://api.sorsa.io/playground/account-age-check) - see when an account was created. For deeper guides, tutorials, and migration walkthroughs, browse the [Sorsa Blog](https://api.sorsa.io/blog). *** ## What Sorsa API does not support Sorsa is a read-only API focused on data extraction and analysis. It does not perform write operations on the X platform: * Posting tweets, replies, or direct messages * Liking, bookmarking, or retweeting * Following or unfollowing accounts * Any action performed on behalf of a user account If your use case requires write access, you can use the official X API for those operations alongside Sorsa for reliable read access at scale. *** ## Next steps * [Quickstart](https://docs.sorsa.io/quickstart) - Get your API key and make your first request * [Authentication](https://docs.sorsa.io/authentication) - Learn how to authorize requests to the v3 API * [API Reference](https://docs.sorsa.io/api-reference-guide) - Browse all available endpoints with parameters and response schemas * [Pricing](https://docs.sorsa.io/pricing) - Plans and request packages (every account starts with 100 free requests) * [Rate Limits](https://docs.sorsa.io/rate-limits) - Learn about usage quotas and throttling * [Use Cases Guide](https://docs.sorsa.io/use-cases-overview) - Real-world implementation patterns and workflows # AI Docs Source: https://docs.sorsa.io/ai-docs # AI-Ready Documentation: Use Sorsa API Docs with LLMs Sorsa's documentation is built for use with AI assistants and large language models. You can feed the complete API reference directly into ChatGPT, Claude, Grok, Cursor, Copilot, or any other LLM to get accurate answers, generate integration code, and debug issues without manually reading through pages of docs. *** ## The AI Docs File We maintain a single, machine-readable file, `sorsa_api_v3_documentation_en.txt`, that contains the full Sorsa API specification: every endpoint, parameter, response schema, and authentication detail in one place. [https://api.sorsa.io/v3/static/sorsa\_api\_v3\_documentation\_en.txt](https://api.sorsa.io/v3/static/sorsa_api_v3_documentation_en.txt) Attach the file or add it to your project context. If it exceeds your tool's context budget, use the [documentation index](https://docs.sorsa.io/llms.txt) to select the endpoint and shared-mechanics pages needed for your task. *** ## How to Use It **ChatGPT / Claude / Grok / Gemini:** Copy the contents of the file (or attach it as a file) at the start of your conversation, then ask questions like "Write a Python script that searches for tweets about AI and exports them to CSV" or "How do I paginate through all followers of an account?" **Cursor / Copilot / Claude Code / Codex / Windsurf:** Add the file to your project context or reference it in your prompt. Ask the assistant to use the documented endpoint URLs, parameters, headers, and response schemas. Review the generated code against the endpoint reference and test a small request before running a batch job. **Custom GPTs and assistants:** Upload the file as a knowledge file when building a custom GPT or AI assistant that needs to work with X/Twitter data via Sorsa. *** ## What's Inside The file is a structured reference for the entire API: * The base URL, authentication method, and common error codes * Shared data models (User, Tweet, Follower, Space, and more) with every field name and type * All endpoint paths grouped by category, each with its HTTP method and its required and optional parameters * Example requests and responses for every endpoint * Pagination behavior, sorting options, per-request limits, and date formats * Search syntax support, including common operators and a link to the complete operator reference * Edge cases and special response formats, such as the field-name differences between Tweet and Article objects (for example, `view_count` vs `views_count`) It is kept up to date as the API evolves, so you always get the current specification. *** ## Why This Matters LLMs are only as good as the context they have. Without accurate API documentation, an AI assistant will guess at parameter names, invent response fields, or use outdated patterns. Providing the reference reduces guessing, but generated code still needs validation. Check the HTTP method, request fields, response wrapper, pagination, and error handling before using it. *** ## Next Steps To run the code your AI assistant generates, you need an API key. Every new account includes 100 free requests with no credit card required, so you can test generated code right away. * [Quickstart](https://docs.sorsa.io/quickstart) - get an API key and make your first request * [Authentication](https://docs.sorsa.io/authentication) - how to pass your API key in requests * [API Reference](https://docs.sorsa.io/api-reference-guide) - the same specification in browsable form # Api Reference Guide Source: https://docs.sorsa.io/api-reference-guide # Complete API Reference: All Sorsa Endpoints All Sorsa API endpoints are listed below, organized by category. Every endpoint uses the same base URL (`https://api.sorsa.io/v3`) and authenticates via the `ApiKey` header. Every call costs exactly 1 request from your quota, regardless of which endpoint you use, and new accounts start with 100 free requests to test all available endpoints (no card required). For detailed parameter tables, response schemas, and interactive testing, click the **Docs** link next to each endpoint. You can also test any endpoint without code using the [API Playground](https://api.sorsa.io/playground). *** ## Users Data Endpoints for retrieving user profiles, follower and following lists, and account metadata. | Endpoint | Method | Description | Docs | | :- | :- | :- | :- | | `/info` | GET | Full profile for a single account (bio, counts, verified status, avatar). Accepts `username`, `user_id`, or `user_link`. | [Docs](https://docs.sorsa.io/api-reference/users-data/user-profile) | | `/info-batch` | GET | Profiles for up to 100 accounts in one request. Accepts `usernames[]` or `user_ids[]`. | [Docs](https://docs.sorsa.io/api-reference/users-data/user-profile-batch) | | `/about` | GET | Account metadata: country, username change count and date, X Premium (Blue) status and start date, account source, and affiliate handle. | [Docs](https://docs.sorsa.io/api-reference/users-data/account-about-info) | | `/followers` | GET | Paginated follower list with full profiles. Up to 200 users per page. | [Docs](https://docs.sorsa.io/api-reference/users-data/followers) | | `/follows` | GET | Paginated following list with full profiles. Up to 200 users per page. | [Docs](https://docs.sorsa.io/api-reference/users-data/following) | | `/verified-followers` | GET | Paginated list of only verified followers. | [Docs](https://docs.sorsa.io/api-reference/users-data/verified-followers) | **Related guides:** [Followers & Following](https://docs.sorsa.io/followers-and-following) | [Audience Geography](https://docs.sorsa.io/Audience-Geography) | [Competitor Analysis](https://docs.sorsa.io/Competitor-Analysis) *** ## Tweets Endpoints for retrieving tweet content, engagement metrics, replies, quotes, retweeters, long-form articles, and trending topics. | Endpoint | Method | Description | Docs | | :- | :- | :- | :- | | `/tweet-info` | POST | Full data for a single tweet (text, metrics, author profile). Body: `tweet_link`. | [Docs](https://docs.sorsa.io/api-reference/tweets/tweet-data) | | `/tweet-info-bulk` | POST | Full data for up to 100 tweets in one request. Body: `tweet_links[]`. | [Docs](https://docs.sorsa.io/api-reference/tweets/tweet-data-batch) | | `/user-tweets` | POST | Paginated timeline of a user's tweets. Body: `user_link`, `username`, or `user_id`. | [Docs](https://docs.sorsa.io/api-reference/tweets/user-tweets) | | `/comments` | POST | Replies under a specific tweet. Body: `tweet_link` (optional `order_by`: `Relevance`, `Recency`, `Likes`). | [Docs](https://docs.sorsa.io/api-reference/tweets/tweet-comments) | | `/quotes` | POST | Quote tweets of a specific tweet. Body: `tweet_link`. | [Docs](https://docs.sorsa.io/api-reference/tweets/quote-tweets) | | `/retweeters` | POST | Users who retweeted a tweet (returns user profiles, not tweets). Body: `tweet_link`. | [Docs](https://docs.sorsa.io/api-reference/tweets/retweeters-list) | | `/article` | POST | Full content of an X Article (long-form post). Body: `tweet_link`. | [Docs](https://docs.sorsa.io/api-reference/tweets/article-data) | | `/trends` | GET | Trending topics for a location. Query: `woeid` (Where On Earth IDentifier). | [Docs](https://docs.sorsa.io/api-reference/tweets/trends-list) | **Related guides:** [Search Tweets](https://docs.sorsa.io/search-tweets) | [Tweet Engagement](https://docs.sorsa.io/tweet-engagement) | [X Articles](https://docs.sorsa.io/x-articles) | [Historical Data](https://docs.sorsa.io/historical-data) *** ## Search Endpoints for searching tweets, mentions, and user profiles, and for looking up X Spaces. | Endpoint | Method | Description | Docs | | :- | :- | :- | :- | | `/search-tweets` | POST | Search tweets by keyword with full X operator support. Body: `query`, `order`, `next_cursor`. | [Docs](https://docs.sorsa.io/api-reference/search/search-tweets) | | `/mentions` | POST | Tweets mentioning a specific handle, with engagement and date filters. Body: `query`, `order`, `min_likes`, `min_retweets`, `min_replies`, `since_date`, `until_date`. | [Docs](https://docs.sorsa.io/api-reference/search/search-mentions) | | `/search-users` | POST | Search user profiles by keyword (matches bios, display names, handles). Body: `query`. | [Docs](https://docs.sorsa.io/api-reference/search/search-users) | | `/spaces` | GET | Full data for an X Space: metadata, creator profile, participants, settings, and stats. Query: `id` or `link`. | [Docs](https://docs.sorsa.io/api-reference/search/spaces-information) | **Related guides:** [Search Tweets](https://docs.sorsa.io/search-tweets) | [Track Mentions](https://docs.sorsa.io/search-mentions) | [Search Operators](https://docs.sorsa.io/search-operators) | [Target Audience Discovery](https://docs.sorsa.io/target-audiences-Discovery) *** ## Verification Endpoints for checking whether a user performed a specific action (follow, retweet, comment, quote, community membership). Designed for campaign and giveaway verification. | Endpoint | Method | Description | Docs | | :- | :- | :- | :- | | `/check-follow` | POST | Check whether `user_2` follows `user_1` (user\_1 = the followed account, user\_2 = the potential follower). Body: one identifier for each side (`username_1`/`user_id_1`/`user_link_1` and `username_2`/`user_id_2`/`user_link_2`). | [Docs](https://docs.sorsa.io/api-reference/verification/check-follow) | | `/check-comment` | GET | Did a user comment on a specific tweet? Returns the comment tweet if found. Query: `tweet_link` + `username`/`user_id`/`user_link`. | [Docs](https://docs.sorsa.io/api-reference/verification/check-comment) | | `/check-retweet` | POST | Did a user retweet a specific tweet? Scans up to 100 retweets per request. Body: `tweet_link` + user identifier. | [Docs](https://docs.sorsa.io/api-reference/verification/check-retweet) | | `/check-quoted` | POST | Did a user quote or retweet a specific tweet? Returns `status`: `quoted`, `retweet`, or `not_found`. Body: `tweet_link` + user identifier. | [Docs](https://docs.sorsa.io/api-reference/verification/check-quote-or-retweet) | | `/check-community-member` | POST | Is a user a member of a specific X Community? Body: `community_id` + user identifier. | [Docs](https://docs.sorsa.io/api-reference/verification/check-community-membership) | | `/check-shadowban` | GET | Visibility checks with `clean`, `banned`, or `unknown` results. Query: `username`. Inspect `checked_at` for cache age. | [Docs](https://docs.sorsa.io/api-reference/verification/check-shadowban) | **Related guide:** [Marketing Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification) *** ## Community Endpoints for accessing X Community data: member lists, tweet feeds, and in-community search. | Endpoint | Method | Description | Docs | | :- | :- | :- | :- | | `/community-tweets` | POST | Tweet feed from a community. Body: `community_id`, `order` (`popular` or `latest`). | [Docs](https://docs.sorsa.io/api-reference/community/community-tweets) | | `/community-search-tweets` | POST | Search tweets within a community by keyword. Body: `community_link`, `query`, `order`. | [Docs](https://docs.sorsa.io/api-reference/community/search-community-tweets) | | `/community-members` | POST | Member list of a community with profiles. Body: `community_link`. | [Docs](https://docs.sorsa.io/api-reference/community/community-members) | **Related guide:** [Lists & Communities](https://docs.sorsa.io/lists-and-communities) *** ## Lists Endpoints for accessing X List data: member profiles, list subscribers, and the combined tweet feed. | Endpoint | Method | Description | Docs | | :- | :- | :- | :- | | `/list-members` | GET | Profiles of accounts in a list. Query: `list_id`. | [Docs](https://docs.sorsa.io/api-reference/lists/list-members) | | `/list-followers` | GET | Profiles of users who follow a list. Query: `list_link`. | [Docs](https://docs.sorsa.io/api-reference/lists/list-followers) | | `/list-tweets` | GET | Recent tweets from all list members. Query: `list_id`. | [Docs](https://docs.sorsa.io/api-reference/lists/list-tweets) | **Related guides:** [Lists & Communities](https://docs.sorsa.io/lists-and-communities) | [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring) *** ## Sorsa Score (Crypto Analytics) Endpoints for crypto-specific analytics: influence scoring, follower categorization, and recent follow activity among crypto accounts in Sorsa's database. Accepts `username`, `user_id`, or `user_link`. | Endpoint | Method | Description | Docs | | :- | :- | :- | :- | | `/score` | GET | Current Sorsa Score (crypto influence metric). | [Docs](https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/sorsa-score) | | `/score-changes` | GET | Score delta over the last week and month. | [Docs](https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/sorsa-score-changes) | | `/followers-stats` | GET | Follower breakdown: influencers, projects, VCs. | [Docs](https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/follower-category-stats) | | `/top-followers` | GET | Top 20 followers ranked by Sorsa Score (each entry includes the follower's score). | [Docs](https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/top-20-followers-by-score) | | `/top-following` | GET | Top 20 followed accounts ranked by Score. | [Docs](https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/top-20-following-by-score) | | `/new-followers-7d` | GET | Crypto accounts that followed in the last 7 days. | [Docs](https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/new-followers-7-days) | | `/new-following-7d` | GET | Crypto accounts the user started following in the last 7 days. | [Docs](https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/new-following-7-days) | **Related guide:** [Sorsa Score & Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) *** ## Technical Utilities Endpoints for ID conversion and API key usage monitoring. | Endpoint | Method | Description | Docs | | :- | :- | :- | :- | | `/username-to-id/{user_handle}` | GET | Convert a username to a permanent User ID. | [Docs](https://docs.sorsa.io/api-reference/technical-endpoints/convert-username-to-user-id) | | `/id-to-username/{user_id}` | GET | Convert a User ID to the current username. | [Docs](https://docs.sorsa.io/api-reference/technical-endpoints/convert-user-id-to-username) | | `/link-to-id` | GET | Extract a User ID from a profile URL. Query: `link`. | [Docs](https://docs.sorsa.io/api-reference/technical-endpoints/convert-profile-link-to-user-id) | | `/key-usage-info` | GET | Current request usage, remaining quota, and balance expiration date. No parameters. | [Docs](https://docs.sorsa.io/api-reference/technical-endpoints/api-key-usage) | **Related guides:** [ID Conversion](https://docs.sorsa.io/ID-Conversion) | [Pricing](https://docs.sorsa.io/pricing) *** ## Data Models Two core objects appear across most endpoints. For full details, including every variant, field type, and edge case, see [Response Format](https://docs.sorsa.io/response-format). ### User Object Returned by `/info` and `/info-batch`, by the follower, following, and user-search endpoints, and embedded in every tweet response as the `user` field. Some crypto analytics endpoints add `followerDate`; general `/followers` and `/follows` responses use the standard User model. See [Response Format](https://docs.sorsa.io/response-format) for those variants. | Field | Type | Description | | :- | :- | :- | | `id` | string | Permanent User ID (Snowflake) | | `username` | string | Current handle | | `display_name` | string | Display name | | `description` | string | Bio text | | `location` | string | Self-reported location | | `created_at` | string | Account creation date (ISO 8601) | | `followers_count` | integer | Follower count | | `followings_count` | integer | Following count | | `favourites_count` | integer | Total likes given | | `tweets_count` | integer | Total tweets posted | | `media_count` | integer | Media posts count | | `verified` | boolean | Verification status | | `protected` | boolean | Protected (private) account | | `can_dm` | boolean | DMs open | | `possibly_sensitive` | boolean | Sensitive content flag | | `profile_image_url` | string | Avatar URL | | `profile_background_image_url` | string | Banner image URL | | `bio_urls` | string\[] | URLs extracted from bio | | `pinned_tweet_ids` | string\[] | IDs of pinned tweets | ### Tweet Object Returned by `/tweet-info`, `/search-tweets`, `/user-tweets`, `/comments`, `/quotes`, `/mentions`, `/list-tweets`, and community tweet endpoints. | Field | Type | Description | | :- | :- | :- | | `id` | string | Tweet ID (Snowflake) | | `full_text` | string | Complete tweet text | | `created_at` | string | Post timestamp (ISO 8601) | | `lang` | string | Language code | | `likes_count` | integer | Likes | | `retweet_count` | integer | Retweets | | `reply_count` | integer | Replies | | `quote_count` | integer | Quotes | | `bookmark_count` | integer | Bookmarks | | `view_count` | integer | Views | | `conversation_id_str` | string | Thread/conversation ID | | `in_reply_to_tweet_id` | string | Parent tweet ID (if reply) | | `in_reply_to_username` | string | Parent author (if reply) | | `is_reply` | boolean | Whether this is a reply | | `is_quote_status` | boolean | Whether this is a quote tweet | | `is_replies_limited` | boolean | Replies restricted by author | | `made_with_ai` | boolean | Labeled as made with AI | | `paid_partnership` | boolean | Paid partnership (branded content) | | `user` | User | Complete author profile | | `entities` | TweetEntity\[] | Attached media and link entities | | `quoted_status` | Tweet | Nested quoted tweet (if quote) | | `retweeted_status` | Tweet | Nested retweeted tweet (if RT) | ### TweetEntity Object | Field | Type | Description | | :- | :- | :- | | `type` | string | Entity type: `photo`, `video`, or `url` | | `link` | string | Direct link (t.co URL) | | `preview` | string | Preview/thumbnail image URL | *** ## Common Patterns **Authentication:** All requests require the `ApiKey` header. See [Authentication](https://docs.sorsa.io/authentication). **Pagination:** Paginated endpoints use cursor-based pagination. Batch lookups and crypto analytics lists such as `/top-followers` return their results without a cursor. Pass the `next_cursor` from the previous response to fetch the next page. When the cursor is absent or null, you have reached the end. See [Pagination](https://docs.sorsa.io/pagination). **Error format:** All endpoints return `{ "message": "..." }` on errors (`400`, `401`, `403`, `404`, `429`, `500`). See [Error Codes](https://docs.sorsa.io/error-codes). **Rate limit:** 20 requests per second on all standard plans. See [Rate Limits](https://docs.sorsa.io/rate-limits). **Input flexibility:** Most user-related endpoints accept three input formats: `username`, `user_id`, or `user_link` (profile URL). Provide exactly one. *** ## Next steps * [Quickstart](https://docs.sorsa.io/quickstart) - Make your first API call in under a minute * [Response Format](https://docs.sorsa.io/response-format) - Detailed field types, null handling, and object schemas * [Search Operators](https://docs.sorsa.io/search-operators) - Build advanced queries for `/search-tweets` * [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) - Reduce request counts with batching, deduplication, and caching # Community Information Source: https://docs.sorsa.io/api-reference/community/community-information https://api.sorsa.io/v3/swagger.json post /community-info Returns community metadata and the full creator User profile. Provide community_id or community_link; community_id takes precedence. created_at is Unix milliseconds. banner_url uses the default banner when no custom banner exists. # Community Members Source: https://docs.sorsa.io/api-reference/community/community-members https://api.sorsa.io/v3/swagger.json post /community-members Returns a paginated list of user profiles for members of the specified Twitter/X Community. Each profile includes display name, bio, follower stats, avatar, account creation date, and verification status. Provide community_id or community_link; community_id takes precedence. Both fields accept an ID or community URL. # Community Tweets Source: https://docs.sorsa.io/api-reference/community/community-tweets https://api.sorsa.io/v3/swagger.json post /community-tweets Returns a paginated feed of tweets published inside the specified Twitter/X Community. Up to 20 tweets per page. Each tweet includes text, creation date, engagement metrics, and author profile. Results can be sorted by `popular` or `latest` (default: `latest`). Provide community_id or community_link; community_id takes precedence. Both fields accept an ID or community URL. # Search Communities Source: https://docs.sorsa.io/api-reference/community/search-communities https://api.sorsa.io/v3/swagger.json post /search-communities Searches for Twitter/X communities by query. Returns id, name, banner_url and member_count for each result, plus next_cursor for pagination. banner_url uses the default banner when no custom banner exists. Empty results are returned as an empty communities array. # Search Community Tweets Source: https://docs.sorsa.io/api-reference/community/search-community-tweets https://api.sorsa.io/v3/swagger.json post /community-search-tweets Searches for tweets within a specific Twitter/X Community by keyword. Up to 20 matching tweets per page. Results can be sorted by `popular` or `latest`. Each tweet includes text, creation date, engagement metrics, and author profile. Provide community_id or community_link; community_id takes precedence. Both fields accept an ID or community URL. # List Followers Source: https://docs.sorsa.io/api-reference/lists/list-followers https://api.sorsa.io/v3/swagger.json get /list-followers Returns a paginated list of users who follow (are subscribed to) the specified Twitter/X List. Each entry contains the follower's profile data. # List Members Source: https://docs.sorsa.io/api-reference/lists/list-members https://api.sorsa.io/v3/swagger.json get /list-members Returns user profiles for all accounts included in the specified Twitter/X List. Each profile contains display name, bio, follower stats, avatar, and verification status. # List Tweets Source: https://docs.sorsa.io/api-reference/lists/list-tweets https://api.sorsa.io/v3/swagger.json get /list-tweets Returns a paginated feed of tweets published by members of the specified Twitter/X List. Up to 20 tweets per page. Each tweet includes text, creation date, engagement metrics, and basic author information. # Search Mentions Source: https://docs.sorsa.io/api-reference/search/search-mentions https://api.sorsa.io/v3/swagger.json post /mentions Returns tweets that mention the specified user handle. Up to 20 results per page, sorted by mention time (newest first by default). Supports the richest set of filters among all search endpoints: minimum likes, replies, retweets, and a date range. Results can also be sorted by `popular` or `latest`. # Search Tweets Source: https://docs.sorsa.io/api-reference/search/search-tweets https://api.sorsa.io/v3/swagger.json post /search-tweets Searches for tweets matching a text query using the same syntax as Twitter/X Advanced Search. Supports operators such as `from:`, `to:`, `since:`, `until:`, exact phrases in quotes, and hashtags. Returns up to 20 results per page, sortable by `popular` or `latest`. For a full list of supported query operators, see: https://github.com/igorbrigadir/twitter-advanced-search # Search Users Source: https://docs.sorsa.io/api-reference/search/search-users https://api.sorsa.io/v3/swagger.json post /search-users Searches for Twitter/X accounts by a keyword or phrase. Returns matching user profiles with basic info: handle, display name, bio, follower stats, and verification status. Supports pagination via `next_cursor`. # Spaces Information Source: https://docs.sorsa.io/api-reference/search/spaces-information https://api.sorsa.io/v3/swagger.json get /spaces Returns detailed information about a Twitter Space by its ID. Includes the Space's metadata (title, current state, scheduled start time), full creator profile, and participant breakdown by role: admins, speakers, and listeners. Also exposes access settings such as lock status, join restrictions, and replay or clipping availability, along with live stats like total listeners and replay views. # Follower Category Stats Source: https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/follower-category-stats https://api.sorsa.io/v3/swagger.json get /followers-stats Returns a breakdown of the account's followers by Sorsa category: influencers, projects, and VC (venture capital) employees. Also indicates whether the account is protected. If the account is already indexed in the Sorsa database, the response is fast. For new accounts, the initial data collection may take longer, especially for large follower bases. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # New Followers (7 Days) Source: https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/new-followers-7-days https://api.sorsa.io/v3/swagger.json get /new-followers-7d Returns accounts that started following the specified user in the last 7 days. Both the target account and the returned followers must be present in the Sorsa database, which primarily tracks crypto-related accounts (influencers, projects, VCs). This means the results do not represent all new followers, only those already tracked by our system. Each entry includes profile data and the exact follow date. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # New Following (7 Days) Source: https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/new-following-7-days https://api.sorsa.io/v3/swagger.json get /new-following-7d Returns accounts that the specified user started following in the last 7 days. Both the target account and the returned followings must be present in the Sorsa database, which primarily tracks crypto-related accounts (influencers, projects, VCs). This means the results do not represent all new follows, only those already tracked by our system. Each entry includes profile data and the exact follow date. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # Sorsa Score Source: https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/sorsa-score https://api.sorsa.io/v3/swagger.json get /score Returns the Sorsa Score for a Twitter/X account. The score is a numeric value that estimates the account's recognition and popularity among crypto influencers, projects, and venture capital firms. A higher score indicates stronger visibility within the ecosystem. For accounts with a large follower base, the response may take longer to generate. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # Sorsa Score Changes Source: https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/sorsa-score-changes https://api.sorsa.io/v3/swagger.json get /score-changes Returns the change in Sorsa Score over the last 7 days and the last 30 days. Useful for tracking momentum and identifying accounts that are gaining or losing influence. The account must already be indexed in the Sorsa database. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # Top 20 Followers by Score Source: https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/top-20-followers-by-score https://api.sorsa.io/v3/swagger.json get /top-followers Returns the 20 followers of the specified account with the highest Sorsa Score. Each entry includes the follower's profile data and follow date. Useful for identifying the most influential accounts in a user's audience. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # Top 20 Following by Score Source: https://docs.sorsa.io/api-reference/sorsa-info-crypto-related/top-20-following-by-score https://api.sorsa.io/v3/swagger.json get /top-following Returns the 20 accounts that the specified user follows with the highest Sorsa Score. Each entry includes profile data and follow date. Useful for understanding whose content and activity the user values most within the ecosystem. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # API Key Usage Source: https://docs.sorsa.io/api-reference/technical-endpoints/api-key-usage https://api.sorsa.io/v3/swagger.json get /key-usage-info Returns usage statistics for the current API key: total allocated requests, remaining balance, and the expiration date of the current request quota. No parameters required; the key is read from the `ApiKey` header. # Convert Profile Link to User ID Source: https://docs.sorsa.io/api-reference/technical-endpoints/convert-profile-link-to-user-id https://api.sorsa.io/v3/swagger.json get /link-to-id Extracts the stable numeric user ID from a Twitter/X profile URL. Useful for normalizing profile links into consistent identifiers before making further API calls. # Convert User ID to Username Source: https://docs.sorsa.io/api-reference/technical-endpoints/convert-user-id-to-username https://api.sorsa.io/v3/swagger.json get /id-to-username/{user_id} Converts a numeric Twitter/X user ID into the account's current username. Useful for resolving stored IDs back into human-readable handles, especially when usernames may have changed. # Convert Username to User ID Source: https://docs.sorsa.io/api-reference/technical-endpoints/convert-username-to-user-id https://api.sorsa.io/v3/swagger.json get /username-to-id/{user_handle} Converts a Twitter/X username into the corresponding stable numeric user ID. Unlike usernames, user IDs never change, making them reliable for long-term storage and cross-referencing. # Article Data Source: https://docs.sorsa.io/api-reference/tweets/article-data https://api.sorsa.io/v3/swagger.json post /article Returns full data for a Twitter/X Article (long-form post) by its URL. The response includes the complete article text, a short preview excerpt, cover image URL, publication date, engagement metrics, and the author's profile. # Quote Tweets Source: https://docs.sorsa.io/api-reference/tweets/quote-tweets https://api.sorsa.io/v3/swagger.json post /quotes Returns a paginated list of tweets that quoted the specified tweet. Up to 20 results per page. Each quote tweet includes the added commentary text, creation date, engagement metrics, and the quoting user's profile. # Retweeters List Source: https://docs.sorsa.io/api-reference/tweets/retweeters-list https://api.sorsa.io/v3/swagger.json post /retweeters Returns a paginated list of users who retweeted the specified tweet, sorted by retweet time (newest first). Each entry contains the retweeter's profile data. Note: the response uses the users format (`common.UsersResponse`), not tweets. # Trends List Source: https://docs.sorsa.io/api-reference/tweets/trends-list https://api.sorsa.io/v3/swagger.json get /trends Returns a list of trending topics for the specified WOEID (location identifier). Each trend contains its name and search query for the region. Useful for monitoring popular topics by location. # Tweet Comments Source: https://docs.sorsa.io/api-reference/tweets/tweet-comments https://api.sorsa.io/v3/swagger.json post /comments Returns a paginated list of replies (comments) posted under a specific tweet. Up to 20 replies per page. Each reply includes text, creation date, engagement metrics, conversation context, and the author's profile. # Tweet Data Source: https://docs.sorsa.io/api-reference/tweets/tweet-data https://api.sorsa.io/v3/swagger.json post /tweet-info Returns full data for a single tweet by its URL: text, creation date, language, engagement metrics (likes, retweets, quotes, replies, views, bookmarks), and the author's profile. If the tweet is a reply, quote, or repost, the nested original tweet and its author data are included as well. # Tweet Data (Batch) Source: https://docs.sorsa.io/api-reference/tweets/tweet-data-batch https://api.sorsa.io/v3/swagger.json post /tweet-info-bulk Returns full data for up to 100 tweets in a single request. Each tweet object includes text, creation date, engagement metrics, and author profile. Works the same way as the single-tweet endpoint, but accepts an array of URLs. The response is not paginated. # User Replies Source: https://docs.sorsa.io/api-reference/tweets/user-replies https://api.sorsa.io/v3/swagger.json post /user-replies Returns a paginated list of replies posted by a specific user. Up to 20 items per page. Each tweet includes text, creation date, engagement metrics, view count, and nested data for quotes and retweets when available. Provide either `link` (profile URL), `username` or `user_id`. # User Retweets Source: https://docs.sorsa.io/api-reference/tweets/user-retweets https://api.sorsa.io/v3/swagger.json post /user-retweets Returns a paginated list of retweets made by a specific user. Up to 20 items per page. The original post is returned in `retweeted_status`. Provide either `link` (profile URL), `username` or `user_id`. # User Tweets Source: https://docs.sorsa.io/api-reference/tweets/user-tweets https://api.sorsa.io/v3/swagger.json post /user-tweets Returns a paginated list of tweets posted by a specific user. Up to 20 tweets per page. `type` selects the timeline: `posts` (default) returns the user's posts (with replies when `with_replies` is true), `replies` returns the user's replies, `retweets` returns the user's retweets; `with_replies` is ignored for `replies` and `retweets`. Each tweet includes text, creation date, engagement metrics, view count, and nested data for replies, quotes, and retweets when available. Provide either `link` (profile URL) or `user_id`. # Account About Info Source: https://docs.sorsa.io/api-reference/users-data/account-about-info https://api.sorsa.io/v3/swagger.json get /about Returns metadata from the "About" section of a Twitter/X account, including country, total number of username changes, and the date of the most recent change. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # Followers Source: https://docs.sorsa.io/api-reference/users-data/followers https://api.sorsa.io/v3/swagger.json get /followers Returns a paginated list of users who follow the specified account. Up to 200 user profiles per page. Use the returned `next_cursor` to load subsequent pages. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # Following Source: https://docs.sorsa.io/api-reference/users-data/following https://api.sorsa.io/v3/swagger.json get /follows Returns a paginated list of accounts that the specified user follows. Up to 200 user profiles per page. Use the returned `next_cursor` to load subsequent pages. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # User Profile Source: https://docs.sorsa.io/api-reference/users-data/user-profile https://api.sorsa.io/v3/swagger.json get /info Returns the public profile of a Twitter/X account, including display name, bio, follower and following counts, tweet count, avatar, account creation date, and verification status. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # User Profile (Batch) Source: https://docs.sorsa.io/api-reference/users-data/user-profile-batch https://api.sorsa.io/v3/swagger.json get /info-batch Returns public profile data for multiple Twitter/X accounts in a single request. Works the same way as the single-user endpoint, but accepts arrays of usernames or user IDs. Up to 100 accounts per request. Provide either `usernames` or `user_ids`. # Verified Followers Source: https://docs.sorsa.io/api-reference/users-data/verified-followers https://api.sorsa.io/v3/swagger.json get /verified-followers Returns a paginated list of verified users who follow the specified account. Works the same way as the regular followers endpoint, but filters to only include accounts with a verified badge. Identify the user by providing exactly one of: `user_link`, `username`, or `user_id`. # Check Comment Source: https://docs.sorsa.io/api-reference/verification/check-comment https://api.sorsa.io/v3/swagger.json get /check-comment Checks whether a specific user has posted a reply under a given tweet. Returns `commented: true` along with the full reply tweet data if a comment is found. Requires `tweet_link` and one of `user_handle` or `user_id`. # Check Community Membership Source: https://docs.sorsa.io/api-reference/verification/check-community-membership https://api.sorsa.io/v3/swagger.json post /check-community-member Checks whether a specific user is a member of a given Twitter/X Community. Returns `is_member: true` if the user belongs to that community. Requires community_id or community_link and one user identifier (username, user_link, or user_id). community_id takes precedence; both community fields accept an ID or community URL. # Check Follow Source: https://docs.sorsa.io/api-reference/verification/check-follow https://api.sorsa.io/v3/swagger.json post /check-follow Checks if one account follows another. User 1 is the account that should be followed, User 2 is the one you want to check. For example: "Does Elon Musk (user_2) follow SorsaApp (user_1)?" Pass only one identifier per side: link, username, or ID. Returns follow: true if the relationship exists, and user_protected: true if the checked user's profile is private. # Check Quote or Retweet Source: https://docs.sorsa.io/api-reference/verification/check-quote-or-retweet https://api.sorsa.io/v3/swagger.json post /check-quoted Checks whether a user has quoted or retweeted a given tweet. Returns a `status` field with one of three values: `quoted`, `retweet`, or `not_found`. If a quote or retweet is found, the response also includes the interaction date and the quote text (when applicable). Identify the user with `username`, `user_link`, or `user_id`. # Check Retweet Source: https://docs.sorsa.io/api-reference/verification/check-retweet https://api.sorsa.io/v3/swagger.json post /check-retweet Checks whether a user has retweeted a given tweet. The endpoint scans up to 100 retweets per request. If the tweet has more retweets than that, a `next_cursor` is returned to continue checking. Identify the user with `username`, `user_link`, or `user_id`. # Check Shadowban Source: https://docs.sorsa.io/api-reference/verification/check-shadowban https://api.sorsa.io/v3/swagger.json get /check-shadowban Checks whether a Twitter/X account is restricted in visibility. Runs two independent measurements: `search_suggestion_ban` — whether the account appears in search suggestions when its exact handle is typed — and `search_ban` — whether its posts are returned by search. Each check reports `clean`, `banned` or `unknown`; `unknown` carries a `reason` and means the measurement could not be made, never that the account is restricted. `is_shadowbanned` is true only when at least one check found an actual restriction. Profile flags (`protected`, `possibly_sensitive`) are reported separately and never count as a shadowban. Results are cached for an hour — see `checked_at` for the age of the data. # Authentication Source: https://docs.sorsa.io/authentication Sorsa API uses API keys to authenticate requests. Your API key grants full access to your account and quota, so treat it like a password. *** ## How authentication works Every request to the Sorsa API must include your API key in the `ApiKey` header. HTTP header names are case-insensitive; use the spelling shown here and preserve the key value exactly. ```text theme={null} ApiKey: your_api_key_here ``` If the header is missing, misspelled, or contains an invalid key, the API returns an error (see [Troubleshooting](#troubleshooting) below). **Example request** ```bash theme={null} curl --request GET \ --url 'https://api.sorsa.io/v3/info?username=elonmusk' \ --header 'ApiKey: YOUR_API_KEY' ``` **For POST endpoints**, also include the `Content-Type` header: ```bash theme={null} curl --request POST \ --url 'https://api.sorsa.io/v3/search-tweets' \ --header 'ApiKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{"query": "bitcoin", "order": "popular"}' ``` > **Tip:** You can test your API key without writing any code using the [API Playground](https://api.sorsa.io/playground). *** ## Request requirements Every API call must meet the following requirements: **HTTPS only.** All requests must use `https://`. Plain HTTP requests will be rejected. **ApiKey header.** Required on every request. No OAuth, no bearer tokens, no query parameter authentication. **Content-Type header.** Required for POST requests. Set it to `application/json` and pass parameters as a JSON body. **HTTP methods.** Endpoints use either GET or POST depending on the operation. The [API Reference](https://docs.sorsa.io/api-reference-guide) specifies the method for each endpoint. *** ## Managing your API keys **Find your key.** Your active API key is displayed on the [Dashboard overview page](https://api.sorsa.io/overview). New accounts include 100 free requests, so your first key works right away with no card on file. **Create or delete keys.** To generate a new key or revoke an existing one, go to the [API Keys](https://api.sorsa.io/overview/keys) section in your dashboard. **Monitor usage.** You can track your request history and remaining quota on the [Usage stats](https://api.sorsa.io/overview/usage) page or programmatically via the `GET /key-usage-info` endpoint. > **Important:** When you delete or replace a key, any application still using the old key will immediately start receiving `401 Unauthorized` errors. Update your integrations before revoking a key. *** ## Security best practices **Never expose your key in client-side code.** Do not call the Sorsa API directly from browsers, mobile apps, or any frontend environment. Your API key would be visible in browser dev tools, network logs, and source code. Always route requests through your own backend server. **Use environment variables.** Store your key in `.env` files or your platform's secrets manager (AWS Secrets Manager, Vercel Environment Variables, Railway Variables, and similar). Never hardcode keys in source files. **Keep keys out of version control.** Add `.env` to your `.gitignore`. Never commit API keys to public or private repositories on GitHub, GitLab, or Bitbucket. **Rotate compromised keys immediately.** If you accidentally expose your key in a commit, a screenshot, or a public forum, go to the [API Keys](https://api.sorsa.io/overview/keys) section, delete the compromised key, and generate a new one. The old key stops working instantly. *** ## Troubleshooting **401 Unauthorized** The `ApiKey` header is missing, the header name is misspelled, or the key has been deleted or was never valid. Double-check that your header is exactly `ApiKey` (not `Api-Key` or `Authorization`). **403 Forbidden** Your key is valid, but your subscription has expired or your monthly request quota is exhausted. Check your remaining balance on the [Dashboard](https://api.sorsa.io/overview) or by calling `GET /key-usage-info`. **429 Too Many Requests** You are sending requests faster than the rate limit allows (a flat 20 requests per second on every plan). Add a short backoff between calls and retry. See [Rate Limits](https://docs.sorsa.io/rate-limits) for details and retry strategies. **CORS errors in the browser** If you see CORS-related errors, you are likely calling the API from frontend JavaScript. Sorsa API is designed for server-side use only. Move your API calls to a backend service or serverless function. *** ## Next steps * [Rate Limits](https://docs.sorsa.io/rate-limits) - Understand request quotas and retry strategies * [Key Usage Info](https://docs.sorsa.io/api-reference/technical-endpoints/api-key-usage) - Check your balance programmatically * [API Reference](https://docs.sorsa.io/api-reference-guide) - Explore all available endpoints # Base URL & Versioning Source: https://docs.sorsa.io/base-url-and-versioning # Base URL, Versioning, and Endpoint Structure Sorsa API uses URL-based versioning to keep your integrations stable. The version number is part of the base URL, so every request explicitly targets one specific version of the API. ## Base URL All requests are made over HTTPS. Plain HTTP is not supported and will be rejected. ```text theme={null} https://api.sorsa.io/v3 ``` Every endpoint path is relative to this base URL. For example, the full URL for fetching a user profile is: ```text theme={null} https://api.sorsa.io/v3/info?username=elonmusk ``` ## Current version: v3 `v3` is the current, stable, and recommended version of the Sorsa API. All new endpoints, response fields, and improvements ship under v3. ## How we version changes Not every update requires a new version number. Changes fall into two categories. ### Non-breaking changes (shipped within v3) These are added to the current version without a version bump. Your existing integrations keep working with no changes on your side: * Adding new endpoints * Adding new optional query parameters to existing endpoints * Adding new fields to JSON response objects * Improving error messages or adding more detail to error responses Because new response fields can be added at any time, parse responses defensively: read the fields you need and ignore any keys you do not recognize, rather than failing when an unexpected field appears. ### Breaking changes (new version number) If we ever need to remove response fields, rename parameters, change how authentication works, or alter core endpoint behavior, we release a new version (for example, `/v4`). When that happens: * The previous version stays online during a clearly communicated sunset period * A migration guide with field mapping and structural changes is published in advance * Deprecation timelines are announced through the dashboard and the documentation ## Legacy version: v2 (retired) v2 is deprecated and has been shut down. The scheduled shutdown date was **May 1, 2026**, and v2 endpoints now return `410 Gone`. For an existing Sorsa v2 integration, use the current [API Reference](https://docs.sorsa.io/api-reference-guide) to review v3 request and response schemas, and [contact support](https://docs.sorsa.io/support) for migration assistance. The separate official X API migration guide covers a different source API. ## Next steps * [Authentication](https://docs.sorsa.io/authentication) - How to authorize your v3 requests * [Support](https://docs.sorsa.io/support) - Assistance migrating an existing Sorsa v2 integration * [API Reference](https://docs.sorsa.io/api-reference-guide) - Browse all endpoints with parameters and response schemas # Error Codes Source: https://docs.sorsa.io/error-codes What each Sorsa API status code means, what causes it, and how to fix it, from 400 to 500. When a request fails, Sorsa returns a standard HTTP status code and a JSON body with a `message` field describing the problem. This page covers every code you may encounter, what causes it, and how to resolve it. ## Error response format All error responses share the same structure: ```json theme={null} { "message": "ApiKey required" } ``` The `message` field is a human-readable description of what went wrong. Check it first when debugging; it often points straight at the problem. ## Quick reference | Code | Type | Meaning | | :- | :- | :- | | 200 | Success | Request completed successfully | | 400 | Client error | Bad request: invalid or missing parameters | | 401 | Client error | Unauthorized: authentication failed | | 403 | Client error | Forbidden: valid key but no access or credits | | 404 | Client error | Not found: resource does not exist or is private | | 429 | Client error | Too many requests: rate limit exceeded | | 500 | Server error | Internal error: something went wrong on our side | ## 400 Bad Request The request could not be processed because of invalid or missing parameters. **Common causes** * A required parameter is missing (for example, `link`, `id`, `username`, or `query`) * A parameter has the wrong type or format (for example, a string where a number is expected) * The JSON body in a POST request is malformed or empty **How to fix:** Check the [API Reference](https://docs.sorsa.io/api-reference-guide) for the endpoint you are calling and confirm every required parameter is present and correctly formatted. For POST requests, set `Content-Type: application/json` and make sure the body is valid JSON. ## 401 Unauthorized Authentication failed. The API could not identify your account. **Common causes** * The `ApiKey` header is missing entirely * The header name is misspelled. Use `ApiKey`; HTTP header names are case-insensitive, but `Api-Key` and `api_key` are different names. * The key value is wrong, was copied with extra whitespace, or belongs to a deleted key **How to fix:** Confirm your request sends the header exactly as `ApiKey: your_key_here`, and check that the key is still active in your [Dashboard](https://api.sorsa.io/overview/keys). When in doubt, copy the key straight from the dashboard. See [Authentication](https://docs.sorsa.io/authentication) for more. ## 403 Forbidden Your API key is valid, but the request was rejected. **Common causes** * Your request quota is exhausted (0 requests remaining) * Your subscription has expired **How to fix:** Check your remaining balance with `GET /key-usage-info` or in your [Dashboard](https://api.sorsa.io/overview). If your requests are used up, top up or upgrade on the [Billing](https://api.sorsa.io/overview/billing) page. ## 404 Not Found The requested resource does not exist. **Common causes** * The X user changed their username, deleted their account, or was suspended * The tweet was deleted by its author * The account or tweet is private (protected). Sorsa can only access public data * The endpoint URL itself is incorrect **How to fix:** Confirm the user or tweet still exists and is publicly accessible on X. User IDs remain stable when a handle changes. If you have a stored user ID, use `/id-to-username/{user_id}` to resolve the current handle, or pass `user_id` directly to endpoints that accept it. ## 429 Too Many Requests You have exceeded the rate limit of 20 requests per second. **Common causes** * Sending requests in a tight loop with no delay * Running multiple parallel workers that share one API key **How to fix:** Add a small delay between requests (a 50ms pause paces a single sequential worker; shared keys need coordinated pacing) or add retry logic with a one-second pause. See [Rate Limits](https://docs.sorsa.io/rate-limits) for strategies and code examples. ## 500 Internal Server Error Something went wrong on our side. **What to do** * Retry after a short delay (1 to 2 seconds). Transient 500s often clear on their own. * If it persists across retries or affects multiple endpoints, check the [Status Page](https://uptime.sorsa.io/status/v3) for ongoing incidents. * If the problem continues, contact support at [contacts@sorsa.io](mailto:contacts@sorsa.io) or on [Discord](https://discord.com/invite/uwAefKCj7X) with the endpoint URL, request body, and approximate timestamp. Remove API keys and authorization headers from diagnostic material. ## Handling errors in code A resilient integration handles every error code gracefully instead of crashing on an unexpected response. Here is a reusable pattern for Python and JavaScript. **Python** ```python theme={null} import time import requests def sorsa_request(method, endpoint, api_key, params=None, json_body=None, max_attempts=3): """GET/POST data retrieval with bounded retries; max_attempts includes the first call.""" method = method.upper() if method not in {"GET", "POST"}: raise ValueError("Use GET or POST") if max_attempts < 1: raise ValueError("max_attempts must be positive") url = f"https://api.sorsa.io/v3{endpoint}" for attempt in range(max_attempts): try: response = requests.request( method, url, headers={"ApiKey": api_key}, params=params, json=json_body, timeout=30, ) except (requests.Timeout, requests.ConnectionError): if attempt + 1 == max_attempts: raise else: if response.ok: return response.json() retryable = response.status_code == 429 or response.status_code >= 500 if not retryable or attempt + 1 == max_attempts: try: message = response.json().get("message", response.reason) except (ValueError, AttributeError): message = response.reason raise requests.HTTPError( f"HTTP {response.status_code}: {message}", response=response, ) time.sleep(min(2 ** attempt, 8)) data = sorsa_request("GET", "/info", "YOUR_API_KEY", params={"username": "elonmusk"}) print(data["display_name"]) ``` **JavaScript** ```javascript theme={null} async function sorsaRequest(method, endpoint, apiKey, body = null, maxAttempts = 3) { method = method.toUpperCase(); if (!["GET", "POST"].includes(method)) throw new Error("Use GET or POST"); if (!Number.isInteger(maxAttempts) || maxAttempts < 1) { throw new Error("maxAttempts must be a positive integer"); } if (method === "GET" && body !== null) throw new Error("Use query parameters for GET"); const url = `https://api.sorsa.io/v3${endpoint}`; const options = { method, headers: { ApiKey: apiKey } }; if (body !== null) { options.headers["Content-Type"] = "application/json"; options.body = JSON.stringify(body); } for (let attempt = 0; attempt < maxAttempts; attempt++) { let response; try { response = await fetch(url, { ...options, signal: AbortSignal.timeout(30000) }); } catch (error) { if (attempt + 1 === maxAttempts) throw error; await new Promise((r) => setTimeout(r, Math.min(2 ** attempt, 8) * 1000)); continue; } if (response.ok) return await response.json(); const retryable = response.status === 429 || response.status >= 500; if (!retryable || attempt + 1 === maxAttempts) { let message = response.statusText; try { message = (await response.json()).message || message; } catch {} throw new Error(`HTTP ${response.status}: ${message}`); } await new Promise((r) => setTimeout(r, Math.min(2 ** attempt, 8) * 1000)); } } const data = await sorsaRequest("GET", "/info?username=elonmusk", "YOUR_API_KEY"); console.log(data.display_name); ``` These examples retry network timeouts, connection failures, `429`, and `5xx` responses, with a maximum of three attempts by default. Other HTTP errors fail immediately. A successful response with invalid JSON also fails so you can inspect it. Run JavaScript on Node.js 18+; install Python's `requests` package before use. ## Next steps * [Rate Limits](https://docs.sorsa.io/rate-limits) - Strategies for staying within the 20 req/s limit * [Pagination](https://docs.sorsa.io/pagination) - Fetch large datasets without errors * [API Reference](https://docs.sorsa.io/api-reference-guide) - Full endpoint list with parameter schemas # Followers & Following Source: https://docs.sorsa.io/followers-and-following # How to Get Twitter Followers and Following Lists via API The follower and following lists of any public X (formerly Twitter) account are some of the most valuable datasets on the platform. A follower list tells you who is interested in a brand, topic, or personality. A following list reveals who that account pays attention to: their influences, competitors, and information sources. Together, they map the social graph around any account. Sorsa API provides two endpoints for extracting this data: `/followers` (who follows an account) and `/follows` (who an account is following). Both return up to 200 full user profiles per request, with cursor-based pagination to walk through the complete list. Every user object includes bio, follower count, tweet count, location, verified status, profile image, and more. This guide covers both endpoints from the simplest possible request through production-scale extraction with filtering, audience overlap analysis, and pagination strategy. > **Free to start:** Every Sorsa endpoint, including `/followers`, `/follows`, and `/verified-followers`, is available on your first 100 requests: one-time, no credit card, no expiry. Since each request returns up to 200 profiles, that covers roughly the first 20,000 followers of any account before you need a paid plan. > **Note:** For a complete walkthrough with extra extraction methods and worked audience-analysis examples, see [Twitter Followers API: Get Followers and Following Lists](https://api.sorsa.io/blog/twitter-followers-api) on the blog. *** ## Simplest Example: Get Followers One request, one response. This is all you need to fetch the first page of followers for any public account. ### cURL ```bash theme={null} curl "https://api.sorsa.io/v3/followers?username=stripe" \ -H "ApiKey: YOUR_API_KEY" ``` ### Python ```python theme={null} import requests resp = requests.get( "https://api.sorsa.io/v3/followers", headers={"ApiKey": "YOUR_API_KEY"}, params={"username": "stripe"}, ) for user in resp.json().get("users", []): print(f"@{user['username']} - {user.get('description', '')[:80]}") ``` ### JavaScript ```javascript theme={null} const resp = await fetch( "https://api.sorsa.io/v3/followers?username=stripe", { headers: { "ApiKey": "YOUR_API_KEY" } } ); const { users } = await resp.json(); users.forEach((u) => console.log(`@${u.username} - ${u.description?.slice(0, 80) ?? ""}`) ); ``` A GET request with your API key and a username. The response is an object containing a `users` array of up to 200 profiles and a `next_cursor` for pagination. > **Tip:** You can preview followers for any account without code using the [Recent Followers](https://api.sorsa.io/playground/recent-followers) tool or the [API Playground](https://api.sorsa.io/playground). *** ## Simplest Example: Get Following (Subscriptions) The `/follows` endpoint works identically but returns the accounts that a user is following: ```bash theme={null} curl "https://api.sorsa.io/v3/follows?username=stripe" \ -H "ApiKey: YOUR_API_KEY" ``` ```python theme={null} resp = requests.get( "https://api.sorsa.io/v3/follows", headers={"ApiKey": "YOUR_API_KEY"}, params={"username": "stripe"}, ) for user in resp.json().get("users", []): print(f"@{user['username']} ({user['followers_count']} followers)") ``` *** ## Endpoint Reference Both endpoints are GET requests with the same input options. ### `GET /v3/followers` Returns users who **follow** the specified account. ### `GET /v3/follows` Returns accounts that the specified user **is following**. ### Input Parameters (query string) | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `username` | string | One of | The handle without `@`. Example: `stripe` | | `user_id` | string | these | The numeric user ID. Example: `44196397` | | `user_link` | string | three | Full profile URL. Example: `https://x.com/stripe` | | `next_cursor` | integer | No | Pagination cursor. Take the `next_cursor` value from the previous response and pass it back here to fetch the next page. | Provide exactly one of `username`, `user_id`, or `user_link`. ### Response ```json theme={null} { "users": [ { "id": "1234567890", "username": "developer_jane", "display_name": "Jane Chen", "description": "Full-stack developer. Building things with APIs.", "location": "San Francisco, CA", "profile_image_url": "https://pbs.twimg.com/profile_images/...", "profile_background_image_url": "https://pbs.twimg.com/profile_banners/...", "followers_count": 4820, "followings_count": 312, "tweets_count": 1847, "favourites_count": 5231, "media_count": 89, "verified": false, "protected": false, "can_dm": true, "possibly_sensitive": false, "created_at": "2018-01-15T08:22:41Z", "bio_urls": ["https://janechen.dev"], "pinned_tweet_ids": ["1987654321098765432"] } ], "next_cursor": 1234567890 } ``` Each user object includes: `id`, `username`, `display_name`, `description`, `location`, `created_at`, `followers_count`, `followings_count`, `favourites_count`, `tweets_count`, `media_count`, `profile_image_url`, `profile_background_image_url`, `bio_urls`, `pinned_tweet_ids`, `verified`, `can_dm`, `protected`, and `possibly_sensitive`. Each page returns up to **200 user objects**. When `next_cursor` is present in the response, more results are available: pass its value back as the `next_cursor` parameter in your next request. When it is absent or null, you have reached the end of the list. *** ## Paginating Through a Full Follower List A single request returns one page. To collect the complete follower list, loop through pages using `next_cursor` until it is absent. ### Python ```python theme={null} import requests import time API_KEY = "YOUR_API_KEY" def get_all_followers(username, max_pages=50): """Fetch the complete follower list of a public account.""" all_users = [] cursor = None for page in range(max_pages): params = {"username": username} if cursor: params["next_cursor"] = cursor resp = requests.get( "https://api.sorsa.io/v3/followers", headers={"ApiKey": API_KEY}, params=params, timeout=30, ) resp.raise_for_status() data = resp.json() users = data.get("users", []) all_users.extend(users) print(f"Page {page + 1}: {len(users)} followers (total: {len(all_users)})") cursor = data.get("next_cursor") if not cursor: print("Reached end of list.") break time.sleep(0.05) # stay under 20 req/s return all_users followers = get_all_followers("stripe", max_pages=100) print(f"\nTotal followers collected: {len(followers)}") ``` ### JavaScript ```javascript theme={null} const API_KEY = "YOUR_API_KEY"; async function getAllFollowers(username, maxPages = 50) { const allUsers = []; let cursor = null; for (let page = 0; page < maxPages; page++) { const params = new URLSearchParams({ username }); if (cursor) params.set("next_cursor", cursor); const resp = await fetch( `https://api.sorsa.io/v3/followers?${params}`, { headers: { "ApiKey": API_KEY } } ); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); allUsers.push(...(data.users || [])); console.log(`Page ${page + 1}: ${data.users?.length || 0} followers (total: ${allUsers.length})`); cursor = data.next_cursor; if (!cursor) break; await new Promise((r) => setTimeout(r, 50)); } return allUsers; } const followers = await getAllFollowers("stripe"); ``` The same pagination pattern works for `/follows`: just change the URL. For a deeper look at pagination behavior across all paginated endpoints, see [Pagination](https://docs.sorsa.io/pagination). *** ## Getting a Full Following List The code is identical with the endpoint swapped. Looking at who an account follows is often more revealing than looking at their followers: a founder's following list tells you which investors, partners, and competitors they pay attention to; an influencer's following list reveals their information sources. ```python theme={null} def get_all_following(username, max_pages=50): """Fetch the complete list of accounts a user follows.""" all_users = [] cursor = None for page in range(max_pages): params = {"username": username} if cursor: params["next_cursor"] = cursor resp = requests.get( "https://api.sorsa.io/v3/follows", headers={"ApiKey": API_KEY}, params=params, timeout=30, ) resp.raise_for_status() data = resp.json() users = data.get("users", []) all_users.extend(users) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.05) return all_users following = get_all_following("naval", max_pages=20) print(f"@naval follows {len(following)} accounts") # Sort by follower count to see the biggest names following.sort(key=lambda u: u.get("followers_count", 0), reverse=True) for u in following[:10]: print(f" @{u['username']} ({u['followers_count']:,} followers)") ``` *** ## Practical Applications ### Filtering Followers by Profile Criteria The raw list is useful, but filtering it makes it actionable. Since every user object includes full profile metadata, you can segment the audience by any attribute without additional API calls: ```python theme={null} followers = get_all_followers("competitor_handle", max_pages=20) # High-value accounts: 1K+ followers, active (100+ tweets), not protected qualified = [ u for u in followers if u.get("followers_count", 0) >= 1000 and u.get("tweets_count", 0) >= 100 and not u.get("protected", False) ] print(f"Qualified leads: {len(qualified)} out of {len(followers)} total") # Accounts with websites in their bio (potential business leads) with_websites = [u for u in followers if u.get("bio_urls")] print(f"Accounts with website links: {len(with_websites)}") # Filter by location keyword (self-reported) in_usa = [ u for u in followers if "usa" in (u.get("location") or "").lower() or "united states" in (u.get("location") or "").lower() or ", us" in (u.get("location") or "").lower() ] print(f"US-based followers: {len(in_usa)}") ``` The `location` field is freeform user-entered text. For more reliable country-level data, use the `/about` endpoint to look up each account's country tag. See [Audience Geography](https://docs.sorsa.io/Audience-Geography) for the full workflow. ### Finding Audience Overlap Between Competitors Pull follower lists from multiple competitors and find users who follow two or more of them. These are the most engaged people in your market: they have actively opted in to the topic multiple times. ```python theme={null} from collections import Counter competitors = ["competitor1", "competitor2", "competitor3"] all_ids = [] for handle in competitors: followers = get_all_followers(handle, max_pages=10) ids = [u["id"] for u in followers] all_ids.extend(ids) print(f"@{handle}: {len(followers)} followers collected") # Count how many competitor lists each user appears in counts = Counter(all_ids) overlap = {uid: count for uid, count in counts.items() if count >= 2} print(f"\nUsers following 2+ competitors: {len(overlap)}") ``` For a complete workflow combining follower extraction with bio search and community mining, see [Target Audience Discovery](https://docs.sorsa.io/target-audiences-Discovery). ### Discovering What Industry Leaders Follow Retrieve the following list of an expert or thought leader to find who they consider worth paying attention to. This surfaces niche accounts, emerging voices, and tools that leaders rely on. ```python theme={null} following = get_all_following("pmarca", max_pages=10) print(f"@pmarca follows {len(following)} accounts. Top by follower count:") following.sort(key=lambda u: u.get("followers_count", 0), reverse=True) for u in following[:15]: print(f" @{u['username']} ({u['followers_count']:,} followers)") print(f" {u.get('description', '')[:70]}\n") ``` *** ## Verified Followers The `/verified-followers` endpoint works the same as `/followers` but returns only verified accounts (Blue, Gold, or Gray checkmarks). This is useful for two cases: 1. **Filtering an audience to high-profile accounts** without post-processing the full list. 2. **Avoiding wasted requests on large accounts** where verified users are a small minority. Walking the entire follower list of a 10M-follower account just to find the 5,000 verified ones spends 50,000 requests; `/verified-followers` returns the same data in roughly 25 requests. ```bash theme={null} curl "https://api.sorsa.io/v3/verified-followers?username=stripe" \ -H "ApiKey: YOUR_API_KEY" ``` The response structure and pagination are identical to `/followers`: iterate with `next_cursor` in exactly the same way. See the [API Reference](https://docs.sorsa.io/api-reference/users-data/verified-followers) for full details. *** ## Estimating API Usage at Scale Each page returns up to 200 user objects. For planning: | Account Size | Pages Needed | Requests | | :- | :- | :- | | 1,000 followers | 5 | 5 | | 10,000 followers | 50 | 50 | | 100,000 followers | 500 | 500 | | 1,000,000 followers | 5,000 | 5,000 | At 20 requests per second, 50 and 500 requests have theoretical rate-limit lower bounds of 2.5 and 25 seconds. A single cursor chain is sequential: each page depends on the previous response, so actual duration also includes response latency, pacing, and retries. For accounts with millions of followers, consider sampling (for example, the first 50 pages, roughly 10,000 followers) rather than a full extraction, unless you specifically need complete coverage. Because each request returns up to 200 profiles, follower extraction is request-cheap. To put the request counts above in context: the free 100 requests cover about 20,000 followers, the Starter plan (10,000 requests per month) covers about 2,000,000, and Pro (100,000 requests per month) covers about 20,000,000. For full pricing, see [Pricing](https://api.sorsa.io/pricing). *** ## Data Freshness and Edge Cases **Follower ordering.** The `/followers` endpoint returns followers in the order X provides them, which is generally reverse-chronological (newest followers first). The first pages contain the most recently acquired followers. **Protected accounts.** If the target account is protected (private), the follower and following lists are not accessible. The endpoint returns an error. **Follower count vs. extracted list.** The `followers_count` value on a profile is a real-time counter maintained by X. The extractable list may differ slightly due to suspended, deactivated, or recently removed accounts. Expect a few percent of drift on large accounts; do not write strict equality checks against `followers_count`. This is a platform-level behavior, not Sorsa-specific. **Profile data is current.** Each user object reflects the profile as it exists at the time of the request (current bio, current follower count, current username), not the state at the moment the follow relationship was established. The numeric `id` is stable; the handle is not. **Sampling for very large accounts.** For accounts over about 500,000 followers, the first 50 to 100 pages (up to 10,000 to 20,000 followers) can be useful for studying recent followers. This is an ordered sample, not a random or representative sample of the entire audience. Full extraction is rarely necessary unless you have a specific reason to need complete coverage. *** ## Next Steps * [Target Audience Discovery](https://docs.sorsa.io/target-audiences-Discovery): combine follower extraction with bio search, community scraping, and content mining. * [Competitor Analysis](https://docs.sorsa.io/Competitor-Analysis): use follower and following data as part of a competitive intelligence pipeline. * [Audience Geography](https://docs.sorsa.io/Audience-Geography): map follower distribution by country using the `/about` endpoint. * [Pagination](https://docs.sorsa.io/pagination): general pagination patterns for large-scale extraction. * [API Reference](https://docs.sorsa.io/api-reference-guide): full specification for `/followers`, `/follows`, `/verified-followers`, and all Sorsa API endpoints. # Historical Data Source: https://docs.sorsa.io/historical-data Sorsa API provides full-archive access to public X (formerly Twitter) data back to March 2006. Historical retrieval uses the same endpoints, authentication, and pagination as recent data. There is no separate "full-archive" tier, no enterprise contract, and no time-window restriction on search. Archive queries also draw from the same request pool as any other call, so you can test them on the 100 free requests every new account receives, no card required. This page covers the two endpoints used for historical work, what the platform exposes versus what it doesn't, and the patterns that hold up at scale. > **Note:** For a fuller walkthrough with a method comparison table, a CSV export pipeline, and additional code examples, see [Historical Twitter Data: How to Search Old Tweets via API](https://api.sorsa.io/blog/historical-twitter-data) on the blog. *** ## Endpoints | Endpoint | Use case | Pagination | Page size | | :- | :- | :- | :- | | `POST /v3/search-tweets` | Keyword-based archive search with date and engagement filters | `next_cursor` | \~20 tweets | | `POST /v3/user-tweets` | A specific account's complete posting history | `next_cursor` | \~20 tweets | `/search-tweets` accepts the full [X search operator set](https://docs.sorsa.io/search-operators), including `since:`, `until:`, `from:`, `to:`, `min_faves:`, `min_retweets:`, `lang:`, and `filter:` directives, passed inline in the `query` field. `/user-tweets` takes only an account identifier (`user_link`, `username`, or `user_id`) and returns that account's full timeline, with no query filtering. *** ## Keyword Archive Search Use `/search-tweets` when you need every tweet matching a query within a date window, across all users. Pass the date-bounded query in the JSON body: ```python theme={null} import requests, time API_KEY = "YOUR_API_KEY" URL = "https://api.sorsa.io/v3/search-tweets" def search_archive(query, max_pages=50): all_tweets, next_cursor = [], None for _ in range(max_pages): body = {"query": query, "order": "latest"} if next_cursor: body["next_cursor"] = next_cursor resp = requests.post( URL, headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json=body, ) resp.raise_for_status() data = resp.json() all_tweets.extend(data.get("tweets", [])) next_cursor = data.get("next_cursor") if not next_cursor: break time.sleep(0.1) return all_tweets tweets = search_archive('"climate change" since:2015-06-01 until:2015-12-31 lang:en min_faves:10') ``` `order` accepts `"latest"` (chronological) or `"popular"` (engagement-ranked). For date-bounded archive collection use `"latest"`; for content research, `"popular"` surfaces the highest-engagement posts first. *** ## Full Account Timeline Use `/user-tweets` when you want the complete posting history of one account, from newest to oldest, without a 3,200-tweet cap. ```python theme={null} resp = requests.post( "https://api.sorsa.io/v3/user-tweets", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json={"user_link": "https://x.com/naval"}, ) ``` Provide exactly one identifier: `user_link`, `username`, or `user_id`. Paginate with `next_cursor` until it returns null; the endpoint walks the timeline in reverse chronological order. To pull one account's tweets within a specific date range, use `/search-tweets` with the `from:` operator instead (for example, `from:naval since:2020-01-01 until:2021-01-01`). `/user-tweets` does not accept date filters. *** ## What You Can Retrieve Every historical tweet returns with the same field set as a recent one: * Full text (no truncation, no URL replacement) * All six engagement metrics: `likes_count`, `retweet_count`, `reply_count`, `quote_count`, `view_count`, `bookmark_count` * Embedded `user` object with the author's full profile * `entities` array with media URLs (photos, videos, GIFs) and link previews * Conversation metadata: `conversation_id_str`, `in_reply_to_tweet_id`, `is_reply`, `is_quote_status` * Language tag (`lang`) See [Response Format](https://docs.sorsa.io/response-format) for the complete field reference. *** ## Platform-Level Limits These are X-side restrictions, not Sorsa-specific. No public API can work around them. * **Deleted tweets** are removed from X's search index and cannot be retrieved. * **Protected accounts** are excluded from all public search and timeline results. * **Profile snapshots are not historical.** A tweet from 2014 returns the author's current bio, username, and follower count, not the 2014 values. * **Engagement metrics are not snapshots.** Like, retweet, and view counts reflect current totals, not the counts as they stood on a specific past date. If you need point-in-time engagement, ingest tweets in real time via [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring) and store the metrics yourself. *** ## Best Practices ### Chunk Large Date Ranges A single query across a multi-year window has no clean retry path and no per-period auditability. Split by month for year-scale collections, by week for volatile event windows. ```python theme={null} def monthly_chunks(year): out = [] for month in range(1, 13): since = f"{year}-{month:02d}-01" nm = month + 1 if month < 12 else 1 ny = year if month < 12 else year + 1 until = f"{ny}-{nm:02d}-01" out.append((since, until)) return out for since, until in monthly_chunks(2020): tweets = search_archive(f'bitcoin since:{since} until:{until} lang:en min_faves:50') ``` ### Filter Retweet Noise Historical popular searches return waves of native retweets that bury original content. Add `-filter:nativeretweets` for sentiment, opinion, or content-pattern research. Use `-filter:retweets` to also exclude legacy `RT @user:` retweets. ### Pair Engagement and Date Filters Combining `since:` / `until:` with `min_faves:` or `min_retweets:` cuts noise and request volume sharply. Example: ```text theme={null} "product launch" since:2022-03-01 until:2022-03-31 min_faves:100 -filter:retweets lang:en ``` ### Split Global Topics by Language For worldwide events, separate queries per `lang:` give cleaner per-locale datasets than mixing languages. ### Paginate Until the Cursor Is Empty Terminate only when `next_cursor` is null, empty, or absent. Don't stop early on small page sizes. Full pattern in [Pagination](https://docs.sorsa.io/pagination). *** ## Related * [Search Tweets](https://docs.sorsa.io/search-tweets): endpoint reference for `/search-tweets` * [Search Operators](https://docs.sorsa.io/search-operators): full operator dictionary * [Pagination](https://docs.sorsa.io/pagination): cursor-based pagination details * [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring): pair with historical backfill for forward-looking ingestion * [Track Mentions](https://docs.sorsa.io/search-mentions): historical mention tracking for any handle * [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage): reduce request count on large collections # Lists & Communities Source: https://docs.sorsa.io/lists-and-communities Use the Sorsa API to retrieve members, followers, and tweet feeds from public X Lists. This page also describes the Community request formats listed in the API Reference; see the availability note before using them in a new workflow. > **Note:** For strategic context, cost math, and end-to-end monitoring workflows, see the [X List API guide](https://api.sorsa.io/blog/x-lists-and-communities-api) on the blog. > **Community availability:** The API Reference lists Community endpoints, but their current data availability needs confirmation. Before building a new Community workflow, [contact support](https://docs.sorsa.io/support) to confirm which operations and results are available. The request examples below document the interface; they are not a live availability test. *** ## Lists An X List is a public collection of up to 5,000 accounts. Lists have two audiences: * **Members:** accounts added to the list by the curator. * **Followers (subscribers):** users who subscribed to read the List timeline. Private Lists are not API-accessible. | Endpoint | Method | Returns | Page size | | :- | :- | :- | :- | | `/v3/list-members` | GET | User profiles of accounts in the list | up to 200 | | `/v3/list-followers` | GET | User profiles of accounts following the list | up to 200 | | `/v3/list-tweets` | GET | Combined chronological feed from list members | \~20 | The List ID is the numeric value in the list URL: `https://x.com/i/lists/1234567890` means the ID is `1234567890`. > **Tip:** Every account starts with 100 free requests (no card, no expiry), enough to pull a mid-sized List end to end. Test any endpoint without writing code in the [API Playground](https://api.sorsa.io/playground). ### Get List Members `GET /v3/list-members` | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `list_id` | string | Yes | Numeric List ID. | | `next_cursor` | string | No | Pagination cursor from a previous response. | ```bash theme={null} curl "https://api.sorsa.io/v3/list-members?list_id=1234567890" \ -H "ApiKey: YOUR_API_KEY" ``` ```python theme={null} import requests API_KEY = "YOUR_API_KEY" BASE = "https://api.sorsa.io/v3" HEADERS = {"ApiKey": API_KEY} def get_list_members(list_id, max_pages=50): members, cursor = [], None for _ in range(max_pages): params = {"list_id": list_id} if cursor: params["next_cursor"] = cursor r = requests.get(f"{BASE}/list-members", headers=HEADERS, params=params, timeout=30) r.raise_for_status() data = r.json() members.extend(data.get("users", [])) cursor = data.get("next_cursor") if not cursor: break return members members = get_list_members("1234567890") for u in members[:5]: print(f"@{u['username']} ({u['followers_count']:,} followers)") ``` A full 5,000-member List takes \~25 requests to extract. ### Get List Followers `GET /v3/list-followers` | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `list_link` | string | Yes | List URL or numeric ID. | | `next_cursor` | string | No | Pagination cursor. | Note the parameter name: `/list-followers` takes `list_link` (URL or ID), while `/list-members` and `/list-tweets` take `list_id` (numeric ID only). ```python theme={null} def get_list_followers(list_link, max_pages=50): followers, cursor = [], None for _ in range(max_pages): params = {"list_link": list_link} if cursor: params["next_cursor"] = cursor r = requests.get(f"{BASE}/list-followers", headers=HEADERS, params=params, timeout=30) r.raise_for_status() data = r.json() followers.extend(data.get("users", [])) cursor = data.get("next_cursor") if not cursor: break return followers subs = get_list_followers("https://x.com/i/lists/1234567890") print(f"{len(subs)} subscribers") ``` ### Get List Tweets `GET /v3/list-tweets` Returns recent tweets from all list members in a single chronological feed. This is the endpoint used in the [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring) workflow for tracking groups of accounts with a single request instead of polling each account separately. | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `list_id` | string | Yes | Numeric List ID. | | `next_cursor` | string | No | Pagination cursor. | ```python theme={null} def get_list_tweets(list_id, max_pages=10): tweets, cursor = [], None for _ in range(max_pages): params = {"list_id": list_id} if cursor: params["next_cursor"] = cursor r = requests.get(f"{BASE}/list-tweets", headers=HEADERS, params=params, timeout=30) r.raise_for_status() data = r.json() tweets.extend(data.get("tweets", [])) cursor = data.get("next_cursor") if not cursor: break return tweets feed = get_list_tweets("1234567890", max_pages=10) for t in feed[:5]: print(f"@{t['user']['username']}: {t['full_text'][:80]}") ``` *** ## Communities The following request formats describe the Community endpoints in the API Reference. Confirm availability as noted above before relying on them. Community membership can be an audience-discovery signal when member data is available. It does not by itself establish recent activity. | Endpoint | Method | Returns | Page size | | :- | :- | :- | :- | | `/v3/community-members` | POST | Community member profiles | \~20 | | `/v3/community-tweets` | POST | Tweets posted inside the community | \~20 | | `/v3/community-search-tweets` | POST | Keyword search inside the community | \~20 | The Community ID is the numeric value in the community URL: `https://x.com/i/communities/1966045657589813686` means the ID is `1966045657589813686`. Private Communities were not API-accessible. ### Get Community Members `POST /v3/community-members` | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `community_link` | string | Yes | Community ID or full URL. | | `next_cursor` | string | No | Pagination cursor. | Returns compact member profiles (id, username, display name, avatar, verified, and protected status). ```python theme={null} def get_community_members(community_link, max_pages=20): members, cursor = [], None for _ in range(max_pages): body = {"community_link": community_link} if cursor: body["next_cursor"] = cursor r = requests.post( f"{BASE}/community-members", headers={**HEADERS, "Content-Type": "application/json"}, json=body, timeout=30, ) r.raise_for_status() data = r.json() members.extend(data.get("users", [])) cursor = data.get("next_cursor") if not cursor: break return members ``` ### Get Community Tweets `POST /v3/community-tweets` | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `community_id` | string | Yes | Numeric Community ID. | | `order` | string | No | `"latest"` (default) or `"popular"`. | | `next_cursor` | string | No | Pagination cursor. | ```python theme={null} def get_community_tweets(community_id, order="latest", max_pages=10): tweets, cursor = [], None for _ in range(max_pages): body = {"community_id": community_id, "order": order} if cursor: body["next_cursor"] = cursor r = requests.post( f"{BASE}/community-tweets", headers={**HEADERS, "Content-Type": "application/json"}, json=body, timeout=30, ) r.raise_for_status() data = r.json() tweets.extend(data.get("tweets", [])) cursor = data.get("next_cursor") if not cursor: break return tweets ``` ### Search Community Tweets `POST /v3/community-search-tweets` | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `community_link` | string | Yes | Community ID or full URL. | | `query` | string | No | Search keyword. Omit to return the full community feed. | | `order` | string | No | `"popular"` or `"latest"`. | | `next_cursor` | string | No | Pagination cursor. | ```python theme={null} def search_community_tweets(community_link, query, order="popular", max_pages=5): tweets, cursor = [], None for _ in range(max_pages): body = {"community_link": community_link, "query": query, "order": order} if cursor: body["next_cursor"] = cursor r = requests.post( f"{BASE}/community-search-tweets", headers={**HEADERS, "Content-Type": "application/json"}, json=body, timeout=30, ) r.raise_for_status() data = r.json() tweets.extend(data.get("tweets", [])) cursor = data.get("next_cursor") if not cursor: break return tweets ``` For one-off membership checks (without paginating the full member list), use the dedicated [`/check-community-member`](https://docs.sorsa.io/api-reference/verification/check-community-membership) endpoint instead. *** ## Exporting to CSV User and tweet feeds from the Lists endpoints above can be exported with a single helper. For users: ```python theme={null} import csv def export_users_to_csv(users, path): fields = ["id", "username", "display_name", "description", "followers_count", "tweets_count", "verified", "location"] with open(path, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fields) writer.writeheader() for u in users: writer.writerow({ "id": u.get("id", ""), "username": u.get("username", ""), "display_name": u.get("display_name", ""), "description": (u.get("description") or "").replace("\n", " "), "followers_count": u.get("followers_count", 0), "tweets_count": u.get("tweets_count", 0), "verified": u.get("verified", False), "location": u.get("location", ""), }) export_users_to_csv(get_list_members("1234567890"), "members.csv") ``` `/list-members` and `/list-followers` return profiles in `users`; `/list-tweets` returns tweet objects in `tweets`, with the author under `user`. Optional profile fields can be empty. When exporting tweets, flatten nested values explicitly, for example `{"username": tweet["user"]["username"]}`; a CSV writer does not resolve a dotted field name such as `user.username`. *** ## Related * [X List API guide](https://api.sorsa.io/blog/x-lists-and-communities-api): strategy, cost math, use cases * [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring): polling patterns for `/list-tweets` * [Target Audience Discovery](https://docs.sorsa.io/target-audiences-Discovery): using lists for audience research * [Marketing Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification): membership checks * [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage): request batching and rate-limit handling # Migration from Official X API Source: https://docs.sorsa.io/migration-from-official-x-api # Migrating from the Official X API v2 to Sorsa API v3 This page is a reference for moving an existing integration from the official Twitter/X API v2 to Sorsa API v3. It covers authentication, endpoint mapping, response format changes, pagination, HTTP method differences, search query syntax, error handling, and code examples in `curl`, Python, and JavaScript. Sorsa API is read-only. If your integration also writes to X (posts tweets, sends DMs, likes, follows), keep the official API key for the write path and migrate only the read path. Every new account includes 100 free requests (no card required), so you can validate the mapped endpoints before switching production traffic over. > **Note:** For a narrative migration walkthrough with cost comparisons and step-by-step examples, see [Migrating from the Twitter/X API: Complete Developer Guide](https://api.sorsa.io/blog/migrate-from-twitter-api) on the blog. ## Overview of changes | Concern | Official X API v2 | Sorsa API v3 | | - | - | - | | Base URL | `https://api.x.com/2` | `https://api.sorsa.io/v3` | | Authentication | OAuth 2.0 Bearer / OAuth 1.0a | API key in `ApiKey` header | | Field selection | `tweet.fields`, `user.fields`, `expansions` | All fields returned by default | | Response envelope | `data` + `includes` + `meta` | Flat object, embedded relations | | Pagination | `pagination_token` / `meta.next_token` | `next_cursor` (top-level) | | Rate limits | Per-endpoint, per-15-min windows | Universal 20 req/s | | Error shape | `errors[]` with `type`, `title`, `detail` | `{ "message": "..." }` | ## Authentication The official API uses OAuth 2.0 Bearer tokens for app-only requests, and OAuth 1.0a User Context for user-scoped requests. ```bash theme={null} # Official API (OAuth 2.0 App-Only) curl "https://api.x.com/2/users/by/username/elonmusk" \ -H "Authorization: Bearer $BEARER_TOKEN" ``` Sorsa API uses a single API key passed in the `ApiKey` header. Generate keys in the [dashboard](https://api.sorsa.io/overview/keys). ```bash theme={null} curl "https://api.sorsa.io/v3/info?username=elonmusk" \ -H "ApiKey: $API_KEY" ``` ```python theme={null} import requests response = requests.get( "https://api.sorsa.io/v3/info", params={"username": "elonmusk"}, headers={"ApiKey": API_KEY}, ) user = response.json() ``` ```javascript theme={null} const response = await fetch("https://api.sorsa.io/v3/info?username=elonmusk", { headers: { ApiKey: API_KEY }, }); const user = await response.json(); ``` See [Authentication](https://docs.sorsa.io/authentication) for full details. ## Endpoint mapping ### Users | Action | Official X API v2 | Sorsa API v3 | | - | - | - | | Get user by username | `GET /2/users/by/username/:username` | `GET /info?username=:username` | | Get user by ID | `GET /2/users/:id` | `GET /info?user_id=:id` | | Get multiple users | `GET /2/users?ids=...` | `GET /info-batch?user_ids=...&user_ids=...` | | Get followers | `GET /2/users/:id/followers` | `GET /followers?user_id=:id` | | Get following | `GET /2/users/:id/following` | `GET /follows?user_id=:id` | | Verified followers | Not available | `GET /verified-followers?user_id=:id` | | Account "About" metadata | Not available | `GET /about?username=:username` | * `GET /info-batch` accepts up to 100 usernames or IDs per request. Repeat the query parameter: `?usernames=a&usernames=b`. * `GET /followers` and `GET /follows` return up to **200 fully-hydrated profiles per page**, including bio, follower counts, and verification status. ### Tweets | Action | Official X API v2 | Sorsa API v3 | | - | - | - | | Get a single tweet | `GET /2/tweets/:id` | `POST /tweet-info` body: `{ "tweet_link": ":id" }` | | Get multiple tweets | `GET /2/tweets?ids=...` | `POST /tweet-info-bulk` body: `{ "tweet_links": [...] }` | | User timeline | `GET /2/users/:id/tweets` | `POST /user-tweets` body: `{ "user_id": ":id" }` | | Quote tweets | `GET /2/tweets/:id/quote_tweets` | `POST /quotes` body: `{ "tweet_link": ":id" }` | | Retweeters | `GET /2/tweets/:id/retweeted_by` | `POST /retweeters` body: `{ "tweet_link": ":id" }` | | Replies (comments) | Not a dedicated endpoint | `POST /comments` body: `{ "tweet_link": ":id" }` | | Long-form X Article | Not available | `POST /article` body: `{ "tweet_link": ":id" }` | * `tweet_link` accepts either a full tweet URL (`https://x.com/user/status/123`) or just the numeric ID (`"123"`). * `POST /tweet-info-bulk` returns up to 100 tweets per request. Use it instead of looping `POST /tweet-info` to reduce request count by up to 100x. * `POST /user-tweets` has no 3,200-tweet ceiling. Paginate until `next_cursor` is absent to retrieve the full timeline back to the account's first tweet. See [Historical data](https://docs.sorsa.io/historical-data). ### Search | Action | Official X API v2 | Sorsa API v3 | | - | - | - | | Search recent tweets | `GET /2/tweets/search/recent?query=...` | `POST /search-tweets` body: `{ "query": "..." }` | | Search full archive | `GET /2/tweets/search/all?query=...` | `POST /search-tweets` (historical included) | | Search mentions | `GET .../search/recent?query=@user` | `POST /mentions` body: `{ "query": "user" }` | | Search users | Not available in v2 | `POST /search-users` body: `{ "query": "..." }` | * Sorsa `/search-tweets` uses X web Advanced Search syntax. Many basic terms transfer, but review API-v2-specific operators before reusing a query. See [Search operators](https://docs.sorsa.io/search-operators). * `POST /mentions` adds filters not available on the official API: `min_likes`, `min_replies`, `min_retweets`, `since_date`, `until_date`. ### Lists | Action | Official X API v2 | Sorsa API v3 | | - | - | - | | List members | `GET /2/lists/:id/members` | `GET /list-members?list_id=:id` | | List followers | `GET /2/lists/:id/followers` | `GET /list-followers?list_link=:id` | | List tweets | `GET /2/lists/:id/tweets` | `GET /list-tweets?list_id=:id` | ### Communities The official X API does not expose Community endpoints. Sorsa-only. | Action | Sorsa API v3 | | - | - | | Community members | `POST /community-members` body: `{ "community_link": ":id" }` | | Community feed | `POST /community-tweets` body: `{ "community_id": ":id", "order": "popular" }` | | Search within a community | `POST /community-search-tweets` body: `{ "community_link": ":id", "query": "..." }` | See [Lists and Communities](https://docs.sorsa.io/lists-and-communities) for request details and the current availability note; confirm support before migrating a Community workflow. ### Verification These check membership-style questions in a single call. The official API has no equivalent; replicating them requires fetching full lists and scanning client-side. | Question | Sorsa API v3 | | - | - | | Does user A follow user B? | `POST /check-follow` | | Did user X comment on tweet Y? | `GET /check-comment?tweet_link=...&username=...` | | Did user X quote or retweet tweet Y? | `POST /check-quoted` | | Did user X retweet tweet Y? | `POST /check-retweet` | | Is user X in community Y? | `POST /check-community-member` | See [Marketing campaign verification](https://docs.sorsa.io/Marketing-Campaign-Verification). ### Analytics (Sorsa-only) | Action | Sorsa API v3 | | - | - | | Influence score | `GET /score?username=...` | | Score deltas (7d, 30d) | `GET /score-changes?username=...` | | Follower breakdown by category | `GET /followers-stats?username=...` | | Top 20 followers by score | `GET /top-followers?username=...` | | Top 20 following by score | `GET /top-following?username=...` | | New followers (7 days) | `GET /new-followers-7d?username=...` | | New following (7 days) | `GET /new-following-7d?username=...` | These endpoints index a crypto-focused subset of accounts (influencers, projects, VCs). See [Sorsa Score and crypto analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics). ### Utility | Action | Sorsa API v3 | | - | - | | Username to numeric ID | `GET /username-to-id/:handle` | | Numeric ID to username | `GET /id-to-username/:id` | | Profile URL to numeric ID | `GET /link-to-id?link=...` | | API key usage stats | `GET /key-usage-info` | See [ID conversion](https://docs.sorsa.io/ID-Conversion). ## Response format changes The largest single change in the migration. Official v2 responses are wrapped in `data`, `includes`, and `meta`. Sorsa returns flat objects with author data embedded directly in each tweet. ### User profile **Official X API v2** (with field selection): ```json theme={null} { "data": { "id": "44196397", "name": "Elon Musk", "username": "elonmusk", "verified": false, "profile_image_url": "https://pbs.twimg.com/...", "public_metrics": { "followers_count": 100000000, "following_count": 500, "tweet_count": 30000, "listed_count": 12000 } } } ``` **Sorsa API v3:** ```json theme={null} { "id": "44196397", "username": "elonmusk", "display_name": "Elon Musk", "description": "...", "location": "Austin, TX", "profile_image_url": "https://pbs.twimg.com/...", "profile_background_image_url": "...", "followers_count": 100000000, "followings_count": 500, "tweets_count": 30000, "favourites_count": 50000, "media_count": 1200, "verified": false, "protected": false, "can_dm": true, "possibly_sensitive": false, "created_at": "2009-06-02T20:12:29Z", "bio_urls": ["https://example.com"], "pinned_tweet_ids": ["17823..."] } ``` ### Tweet **Official X API v2** (with `expansions=author_id`): ```json theme={null} { "data": { "id": "1234567890", "text": "Hello world", "created_at": "2024-01-15T12:00:00.000Z", "author_id": "44196397", "conversation_id": "1234567890", "lang": "en", "public_metrics": { "retweet_count": 100, "reply_count": 50, "like_count": 500, "quote_count": 25, "bookmark_count": 10, "impression_count": 50000 } }, "includes": { "users": [ { "id": "44196397", "name": "Elon Musk", "username": "elonmusk" } ] } } ``` **Sorsa API v3:** ```json theme={null} { "id": "1234567890", "full_text": "Hello world", "created_at": "2024-01-15T12:00:00Z", "lang": "en", "conversation_id_str": "1234567890", "likes_count": 500, "retweet_count": 100, "reply_count": 50, "quote_count": 25, "view_count": 50000, "bookmark_count": 10, "is_reply": false, "is_quote_status": false, "is_replies_limited": false, "in_reply_to_tweet_id": null, "in_reply_to_username": null, "user": { "id": "44196397", "username": "elonmusk", "display_name": "Elon Musk", "followers_count": 100000000 }, "entities": [], "quoted_status": null, "retweeted_status": null } ``` ### Field mapping #### User fields | Official X API v2 | Sorsa API v3 | Notes | | - | - | - | | `id` | `id` | Same | | `username` | `username` | Same | | `name` | `display_name` | Renamed | | `description` | `description` | Same | | `location` | `location` | Same | | `verified` | `verified` | Same | | `protected` | `protected` | Same | | `profile_image_url` | `profile_image_url` | Same | | `created_at` | `created_at` | Same | | `public_metrics.followers_count` | `followers_count` | Flattened | | `public_metrics.following_count` | `followings_count` | Flattened, renamed | | `public_metrics.tweet_count` | `tweets_count` | Flattened, renamed | | `public_metrics.listed_count` | Not available | | | Not available | `favourites_count` | Sorsa-only | | Not available | `media_count` | Sorsa-only | | Not available | `can_dm` | Sorsa-only | | Not available | `bio_urls` | Sorsa-only | | Not available | `pinned_tweet_ids` | Sorsa-only | | Not available | `profile_background_image_url` | Sorsa-only | | Not available | `possibly_sensitive` | Sorsa-only | #### Tweet fields | Official X API v2 | Sorsa API v3 | Notes | | - | - | - | | `id` | `id` | Same | | `text` | `full_text` | Renamed | | `created_at` | `created_at` | Same | | `lang` | `lang` | Same | | `conversation_id` | `conversation_id_str` | Renamed | | `in_reply_to_user_id` | `in_reply_to_username` | Returns handle instead of numeric ID | | `public_metrics.like_count` | `likes_count` | Flattened, renamed (note plural) | | `public_metrics.retweet_count` | `retweet_count` | Flattened | | `public_metrics.reply_count` | `reply_count` | Flattened | | `public_metrics.quote_count` | `quote_count` | Flattened | | `public_metrics.bookmark_count` | `bookmark_count` | Flattened | | `public_metrics.impression_count` | `view_count` | Flattened, renamed | | `author_id` + `includes.users[]` | `user` (full object inline) | Embedded | | Referenced tweets via `includes` | `quoted_status`, `retweeted_status` | Inline objects | | Not available | `is_reply`, `is_quote_status`, `is_replies_limited` | Sorsa-only booleans | | Not available | `in_reply_to_tweet_id` | Sorsa-only | | `entities` (urls, mentions, hashtags, media) | `entities` array of `{ type, link, preview }` | Different shape | ## Pagination The official API uses `pagination_token` in query parameters and returns `meta.next_token`. Sorsa uses a single field, `next_cursor`, in both directions. **For GET endpoints**, pass `next_cursor` as a query parameter: ```bash theme={null} curl "https://api.sorsa.io/v3/followers?username=elonmusk&next_cursor=ABC123" \ -H "ApiKey: $API_KEY" ``` **For POST endpoints**, include `next_cursor` in the JSON body: ```bash theme={null} curl -X POST "https://api.sorsa.io/v3/search-tweets" \ -H "ApiKey: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "from:elonmusk", "next_cursor": "ABC123" }' ``` The response always returns `next_cursor` at the top level: ```json theme={null} { "tweets": [...], "next_cursor": "XYZ789" } ``` When `next_cursor` is missing or `null`, there are no more pages. See [Pagination](https://docs.sorsa.io/pagination) for details. ## HTTP method differences Some endpoints that are `GET` on the official API are `POST` on Sorsa. | Action | Official API | Sorsa API | | - | - | - | | Get a tweet | GET | **POST** | | Search tweets | GET | **POST** | | User timeline | GET | **POST** | | Quote tweets | GET | **POST** | | Retweeters | GET | **POST** | | Replies (comments) | (no direct equivalent) | **POST** | | User profile | GET | GET | | Followers / following | GET | GET | | Lists | GET | GET | Rule of thumb: tweet-content, search, and community endpoints use `POST` with a JSON body, and this includes `/user-tweets` even though it takes a user identifier. User, list, and utility lookups use `GET` with query or path parameters. The one cross-cutting exception is `/check-comment`, which is `GET` despite taking a tweet link. When in doubt, check the endpoint reference. ## Code migration examples ### Get a user profile **Before (Official API):** ```bash theme={null} curl "https://api.x.com/2/users/by/username/elonmusk?user.fields=description,public_metrics,profile_image_url,verified,created_at" \ -H "Authorization: Bearer $BEARER_TOKEN" ``` ```python theme={null} import requests response = requests.get( "https://api.x.com/2/users/by/username/elonmusk", params={"user.fields": "description,public_metrics,profile_image_url,verified,created_at"}, headers={"Authorization": f"Bearer {BEARER_TOKEN}"}, ) user = response.json()["data"] followers = user["public_metrics"]["followers_count"] name = user["name"] ``` ```javascript theme={null} const url = "https://api.x.com/2/users/by/username/elonmusk" + "?user.fields=description,public_metrics,profile_image_url,verified,created_at"; const res = await fetch(url, { headers: { Authorization: `Bearer ${BEARER_TOKEN}` }, }); const { data: user } = await res.json(); const followers = user.public_metrics.followers_count; const name = user.name; ``` **After (Sorsa API):** ```bash theme={null} curl "https://api.sorsa.io/v3/info?username=elonmusk" \ -H "ApiKey: $API_KEY" ``` ```python theme={null} import requests response = requests.get( "https://api.sorsa.io/v3/info", params={"username": "elonmusk"}, headers={"ApiKey": API_KEY}, ) user = response.json() followers = user["followers_count"] name = user["display_name"] ``` ```javascript theme={null} const res = await fetch("https://api.sorsa.io/v3/info?username=elonmusk", { headers: { ApiKey: API_KEY }, }); const user = await res.json(); const followers = user.followers_count; const name = user.display_name; ``` ### Search tweets **Before:** ```bash theme={null} curl "https://api.x.com/2/tweets/search/recent?query=from%3Aelonmusk%20since%3A2024-01-01&tweet.fields=created_at,public_metrics,lang&expansions=author_id&user.fields=username,name&max_results=10" \ -H "Authorization: Bearer $BEARER_TOKEN" ``` ```python theme={null} params = { "query": "from:elonmusk since:2024-01-01", "tweet.fields": "created_at,public_metrics,lang", "expansions": "author_id", "user.fields": "username,name", "max_results": 10, } response = requests.get( "https://api.x.com/2/tweets/search/recent", headers={"Authorization": f"Bearer {BEARER_TOKEN}"}, params=params, ) data = response.json() tweets = data["data"] users = {u["id"]: u for u in data.get("includes", {}).get("users", [])} next_token = data.get("meta", {}).get("next_token") for tweet in tweets: author = users.get(tweet["author_id"]) print(tweet["text"], "by", author["username"]) ``` ```javascript theme={null} const params = new URLSearchParams({ query: "from:elonmusk since:2024-01-01", "tweet.fields": "created_at,public_metrics,lang", expansions: "author_id", "user.fields": "username,name", max_results: "10", }); const res = await fetch(`https://api.x.com/2/tweets/search/recent?${params}`, { headers: { Authorization: `Bearer ${BEARER_TOKEN}` }, }); const data = await res.json(); const tweets = data.data || []; const users = Object.fromEntries((data.includes?.users || []).map(u => [u.id, u])); for (const t of tweets) { const author = users[t.author_id]; console.log(t.text, "by", author.username); } ``` **After:** ```bash theme={null} curl -X POST "https://api.sorsa.io/v3/search-tweets" \ -H "ApiKey: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "from:elonmusk since:2024-01-01"}' ``` ```python theme={null} response = requests.post( "https://api.sorsa.io/v3/search-tweets", headers={"ApiKey": API_KEY}, json={"query": "from:elonmusk since:2024-01-01"}, ) data = response.json() for tweet in data["tweets"]: print(tweet["full_text"], "by", tweet["user"]["username"]) ``` ```javascript theme={null} const res = await fetch("https://api.sorsa.io/v3/search-tweets", { method: "POST", headers: { ApiKey: API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ query: "from:elonmusk since:2024-01-01" }), }); const data = await res.json(); for (const t of data.tweets) { console.log(t.full_text, "by", t.user.username); } ``` ### Paginate through all followers **Before:** ```python theme={null} def fetch_all_followers_official(user_id, token): url = f"https://api.x.com/2/users/{user_id}/followers" headers = {"Authorization": f"Bearer {token}"} followers = [] pagination_token = None while True: params = {"max_results": 1000} if pagination_token: params["pagination_token"] = pagination_token r = requests.get(url, headers=headers, params=params) r.raise_for_status() data = r.json() followers.extend(data.get("data", [])) pagination_token = data.get("meta", {}).get("next_token") if not pagination_token: break return followers ``` **After:** ```bash theme={null} CURSOR="" while :; do RES=$(curl -s "https://api.sorsa.io/v3/followers?username=elonmusk${CURSOR:+&next_cursor=$CURSOR}" \ -H "ApiKey: $API_KEY") echo "$RES" | jq '.users' CURSOR=$(echo "$RES" | jq -r '.next_cursor // empty') [ -z "$CURSOR" ] && break done ``` ```python theme={null} def fetch_all_followers(user_id, api_key): url = "https://api.sorsa.io/v3/followers" headers = {"ApiKey": api_key} followers = [] next_cursor = None while True: params = {"user_id": user_id} if next_cursor: params["next_cursor"] = next_cursor r = requests.get(url, headers=headers, params=params) r.raise_for_status() data = r.json() followers.extend(data.get("users", [])) next_cursor = data.get("next_cursor") if not next_cursor: break return followers ``` ```javascript theme={null} async function fetchAllFollowers(userId, apiKey) { const followers = []; let nextCursor = null; do { const params = new URLSearchParams({ user_id: userId }); if (nextCursor) params.set("next_cursor", nextCursor); const res = await fetch( `https://api.sorsa.io/v3/followers?${params}`, { headers: { ApiKey: apiKey } } ); const json = await res.json(); followers.push(...(json.users || [])); nextCursor = json.next_cursor || null; } while (nextCursor); return followers; } ``` Each Sorsa page returns up to 200 fully-hydrated profiles. The official API typically returns IDs and minimal user data, requiring a separate lookup to hydrate profiles. ## Search query syntax Sorsa uses X web Advanced Search syntax, which differs from the official API v2 operator set. Keep basic keywords, phrases, `from:`, and `to:` where appropriate, then translate API-specific filters and test the resulting query. For example, use `-filter:nativeretweets` to exclude native retweets. | Operator | Example | | - | - | | `from:` | `from:elonmusk` | | `to:` | `to:elonmusk` | | `since:` / `until:` | `since:2024-01-01 until:2024-02-01` | | Hashtag | `#bitcoin` | | Exact phrase | `"hello world"` | | `OR` | `bitcoin OR ethereum` | | Exclusion | `-filter:nativeretweets` | | Combined | `from:elonmusk #bitcoin -filter:nativeretweets` | Full reference: [Search operators](https://docs.sorsa.io/search-operators). The `/mentions` endpoint additionally supports server-side filters: `min_likes`, `min_replies`, `min_retweets`, `since_date`, `until_date`. See [Track mentions](https://docs.sorsa.io/search-mentions). ## Error handling The official API returns errors as a structured `errors` array: ```json theme={null} { "errors": [ { "message": "Not Found", "type": "https://api.x.com/2/problems/resource-not-found", "title": "Not Found Error", "detail": "Could not find tweet with id: [123].", "status": 404 } ] } ``` Sorsa returns a simple shape: ```json theme={null} { "message": "Tweet not found" } ``` Status codes are consistent across all endpoints: `400`, `401`, `403`, `404`, `429`, `500`. See [Error codes](https://docs.sorsa.io/error-codes). Rate limit handling: on `429`, back off and retry. The universal limit is **20 req/s** across all endpoints. There are no per-endpoint windows to track. See [Rate limits](https://docs.sorsa.io/rate-limits). A retry wrapper that handles both APIs: ```python theme={null} import time import requests def call_with_retry(method, url, max_retries=3, **kwargs): for attempt in range(max_retries): r = requests.request(method, url, **kwargs) if r.status_code == 429: time.sleep(2 ** attempt) continue r.raise_for_status() return r.json() raise RuntimeError(f"Failed after {max_retries} retries") ``` ## Migration checklist * Replace `Authorization: Bearer ...` with `ApiKey: ...`. * Remove OAuth 1.0a signature logic (consumer keys, access tokens, signature generation). * Update base URL from `https://api.x.com/2` to `https://api.sorsa.io/v3`. * Map every endpoint path using the tables above. * Switch GET to POST for tweet, search, comment, quote, retweeter endpoints. * Remove `tweet.fields`, `user.fields`, `media.fields`, and `expansions` parameters. * Update response parsers: remove `data` / `includes` / `meta` unwrapping. * Rename fields (`name` to `display_name`, `text` to `full_text`, etc.). * Flatten metric access (drop the `public_metrics` wrapper). * Replace `pagination_token` / `next_token` with `next_cursor`. * Update error handling for the simplified `{ "message": "..." }` format. * Adjust rate-limit logic: 20 req/s universal, no per-endpoint windows. * Test critical endpoints in the [API Playground](https://api.sorsa.io/playground). * Monitor quota via `GET /key-usage-info`. * Retain the official API key for write operations (posting, DMs) if needed. ## Features without an official API equivalent | Feature | Endpoint | | - | - | | Tweet timeline without 3,200-tweet cap | `POST /user-tweets` | | Single-call follow check | `POST /check-follow` | | Single-call retweet check | `POST /check-retweet` | | Single-call comment check | `GET /check-comment` | | Single-call quote/retweet check | `POST /check-quoted` | | Community membership check | `POST /check-community-member` | | Community members and feed | `POST /community-members`, `POST /community-tweets` | | In-community search | `POST /community-search-tweets` | | Long-form X Article content | `POST /article` | | Verified-only follower filter | `GET /verified-followers` | | Account country and username change history | `GET /about` | | Influence score | `GET /score`, `GET /score-changes` | | Top followers and following by influence | `GET /top-followers`, `GET /top-following` | | Categorized follower breakdown | `GET /followers-stats` | ## Related references * [Authentication](https://docs.sorsa.io/authentication) * [Base URL and versioning](https://docs.sorsa.io/base-url-and-versioning) * [Rate limits](https://docs.sorsa.io/rate-limits) * [Pagination](https://docs.sorsa.io/pagination) * [Error codes](https://docs.sorsa.io/error-codes) * [Response format](https://docs.sorsa.io/response-format) * [Search operators](https://docs.sorsa.io/search-operators) * [Optimizing API usage](https://docs.sorsa.io/optimizing-api-usage) * [API Reference](https://docs.sorsa.io/api-reference-guide) # Optimizing API Usage Source: https://docs.sorsa.io/optimizing-api-usage Reduce unnecessary requests by reusing embedded profiles, batching lookups, and storing results by stable ID. The examples below show when each approach applies. > **Tip:** Every account includes 100 free requests (no card, no expiry). That is plenty to prototype the patterns below and measure your real request volume before choosing a plan. *** ## Example setup Python examples use `requests` (`python -m pip install requests`) and run on your backend. Replace the API key and example IDs before running. `search_results` means a list of tweet objects from an earlier request. ```python theme={null} import requests API_KEY = "YOUR_API_KEY" ``` ## Principle 1: Every Tweet Response Already Contains User Data This is the single most important efficiency insight for working with Sorsa API. Every endpoint that returns tweets (`/search-tweets`, `/user-tweets`, `/list-tweets`, `/comments`, `/quotes`, `/mentions`) embeds the **complete author profile** inside each tweet object. ```json theme={null} { "tweets": [ { "id": "2029914600217473314", "full_text": "Great thread on API design patterns...", "likes_count": 142, "user": { "id": "1422280682240450563", "username": "dev_sarah", "display_name": "Sarah Chen", "description": "Staff engineer @stripe. APIs, distributed systems.", "followers_count": 12400, "followings_count": 890, "tweets_count": 4521, "verified": true, "location": "San Francisco", "created_at": "2021-02-01T09:15:22Z" } } ] } ``` The `user` object inside each tweet contains the same data that a dedicated `/info` call would return: ID, username, display name, bio, follower count, following count, tweet count, verified status, location, creation date, profile image, and more. **What this means in practice:** If you search for tweets about a topic and want to build a list of users discussing it, you do not need to make separate `/info` calls for each author. The user data is already in the response. Extract it directly: ```python theme={null} # Collect unique users from a tweet search - zero extra API calls seen_ids = set() unique_users = [] for tweet in search_results: user = tweet["user"] if user["id"] not in seen_ids: seen_ids.add(user["id"]) unique_users.append(user) print(f"Found {len(unique_users)} unique users from {len(search_results)} tweets") ``` This one pattern can eliminate hundreds or thousands of unnecessary `/info` requests in a typical workflow. *** ## Principle 2: Use Batch Endpoints When They Exist Sorsa provides batch variants for the most common lookup operations. Using them instead of looping through the single-item endpoint reduces your request count dramatically. ### `/info-batch` Instead of Looping `/info` If you need profile data for multiple accounts, do not call `/info` in a loop. Use `/info-batch` to fetch up to 100 at once: ```python theme={null} # A separate /info call for each account would use 10 requests. # Efficient: 10 accounts = 1 request resp = requests.get( "https://api.sorsa.io/v3/info-batch", headers={"ApiKey": API_KEY}, params={"usernames": ["NASA", "SpaceX", "Tesla", "OpenAI", "stripe", "shopify", "vercel", "github", "notion", "linear"]}, ) resp.raise_for_status() profiles = resp.json().get("users", []) ``` **Savings:** 10 accounts = 1 request instead of 10. The reduction scales linearly, up to 100 accounts per call. ### `/tweet-info-bulk` Instead of Looping `/tweet-info` If you have a list of tweet IDs (from an archive, a mention export, or a list of bookmarked links) and need their current engagement metrics and author data, use the bulk endpoint to hydrate up to 100 tweets in a single request: ```python theme={null} # A separate /tweet-info call for each tweet would use up to 100 requests. # Efficient: 100 tweets = 1 request resp = requests.post( "https://api.sorsa.io/v3/tweet-info-bulk", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json={ "tweet_links": [ "https://x.com/user/status/111111", "https://x.com/user/status/222222", # ... up to 100 links ] }, ) resp.raise_for_status() tweets = resp.json().get("tweets", []) ``` **Savings:** 100 tweets = 1 request instead of 100. This is a 99% reduction. *** ## Principle 3: Use `/list-tweets` Instead of Multiple `/user-tweets` Calls If you are monitoring or collecting recent tweets from multiple accounts, do not poll each one individually. Add them to an X List and make a single `/list-tweets` request that returns the combined recent activity from all members. ```python theme={null} # Separate /user-tweets calls would use 30 requests per polling cycle. LIST_ID = "YOUR_LIST_ID" # Efficient: 1 request covers all 30 accounts resp = requests.get( f"https://api.sorsa.io/v3/list-tweets?list_id={LIST_ID}", headers={"ApiKey": API_KEY}, timeout=30, ) resp.raise_for_status() ``` **First-page estimate:** one List request per 10-second cycle uses 259,200 requests in 30 days, compared with 7,776,000 for 30 separate first-page timeline requests. Additional pages and retries increase either total; a busy List may need pagination within each cycle. This is covered in detail in the [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring) guide. For instructions on setting up and managing X Lists, see [Lists & Communities](https://docs.sorsa.io/lists-and-communities). *** ## Principle 4: Use `/info` as Your Universal Resolver The `/info` endpoint accepts a username, user ID, or profile link, in any format. It returns the full profile object including the permanent User ID. This makes it the most flexible endpoint for normalizing mixed inputs. If you receive account references in different formats and need to resolve all of them to full profiles, use `/info` once per account instead of calling a conversion endpoint (`/username-to-id`, `/link-to-id`) followed by a separate `/info` call: ```python theme={null} # Resolving an ID and then fetching its profile would use two requests. # Efficient: 1 request per account response = requests.get( "https://api.sorsa.io/v3/info", headers={"ApiKey": API_KEY}, params={"username": "stripe"}, # accepts username, user_id, or user_link timeout=30, ) response.raise_for_status() profile = response.json() # profile already contains the user ID, plus everything else ``` **When to use conversion endpoints separately:** Only when you specifically need just the ID or just the handle, and do not need the full profile. For example, converting a list of 1,000 handles to IDs for database storage. If you already have a profile from a prior step, reuse its `id` without another request. See [ID Conversion](https://docs.sorsa.io/ID-Conversion) for conversion patterns. *** ## Principle 5: Deduplicate in Your Database Layer When you collect data from multiple sources (search results, follower lists, mention feeds, timeline scrapes), the same user will appear many times. Use the permanent User ID as your deduplication key and update existing records rather than inserting duplicates. ```python theme={null} import sqlite3 db = sqlite3.connect("audience.db") db.execute(""" CREATE TABLE IF NOT EXISTS users ( user_id TEXT PRIMARY KEY, username TEXT, display_name TEXT, description TEXT, followers_count INTEGER, tweets_count INTEGER, verified INTEGER, updated_at TEXT ) """) def upsert_user(db, user): """Insert or update a user record keyed by permanent User ID.""" db.execute(""" INSERT INTO users (user_id, username, display_name, description, followers_count, tweets_count, verified, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now')) ON CONFLICT(user_id) DO UPDATE SET username = excluded.username, display_name = excluded.display_name, description = excluded.description, followers_count = excluded.followers_count, tweets_count = excluded.tweets_count, verified = excluded.verified, updated_at = datetime('now') """, ( user["id"], user["username"], user.get("display_name", ""), user.get("description", ""), user.get("followers_count", 0), user.get("tweets_count", 0), user.get("verified", False), )) db.commit() # Every time you encounter a user in any API response, upsert: for tweet in search_results: upsert_user(db, tweet["user"]) # The user's profile data stays fresh without separate /info calls ``` This means your user table is continuously updated with the latest profile data every time that user appears in any API response: search results, mentions, follower lists, comments, quotes. Accounts that do not reappear in your inputs will not be refreshed by this pattern. Schedule batch refreshes when your application requires a defined freshness interval. *** ## Principle 6: Avoid Re-fetching Data You Already Have When building multi-step pipelines, pass data forward between steps instead of re-fetching it. **Example: Audience geography analysis.** The workflow is: (1) fetch followers, (2) look up country via `/about` for each follower. After step 1, you already have the full profile object for every follower (bio, follower count, verified status). Do not call `/info` again in step 2. You only need `/about` for the country data that is not in the standard profile. See the [Audience Geography](https://docs.sorsa.io/Audience-Geography) guide for the complete workflow. **Example: Campaign verification.** When verifying follow + retweet + comment for a participant, the `/check-comment` response includes the full tweet object of the comment (when `commented: true`). If you need to analyze the comment text for quality, extract it from the verification response. Do not make a separate `/search-tweets` or `/comments` call to find it again. **Example: Building a user list from search results.** If you searched for tweets and collected 500 unique users from the embedded `user` objects, and now want to find which of them have 10K+ followers, filter the data you already collected. Do not call `/info` for each of the 500 users. *** ## Quick Reference: Choosing the Right Endpoint | You have... | You need... | Use this | Not this | | :- | :- | :- | :- | | A list of handles | Full profiles for all of them | `GET /info-batch` (one request) | `/info` in a loop | | A list of tweet URLs | Full tweet data + author profiles | `POST /tweet-info-bulk` (up to 100/request) | `/tweet-info` in a loop | | 30 accounts to monitor | Recent tweets from all of them | `GET /list-tweets` (one request) | `/user-tweets` x 30 | | A handle, need the full profile | ID + bio + counts + everything | `GET /info` | `/username-to-id` then `/info` | | Tweet search results | Author profiles | Extract `tweet["user"]` from response | `/info` for each author | | Follower list | Profile data per follower | Already in `/followers` response | `/info` for each follower | *** ## Estimating Your Request Budget When planning a project, estimate your total request count before you start: | Task | Efficient approach | Requests needed | | :- | :- | :- | | Profile data for 50 accounts | `/info-batch` | \~1 | | 10,000 followers of an account | `/followers` with pagination (200/page) | 50 | | Country data for those 10,000 followers | `/about` for each | 10,000 | | 100 tweets hydrated with current metrics | `/tweet-info-bulk` | 1 | | Monitor 50 accounts every 10 seconds for a day | `/list-tweets` | 8,640 | | Verify 5 tasks for 1,000 campaign participants | 5 checks per user | 5,000 | The total for a typical competitive intelligence and campaign verification workflow might be: 50 (followers) + 10,000 (geography) + 1 (bulk tweets) + 8,640 (monitoring) + 5,000 (campaign) = \~23,700 requests. At 20 req/s, this volume has a theoretical throughput lower bound of about 20 minutes. The monitoring portion still spans a full day, and sequential pagination, response latency, and retries add elapsed time. See [Pricing](https://api.sorsa.io/pricing) to estimate the cost for your plan. *** ## Next Steps * [Rate Limits](https://docs.sorsa.io/rate-limits) - handling 429 errors and optimizing throughput. * [Pagination](https://docs.sorsa.io/pagination) - cursor-based pagination patterns for large-scale extraction. * [Pricing](https://api.sorsa.io/pricing) - understand cost per request and plan your budget. * [API Reference](https://docs.sorsa.io/api-reference-guide) - full specification for `/info-batch`, `/tweet-info-bulk`, and all Sorsa API endpoints. # Pagination Source: https://docs.sorsa.io/pagination When you request a list of items from Sorsa API, the results are returned in pages. To retrieve the full dataset, you iterate through pages using cursors until no more data is available. *** ## How cursor-based pagination works Sorsa uses cursor-based pagination instead of traditional page numbers. This approach is more reliable for social media data, where new content is constantly being added and offset-based pagination would skip or duplicate results. The flow is the same for every paginated endpoint: 1. Send your initial request without a cursor. 2. The response includes your data and a `next_cursor` field. 3. Pass the `next_cursor` value in your next request to fetch the next page. 4. When `next_cursor` is `null` or absent from the response, you have reached the end. Not every endpoint uses pagination. Single-object endpoints like `/info`, `/tweet-info`, `/score`, and `/about` return one result and have no cursor. Some list endpoints also return a single response without pagination, including `/info-batch`, `/tweet-info-bulk`, and crypto analytics lists such as `/top-followers`. Check the endpoint reference for a `next_cursor` parameter. *** ## Response structure Paginated responses always follow one of these two patterns: ```text theme={null} { "users": [ ... ], "next_cursor": "DAABCgABF7Y..." } ``` ```text theme={null} { "tweets": [ ... ], "next_cursor": "DAABCgABF7Y..." } ``` For these paginated user and tweet responses, data is in `users` or `tweets`. Treat `next_cursor` as an opaque value: pass it back unchanged and stop when it is null, empty, or absent. Do not increment a cursor or convert it to a JavaScript Number. For full details on response wrappers and object schemas, see [Response Format](https://docs.sorsa.io/response-format). *** ## How cursors are passed The cursor field is always called `next_cursor`. The only difference between endpoints is where you put it: GET endpoints take it as a query parameter, POST endpoints take it in the JSON body. **GET endpoints** (like `/followers`, `/follows`, `/list-tweets`) take `next_cursor` as a query parameter: ```bash theme={null} # First page curl --request GET \ --url 'https://api.sorsa.io/v3/followers?username=elonmusk' \ --header 'ApiKey: YOUR_API_KEY' # Next page curl --request GET \ --url 'https://api.sorsa.io/v3/followers?username=elonmusk&next_cursor=DAABCgABF7Y...' \ --header 'ApiKey: YOUR_API_KEY' ``` **POST endpoints** (like `/search-tweets`, `/user-tweets`, `/comments`) take `next_cursor` in the JSON body: ```bash theme={null} # First page curl --request POST \ --url 'https://api.sorsa.io/v3/search-tweets' \ --header 'ApiKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{"query": "bitcoin"}' # Next page curl --request POST \ --url 'https://api.sorsa.io/v3/search-tweets' \ --header 'ApiKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{"query": "bitcoin", "next_cursor": "DAABCgABF7Y..."}' ``` *** ## Page sizes are not fixed Due to the nature of X platform data, the number of items returned per page can vary. An endpoint that returns up to 20 items per page might return 18, 12, or even 5 on a given page, even when more data exists on the next page. **Never use the item count to decide whether you have reached the end.** A page with fewer items than expected does not mean there is no more data. Always check the `next_cursor` field. If it exists and is not `null`, there are more pages to fetch. *** ## Full pagination examples These examples collect results in memory. For large jobs, write each page to storage, deduplicate by string ID, and save a checkpoint after the page is stored. Keep the account, query, filters, and sort order unchanged while using a cursor. Set a page or request budget and detect repeated cursors to avoid accidental endless jobs. A delay in one loop does not coordinate other workers sharing the same API key. **Python: paginating through followers (GET endpoint)** ```python theme={null} import time import requests API_KEY = "YOUR_API_KEY" def fetch_all_followers(username): all_users = [] cursor = None while True: params = {"username": username} if cursor: params["next_cursor"] = cursor response = requests.get( "https://api.sorsa.io/v3/followers", params=params, headers={"ApiKey": API_KEY}, timeout=30, ) response.raise_for_status() data = response.json() users = data.get("users", []) all_users.extend(users) print(f"Page fetched: {len(users)} users. Total so far: {len(all_users)}") cursor = data.get("next_cursor") if not cursor: break time.sleep(0.05) # respect 20 req/s rate limit return all_users followers = fetch_all_followers("elonmusk") print(f"Done. {len(followers)} followers total.") ``` **Python: paginating through search results (POST endpoint)** ```python theme={null} import time import requests API_KEY = "YOUR_API_KEY" def search_all_tweets(query): all_tweets = [] cursor = None while True: body = {"query": query} if cursor: body["next_cursor"] = cursor response = requests.post( "https://api.sorsa.io/v3/search-tweets", json=body, headers={"ApiKey": API_KEY}, timeout=30, ) response.raise_for_status() data = response.json() tweets = data.get("tweets", []) all_tweets.extend(tweets) print(f"Page fetched: {len(tweets)} tweets. Total so far: {len(all_tweets)}") cursor = data.get("next_cursor") if not cursor: break time.sleep(0.05) return all_tweets results = search_all_tweets("bitcoin") print(f"Done. {len(results)} tweets total.") ``` **JavaScript: paginating through followers (GET endpoint)** ```javascript theme={null} async function fetchAllFollowers(username) { const API_KEY = "YOUR_API_KEY"; const allUsers = []; let cursor = null; while (true) { const params = new URLSearchParams({ username }); if (cursor) params.append("next_cursor", cursor); const response = await fetch( `https://api.sorsa.io/v3/followers?${params}`, { headers: { "ApiKey": API_KEY } } ); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); const users = data.users || []; allUsers.push(...users); console.log(`Page fetched: ${users.length} users. Total: ${allUsers.length}`); cursor = data.next_cursor; if (!cursor) break; await new Promise(r => setTimeout(r, 50)); } return allUsers; } ``` **JavaScript: paginating through search results (POST endpoint)** ```javascript theme={null} async function searchAllTweets(query) { const API_KEY = "YOUR_API_KEY"; const allTweets = []; let cursor = null; while (true) { const body = { query }; if (cursor) body.next_cursor = cursor; const response = await fetch("https://api.sorsa.io/v3/search-tweets", { method: "POST", headers: { "ApiKey": API_KEY, "Content-Type": "application/json" }, body: JSON.stringify(body) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); const tweets = data.tweets || []; allTweets.push(...tweets); console.log(`Page fetched: ${tweets.length} tweets. Total: ${allTweets.length}`); cursor = data.next_cursor; if (!cursor) break; await new Promise(r => setTimeout(r, 50)); } return allTweets; } ``` *** ## Pagination with error handling In production, combine pagination with retry logic so that a single failed page does not break your entire data collection run. For the full error handling reference, see [Error Codes](https://docs.sorsa.io/error-codes). ```python theme={null} import time import requests API_KEY = "YOUR_API_KEY" def paginate_with_retries(username, max_retries=3): all_users = [] cursor = None while True: params = {"username": username} if cursor: params["next_cursor"] = cursor for attempt in range(max_retries): response = requests.get( "https://api.sorsa.io/v3/followers", params=params, headers={"ApiKey": API_KEY}, timeout=30, ) if response.status_code == 200: break elif response.status_code == 429: time.sleep(1) continue elif response.status_code >= 500: time.sleep(2) continue else: raise Exception(f"Error {response.status_code}: {response.text}") else: raise Exception("Max retries exceeded") data = response.json() all_users.extend(data.get("users", [])) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.05) return all_users ``` *** ## Next steps * [Rate Limits](https://docs.sorsa.io/rate-limits) - Understand the 20 req/s limit when paginating through large datasets * [Error Codes](https://docs.sorsa.io/error-codes) - Handle 429 and other errors during pagination loops * [Response Format](https://docs.sorsa.io/response-format) - Full reference for User and Tweet object schemas # Pay as you go Source: https://docs.sorsa.io/pay-as-you-go Keep making requests after your plan's included quota runs out, at the same per-request rate as your plan. Pay as you go (PAYG), shown as **Extra requests** in Billing, lets you use a prepaid balance for requests beyond your plan's included quota. Add funds by card or crypto, set a spending limit, and use your balance when you need it. * **No expiry.** Your unused balance stays in your account until you use it. * **Your plan's rate.** Extra requests cost the same per request as your current plan's monthly rate. * **Controlled spending.** Set how much of your balance can be spent on extra requests each billing period. > PAYG requires an active paid plan. Adding funds does not activate or renew a plan. ## How it works 1. Your available included requests are used first. 2. Once those requests run out, additional requests are charged to your PAYG balance, provided **Extra requests** is enabled and your spending limit has not been reached. 3. When your next included quota becomes available, requests use that quota again. Your remaining PAYG balance is saved for later. Your balance is stored in **USD**, not as a fixed request package. Any request estimate shown in Billing is calculated using your current plan's PAYG rate. ## Pricing Your PAYG rate is calculated from your current plan's monthly price and monthly request quota: **Price per request = monthly plan price ÷ monthly included requests** There is no additional PAYG markup. Check **Billing → Extra requests** for your current rate. For example, at a rate of **USD 1.90 per 1,000 requests**, using 10,000 extra requests costs **USD 19**. **Annual plans use the same PAYG rate as the monthly version of that plan.** The annual subscription discount does not reduce the PAYG rate. ### Changing plans Your dollar balance stays the same when you change plans. Once the new plan takes effect, future extra requests use its rate. Previous charges are not recalculated. A lower per-request rate means the same balance covers more requests; a higher rate means it covers fewer. A scheduled plan change does not affect your rate until it takes effect. ## Add funds In **Billing**, enable **Extra requests**, then select **Add funds**. Choose a preset or enter a custom amount, select **card or crypto**, and complete the payment. Funds become available once the payment is confirmed. You can use either payment method regardless of how you pay for your plan. You can also add funds in advance, before your included requests run out. Top-ups are manual. Adding funds increases your available balance; it does not change your plan, increase your included quota, or reset your spending limit. > **Funds cannot be withdrawn.** Once added, they stay in your account for future usage and cannot be transferred back to your card or crypto wallet. Unused funds never expire. ## Set a spending limit Open **Extra requests → Limits settings** to set the maximum amount you want to spend on extra requests per monthly billing period. Billing shows how much you have spent and when the limit resets. **Your balance and your spending limit are separate.** For example, you can add **USD 1,000** and set a **USD 100** spending limit. At most USD 100 can be used for extra requests during that period; the remaining funds stay available for future periods. * The limit applies only to PAYG usage, not to top-ups or your subscription payment. * Reaching the limit pauses extra requests, even if you still have money in your balance. Any available included requests remain usable. * Adding funds does not remove this restriction. Increase the limit or wait for the next period to use PAYG again. * Changing plans does not reset the amount already spent during the current period. * On an annual plan, the PAYG spending limit still resets monthly. ## When extra requests pause | Situation | What happens | How to continue | | - | - | - | | Extra requests is switched off | Your balance is preserved and is not used for PAYG. | Enable Extra requests. | | There are not enough funds for the next request | PAYG pauses. | Add funds. | | Your spending limit is reached | PAYG pauses; your remaining balance is preserved. | Increase the limit or wait for it to reset. | | You no longer have an active paid plan | PAYG is unavailable; your balance is preserved. | Activate a paid plan. | PAYG can resume once you have an active paid plan, Extra requests enabled, enough funds, and room within your spending limit. ## If you cancel your plan or return later Canceling a card subscription stops future subscription renewals. PAYG remains available until your already-paid access ends, subject to your balance and spending limit. If your paid plan expires, **your PAYG balance does not expire with it**. Return later and activate a paid plan to use your remaining funds at that plan's PAYG rate. The no-expiry rule applies to your **unused PAYG balance**. Requests included in your plan still follow that plan's quota and expiry rules. # Pricing Source: https://docs.sorsa.io/pricing # Pricing, Plans, and Cost per Request Sorsa API uses a flat pricing model: **1 API call = 1 request from your quota**, regardless of which endpoint you hit. A complex tweet search costs the same as a simple ID conversion. There are no "credit multipliers," no per-endpoint pricing tiers, and no hidden charges for user data embedded in tweet responses. This page explains the available plans, breaks down the effective cost per data unit for every endpoint, and shows how to estimate your monthly spend for common workflows. *** ## Plans | | Starter | Pro | Enterprise | | :- | :- | :- | :- | | Monthly requests | 10,000 | 100,000 | 500,000 | | Price | \$49/mo | \$199/mo | \$899/mo | | Cost per request | \$0.0049 | \$0.00199 | \$0.0018 | | Rate limit | 20 req/s | 20 req/s | 20 req/s | | All endpoints included | Yes | Yes | Yes | **Start with 100 free requests.** Every new account gets 100 requests at no cost, with no credit card required. They never expire and work across all available endpoints, so you can test any part of the API before choosing a plan. **Custom plans.** If you need more than 500,000 requests per month (for AI training, large-scale data retrieval, or continuous monitoring across thousands of accounts), we offer custom volumes with dedicated rate limits. Get in touch through [Talk to Sales](https://api.sorsa.io/talk-to-sales) or reach us on [Discord](https://discord.com/invite/uwAefKCj7X). **Rate limit is the same on all standard plans.** Every standard plan gets 20 requests per second. Upgrading from Starter to Pro does not change your speed, only your monthly quota. Custom plans can include higher, dedicated rate limits. *** ## The Flat-Rate Principle Many X data providers use credit-based pricing, where different endpoints consume different numbers of credits per call. A search might cost 25 credits, a profile lookup 5 credits, and a follower extraction 50 credits. This makes budgeting difficult and creates situations where the same data costs wildly different amounts depending on how you access it. Sorsa does not work this way. Every request to every endpoint costs exactly 1 request from your quota. On top of that: * **User data is included for free in tweet responses.** When `/search-tweets`, `/user-tweets`, `/comments`, `/mentions`, or any tweet endpoint returns results, each tweet includes the full author profile (ID, username, bio, follower count, verified status, and more). You are not charged extra for this embedded data. * **Batch endpoints exist for high-volume lookups.** `/info-batch` and `/tweet-info-bulk` let you fetch up to 100 items in a single request, so the effective cost per item drops significantly when you use them. * **Follower and following endpoints return up to 200 profiles per request.** This is one of the highest per-request yields available. *** ## Cost per Data Unit The real question is not "how much does a request cost?" but "how much does it cost to get the data I need?" The answer depends on how many items each endpoint returns per request. ### Tweet Data | Endpoint | Items per request | Starter | Pro | Enterprise | | :- | :- | :- | :- | :- | | `/search-tweets` | \~20 tweets | \$0.00025 | \$0.0001 | \$0.00009 | | `/user-tweets` | \~20 tweets | \$0.00025 | \$0.0001 | \$0.00009 | | `/mentions` | \~20 tweets | \$0.00025 | \$0.0001 | \$0.00009 | | `/comments` | \~20 tweets | \$0.00025 | \$0.0001 | \$0.00009 | | `/quotes` | \~20 tweets | \$0.00025 | \$0.0001 | \$0.00009 | | `/list-tweets` | \~20 tweets | \$0.00025 | \$0.0001 | \$0.00009 | | `/tweet-info-bulk` | up to 100 tweets | \$0.00005 | \$0.00002 | \$0.000018 | | `/tweet-info` | 1 tweet | \$0.0049 | \$0.00199 | \$0.0018 | **Takeaway:** Always use `/tweet-info-bulk` instead of looping `/tweet-info`. The per-tweet cost difference is roughly 100x. For collecting tweets by keyword, topic, or user, the search and timeline endpoints are the most efficient at \~\$0.0001 per tweet on Pro. ### User Profile Data | Endpoint | Items per request | Starter | Pro | Enterprise | | :- | :- | :- | :- | :- | | `/followers` | up to 200 users | \$0.000025 | \$0.00001 | \$0.000009 | | `/follows` | up to 200 users | \$0.000025 | \$0.00001 | \$0.000009 | | `/verified-followers` | up to 200 users | \$0.000025 | \$0.00001 | \$0.000009 | | `/community-members` | \~20 users | \$0.00025 | \$0.0001 | \$0.00009 | | `/list-members` | \~20 users | \$0.00025 | \$0.0001 | \$0.00009 | | `/retweeters` | \~20 users | \$0.00025 | \$0.0001 | \$0.00009 | | `/search-users` | \~20 users | \$0.00025 | \$0.0001 | \$0.00009 | | `/info-batch` | up to 100 profiles | \$0.00005 | \$0.00002 | \$0.000018 | | `/info` | 1 profile | \$0.0049 | \$0.00199 | \$0.0018 | **Takeaway:** Follower and following extraction is extremely cost-efficient at \$0.00001 per profile on Pro. For bulk profile lookups, always use `/info-batch` instead of looping `/info`. ### Verification Checks | Endpoint | Items per request | Starter | Pro | Enterprise | | :- | :- | :- | :- | :- | | `/check-follow` | 1 check | \$0.0049 | \$0.00199 | \$0.0018 | | `/check-retweet` | 1 check | \$0.0049 | \$0.00199 | \$0.0018 | | `/check-quoted` | 1 check | \$0.0049 | \$0.00199 | \$0.0018 | | `/check-comment` | 1 check | \$0.0049 | \$0.00199 | \$0.0018 | | `/check-community-member` | 1 check | \$0.0049 | \$0.00199 | \$0.0018 | Verification endpoints are one-check-per-request by design (each checks a specific user against a specific action). For a campaign with 1,000 participants and 5 tasks each, budget 5,000 requests. ### Utility Endpoints | Endpoint | Items per request | Starter | Pro | Enterprise | | :- | :- | :- | :- | :- | | `/about` (country data) | 1 result | \$0.0049 | \$0.00199 | \$0.0018 | | `/username-to-id` | 1 conversion | \$0.0049 | \$0.00199 | \$0.0018 | | `/id-to-username` | 1 conversion | \$0.0049 | \$0.00199 | \$0.0018 | | `/link-to-id` | 1 conversion | \$0.0049 | \$0.00199 | \$0.0018 | | `/score` | 1 result | \$0.0049 | \$0.00199 | \$0.0018 | *** ## What You Get Without Extra Requests Some data comes at zero additional cost because it is embedded in responses you are already making: **User profiles inside tweet responses.** Every tweet from `/search-tweets`, `/user-tweets`, `/mentions`, `/comments`, `/quotes`, and `/list-tweets` includes the full author profile. If you search for 1,000 tweets and they come from 400 unique users, you now have 400 complete profiles without a single `/info` call. **Comment text inside verification responses.** When `/check-comment` returns `commented: true`, the response includes the full tweet object of the comment. There is no need to call `/comments` separately to read what the user wrote. **Quote text inside quote verification.** When `/check-quoted` returns `status: "quoted"`, the response includes the quote text and timestamp. **Article data inside `/article`.** The `/article` endpoint returns the full article text, engagement metrics, and the author profile in one call. *** ## Budget Estimation for Common Workflows ### Social Listening: monitor a keyword and collect tweets You poll `/search-tweets` every 30 seconds, 24/7, to capture tweets about your brand. * 1 request per 30 seconds = 2 req/min = 2,880 req/day = **86,400 req/month** * Fits in the **Pro plan** (\$199/mo) * Each request returns \~20 tweets, so you collect up to \~1.7M tweets/month ### Competitor Analysis: profile, content, and mentions for 5 competitors * 5 profile lookups via `/info`: 5 requests * 50 pages of tweets per competitor via `/user-tweets`: 250 requests * 10 pages of mentions per competitor via `/mentions`: 50 requests * Total per run: **305 requests** * Run weekly: \~1,220 req/month, fits comfortably in the **Starter plan** ### Audience Research: extract and geo-analyze 50,000 followers * 50,000 followers via `/followers` (200/page): 250 requests * 50,000 country lookups via `/about`: 50,000 requests * Total: **50,250 requests** * Fits in the **Pro plan** (\$199/mo) * Cost: \~\$100 (50,250 x \$0.00199) ### Campaign Verification: 2,000 participants, 5 tasks each * 2,000 x 5 verification checks: 10,000 requests * Fits in the **Starter plan** (\$49/mo) ### Large-Scale Data Collection: full timeline retrieval for 100 accounts * Average 500 tweets per account, \~20 per page = 25 pages per account * 100 accounts x 25 pages = 2,500 requests * Plus you get \~50,000 tweets with embedded user profiles * Fits in the **Starter plan** (\$49/mo) ### Real-Time Monitoring: track 200 accounts via X List, 5-second interval * 1 request per 5 seconds = 17,280 req/day = **518,400 req/month** * This is just over the Enterprise quota of 500,000, so it needs a **custom plan** (or a slightly longer polling interval to fit Enterprise at \$899/mo) * A single `/list-tweets` request per cycle covers all 200 accounts *** ## Cost Optimization Strategies These are covered in depth in the [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) guide, but here is the summary: **Use batch endpoints.** `/tweet-info-bulk` (up to 100 tweets/request) and `/info-batch` (up to 100 profiles/request) exist specifically to reduce request counts for bulk lookups. **Use `/list-tweets` for multi-account monitoring.** Monitoring 50 accounts individually costs 50x more requests than grouping them in an X List and polling once. **Extract user data from tweet responses.** Every tweet endpoint embeds author profiles. Build your user database from search and timeline results instead of making separate `/info` calls. **Use `/info` instead of conversion plus lookup.** If you need both the user ID and the full profile, call `/info` once (it accepts usernames, IDs, and links) instead of calling `/username-to-id` and then `/info` separately. **Choose the right plan for your volume.** Moving from Starter to Pro cuts your cost per request by 59% (\$0.0049 to \$0.00199). Moving from Pro to Enterprise cuts it by another 10%. If you consistently use more than 80% of your plan's quota, upgrading saves money. > **How does this compare to the official X API?** The official X API uses pay-per-use pricing at \$0.005 per post read, with author profiles billed separately at \$0.010 each, and caps pay-per-use accounts at 2M post reads per month. Reading around 1M posts runs about \$5,000/mo. On Sorsa Pro at \$199/mo, 100,000 requests yield roughly 2M tweets with author profiles included, which works out to about \$0.0001 per tweet versus \$0.005 on the official API. Authentication is also simpler: a single API key header instead of OAuth 2.0. For a full walkthrough, see the [Migration from the official X API](https://docs.sorsa.io/migration-from-official-x-api) guide. *** ## Checking Your Current Usage Use `GET /key-usage-info` to see your current request consumption and remaining quota: ```text theme={null} curl "https://api.sorsa.io/v3/key-usage-info" \ -H "ApiKey: YOUR_API_KEY" ``` ```json theme={null} { "key_requests": 100000, "remaining_requests": 87340, "total_requests": 512680, "valid_until": "2026-08-01T00:00:00Z" } ``` * `key_requests` - total requests allocated for the current billing period (your plan's quota) * `remaining_requests` - requests still available in the current period * `total_requests` - cumulative requests used across all billing periods * `valid_until` - when the current request balance expires Build this check into your monitoring scripts to alert you before you approach your quota limit. You can also view usage history visually on the [Usage stats](https://api.sorsa.io/overview/usage) dashboard page. *** ## Next Steps * [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) - detailed patterns for deduplication, batch endpoints, and avoiding redundant calls. * [Rate Limits](https://docs.sorsa.io/rate-limits) - handling the 20 req/s limit, 429 errors, and backoff strategies. * [API Reference](https://docs.sorsa.io/api-reference-guide) - full specification for all endpoints. # Quickstart Source: https://docs.sorsa.io/quickstart Get your API key and make your first request to the X (Twitter) API in minutes. This guide takes you from zero to your first response: create an account, authenticate, then run your first `GET` and `POST` requests. Each step includes working examples in cURL, Python, and JavaScript. ## Step 1: Get your API key 1. Open the [Sorsa Dashboard](https://api.sorsa.io/overview) and click **Sign in**. Register with any available option. Sorsa asks for nothing beyond what your auth provider shares. 2. Your account starts with **100 free requests**. No card required, they work on all available endpoints, and they never expire, so you can start testing immediately. 3. When you need more volume, choose a plan (10K, 100K, or 500K requests per month) and a billing cycle (monthly or yearly), then pay by card or crypto. Once you are signed in, the dashboard shows your **API key** and your **remaining request quota**. > **Keep your API key private.** Never expose it in frontend code, public repositories, or client-side JavaScript. Treat it like a password. ## Step 2: Understand the basics A few things to know before your first call. **Base URL** ```text theme={null} https://api.sorsa.io/v3 ``` **Authentication** Every request must include your API key in the `ApiKey` header: ```text theme={null} ApiKey: YOUR_API_KEY ``` **Response format** All endpoints return JSON. A successful request responds with HTTP `200`. **Rate limit** Every plan shares a limit of 20 requests per second. See [Rate Limits](https://docs.sorsa.io/rate-limits) for pacing and retry strategies. ## Step 3: Send your first GET request **Before running the examples:** install Python's `requests` package with `python -m pip install requests`. Run JavaScript on your backend using Node.js 18+ with `fetch`; save examples as `.mjs` for top-level `await`. Replace `YOUR_API_KEY` with your key. The profile and tweet responses below are illustrative. The quickest way to confirm your setup is to fetch a public profile with the `/info` endpoint. **cURL** ```bash theme={null} curl --request GET \ --url 'https://api.sorsa.io/v3/info?username=elonmusk' \ --header 'ApiKey: YOUR_API_KEY' ``` **Python** ```python theme={null} import requests response = requests.get( "https://api.sorsa.io/v3/info", params={"username": "elonmusk"}, headers={"ApiKey": "YOUR_API_KEY"}, timeout=30, ) response.raise_for_status() data = response.json() print(data["display_name"]) # Elon Musk print(data["followers_count"]) # 236021252 ``` **JavaScript** ```javascript theme={null} const response = await fetch( "https://api.sorsa.io/v3/info?username=elonmusk", { headers: { ApiKey: "YOUR_API_KEY" } } ); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); console.log(data.display_name); // Elon Musk console.log(data.followers_count); // 236021252 ``` **Example response** ```json theme={null} { "id": "44196397", "username": "elonmusk", "display_name": "Elon Musk", "description": "", "location": "", "profile_image_url": "https://pbs.twimg.com/profile_images/1234567890/avatar.jpg", "profile_background_image_url": "https://pbs.twimg.com/profile_banners/44196397/1700000000", "followers_count": 236021252, "followings_count": 1292, "tweets_count": 98479, "favourites_count": 214650, "media_count": 4374, "verified": true, "protected": false, "can_dm": false, "possibly_sensitive": false, "created_at": "2009-06-02T20:12:29Z", "bio_urls": [], "pinned_tweet_ids": ["2028500984977330453"] } ``` If you get JSON with user data back, your key works and you are ready to go. ## Step 4: Send your first POST request Many tweet and search endpoints use `POST` with a JSON body. Here is a tweet search with `/search-tweets`: **cURL** ```bash theme={null} curl --request POST \ --url 'https://api.sorsa.io/v3/search-tweets' \ --header 'ApiKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "query": "bitcoin" }' ``` **Python** ```python theme={null} import requests response = requests.post( "https://api.sorsa.io/v3/search-tweets", headers={"ApiKey": "YOUR_API_KEY"}, json={"query": "bitcoin"}, timeout=30, ) response.raise_for_status() data = response.json() for tweet in data.get("tweets", []): print(tweet["full_text"]) ``` **JavaScript** ```javascript theme={null} const response = await fetch("https://api.sorsa.io/v3/search-tweets", { method: "POST", headers: { ApiKey: "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ query: "bitcoin" }), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); data.tweets?.forEach((tweet) => console.log(tweet.full_text)); ``` **Example response (truncated)** ```json theme={null} { "tweets": [ { "id": "1782368585664626774", "full_text": "Bitcoin just crossed another milestone.", "created_at": "2024-01-15T10:30:00Z", "lang": "en", "likes_count": 200, "retweet_count": 50, "reply_count": 10, "view_count": 10000, "user": { "id": "44196397", "username": "elonmusk", "display_name": "Elon Musk", "followers_count": 236021252, "verified": true } } ], "next_cursor": "DAABCgABF7d..." } ``` To page through more results, pass the returned `next_cursor` in your next request. See [Pagination](https://docs.sorsa.io/pagination) for the full pattern. ## Step 5: Check your API usage Check how many requests you have left at any time with `/key-usage-info`: ```bash theme={null} curl --request GET \ --url 'https://api.sorsa.io/v3/key-usage-info' \ --header 'ApiKey: YOUR_API_KEY' ``` **Example response** ```json theme={null} { "key_requests": 100000, "remaining_requests": 94231, "total_requests": 5769, "valid_until": "2026-08-01T00:00:00Z" } ``` It is good practice to call this before running large batch jobs. You can also review your full history in the [Dashboard](https://api.sorsa.io/overview/usage). ## Common error codes | Code | Meaning | What to do | | :- | :- | :- | | 200 | OK | Request succeeded | | 400 | Bad request | Check your query parameters and request body | | 401 | Unauthorized | Your API key is missing or invalid; verify the `ApiKey` header | | 403 | Forbidden | Access to this resource is denied | | 404 | Not found | Check the endpoint URL or the resource ID | | 429 | Too many requests | You hit the rate limit; wait and retry | | 500 | Server error | Retry after a short delay; contact support if it persists | For full details and handling strategies, see the [Error Codes Reference](https://docs.sorsa.io/error-codes). ## Try it without code Prefer to click around first? Explore every endpoint in the [API Reference](https://docs.sorsa.io/api-reference-guide) or use the no-code [API Playground](https://api.sorsa.io/playground). Both let you send real requests and inspect responses before writing a line of code. ## Next steps * [Authentication](https://docs.sorsa.io/authentication) - Security best practices and header configuration * [Pagination](https://docs.sorsa.io/pagination) - Handling cursors and paginated responses * [Rate Limits](https://docs.sorsa.io/rate-limits) - Request pacing and retry strategies * [API Reference](https://docs.sorsa.io/api-reference-guide) - All available endpoints with schemas * [Use Cases Guide](https://docs.sorsa.io/use-cases-overview) - Real-world implementation patterns # Rate Limits Source: https://docs.sorsa.io/rate-limits Sorsa uses one simple rate limit: 20 requests per second on every plan. Here is how it works and how to stay within it. Sorsa enforces a single, universal rate limit to keep the service fast and stable for everyone. No per-endpoint tiers, no rolling windows, just one number to design around. ## The rule: 20 requests per second Every API key is limited to **20 requests per second**. That is the only rate limit. There are no per-endpoint limits, no 15-minute windows, no hourly resets, and no differences between plans. Coordinate the combined request rate of every worker using the same key, and handle `429` responses even when you pace requests locally. A few details worth knowing: * The limit applies **per API key**, not per IP address. If you have multiple keys, each gets its own 20 req/s allowance. * **Every request counts equally.** A single `/info` call and a `/tweet-info-bulk` call carrying 100 tweets both count as one request. * **It is not a sliding window.** The count resets every second. If you send 20 requests at `T+0.00`, you can send another 20 at `T+1.00`. ## What happens when you exceed the limit If you send more than 20 requests in one second, the API returns `429 Too Many Requests` for the excess. No data is lost and your key is not penalized. Wait until the next second and retry. There are no `x-ratelimit-remaining` or `x-ratelimit-reset` headers. Because the limit resets every second, tracking remaining credits in headers would add complexity without much practical value. ## How to stay within the limit For most workloads you will never approach 20 req/s during normal operation. If you are running batch jobs or processing large datasets, here are two simple approaches. ### Option 1: Fixed delay between requests In a single sequential worker, a pause of at least 50ms after each response limits how quickly it sends requests. Use a longer pause for headroom. For multiple workers sharing a key, use one shared rate limiter; independent 50ms sleeps do not enforce a combined 20 req/s limit. **Python** ```python theme={null} import time import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.sorsa.io/v3" usernames = ["elonmusk", "naval", "paulg", "vaborsh"] for username in usernames: response = requests.get( f"{BASE_URL}/info", params={"username": username}, headers={"ApiKey": API_KEY}, timeout=30, ) response.raise_for_status() print(response.json()["display_name"]) time.sleep(0.05) # 50ms between requests ``` **JavaScript** ```javascript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://api.sorsa.io/v3"; const usernames = ["elonmusk", "naval", "paulg", "vaborsh"]; for (const username of usernames) { const res = await fetch(`${BASE_URL}/info?username=${username}`, { headers: { ApiKey: API_KEY }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); console.log(data.display_name); await new Promise((r) => setTimeout(r, 50)); // 50ms between requests } ``` ### Option 2: Retry on 429 If you prefer to run at full speed and handle limits reactively, catch `429` responses and retry after a short pause. **Python** ```python theme={null} import time import requests def fetch_with_retry(url, headers, max_retries=3): for attempt in range(max_retries): response = requests.get(url, headers=headers, timeout=30) if response.status_code == 429: time.sleep(1) continue response.raise_for_status() return response.json() raise Exception("Rate limit: max retries exceeded") ``` **JavaScript** ```javascript theme={null} async function fetchWithRetry(url, headers, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const res = await fetch(url, { headers }); if (res.status === 429) { await new Promise((r) => setTimeout(r, 1000)); continue; } if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } throw new Error("Rate limit: max retries exceeded"); } ``` In practice, combining both works best: a small delay to avoid `429`s in the first place, plus retry logic as a safety net. ## Batch endpoints reduce the need for high throughput Before optimizing for speed, check whether batch endpoints can cut your total request count: * `/info-batch` fetches up to 100 user profiles in a single request * `/tweet-info-bulk` fetches up to 100 tweets in a single request One batch request counts the same as one regular request against both your rate limit and your quota. When you need data for many users or tweets, batching is almost always more efficient than firing individual requests in parallel. For more patterns like this, see the [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) guide. ## Need a higher limit? If your project needs sustained throughput above 20 req/s (for example, 100+ req/s for a real-time monitoring pipeline), reach out through [Talk to Sales](https://api.sorsa.io/talk-to-sales) or our [Discord](https://discord.com/invite/uwAefKCj7X) to discuss dedicated infrastructure and custom plans. ## Next steps * [Pagination](https://docs.sorsa.io/pagination) - Fetch large datasets efficiently within the rate limit * [Error Codes](https://docs.sorsa.io/error-codes) - Full reference for 400, 401, 403, 404, 429, and 500 responses # Real-Time Monitoring Source: https://docs.sorsa.io/real-time-monitoring Detect new tweets from specific accounts, track keyword mentions as they happen, and feed live X data into your application. This guide shows how to build a near-real-time monitoring pipeline on top of the Sorsa API using a pull-based polling pattern. Detection latency depends on the polling interval, API response time, and when a post becomes available in the selected feed or search index. Build the monitor around checkpoints, pagination, and deduplication rather than assuming every newly posted tweet appears in the next response. > **Free to prototype:** Every Sorsa endpoint is available on your first 100 requests: one-time, no credit card, no expiry. That is enough to stand up any of the monitors below, confirm it detects live tweets, and validate your Slack or Discord routing before choosing a plan. Because polling is request-heavy, size a paid plan to your poll interval using the usage table further down. > **Note:** For a fuller walkthrough with additional architecture patterns and end-to-end examples, see [Real-Time Twitter Monitoring with REST API](https://api.sorsa.io/blog/real-time-twitter-monitoring) on the blog. *** ## How Polling-Based Monitoring Works There are two ways to get data from a social platform: push-based (streaming, webhooks) or pull-based (polling). The Sorsa API uses polling. The pattern has four steps: 1. **Poll an endpoint** at a regular interval (1 to 30 seconds). 2. **Compare results** against previously seen tweet IDs to identify what is new. 3. **Process new tweets:** send alerts, store, fan out to Slack, Discord, etc. 4. **Repeat.** Tweet IDs encode creation time and can be compared using Python integers or JavaScript BigInt. A highest-seen ID is a useful checkpoint, but posts may appear late or out of order. For production, revisit an overlapping time window and deduplicate by ID. Resuming after a crash requires explicitly persisting and loading your checkpoint. ### Choosing the Right Endpoint | What you want to monitor | Endpoint | Method | Why this one | | :- | :- | :- | :- | | A single account | `/user-tweets` | POST | Returns the latest tweets from one user's timeline | | Up to 5,000 accounts at once | `/list-tweets` | GET | One request covers all members of an X List | | A keyword or hashtag | `/search-tweets` | POST | Full search operator support, chronological ordering with `order: latest` | | @mentions of an account | `/mentions` | POST | Purpose-built for mention tracking with engagement filters | *** ## Level 1: Monitor a Single Account The simplest case. The loop polls the first page of `/user-tweets` and emits tweets newer than the last seen ID. On its first successful poll it sets a baseline without emitting existing tweets. This is a development example: it can miss posts if more than one page arrives between polls or while the process is stopped. ### Python ```python theme={null} import requests import time API_KEY = "YOUR_API_KEY" USERNAME = "elonmusk" POLL_INTERVAL = 5 # seconds URL = "https://api.sorsa.io/v3/user-tweets" HEADERS = {"ApiKey": API_KEY, "Content-Type": "application/json"} last_seen_id = None print(f"Monitoring @{USERNAME}...") while True: try: resp = requests.post(URL, headers=HEADERS, json={"username": USERNAME}, timeout=30) resp.raise_for_status() tweets = resp.json().get("tweets", []) if tweets: # Snowflake IDs arrive as strings. Use the highest (newest) ID in the # batch; this stays correct even if a pinned tweet appears first. top_id = max(int(t["id"]) for t in tweets) if last_seen_id is None: last_seen_id = top_id print(f"Baseline set: {last_seen_id}") else: new_tweets = [t for t in tweets if int(t["id"]) > last_seen_id] for tweet in reversed(new_tweets): # oldest first print(f"[NEW] @{USERNAME}: {tweet['full_text'][:140]}") if new_tweets: last_seen_id = top_id except requests.exceptions.RequestException as e: print(f"Error: {e}") time.sleep(POLL_INTERVAL * 2) continue time.sleep(POLL_INTERVAL) ``` ### JavaScript ```javascript theme={null} const API_KEY = "YOUR_API_KEY"; const USERNAME = "elonmusk"; const POLL_INTERVAL = 5000; let lastSeenId = null; console.log(`Monitoring @${USERNAME}...`); while (true) { try { const resp = await fetch("https://api.sorsa.io/v3/user-tweets", { method: "POST", headers: { "ApiKey": API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ username: USERNAME }), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const tweets = (await resp.json()).tweets || []; if (tweets.length > 0) { // BigInt avoids precision loss on 64-bit Snowflake IDs. // Use the highest ID in the batch (robust if a pinned tweet appears first). let topId = 0n; for (const t of tweets) { const id = BigInt(t.id); if (id > topId) topId = id; } if (lastSeenId === null) { lastSeenId = topId; console.log(`Baseline set: ${lastSeenId}`); } else { const newTweets = tweets.filter((t) => BigInt(t.id) > lastSeenId); for (const t of [...newTweets].reverse()) { console.log(`[NEW] @${USERNAME}: ${t.full_text.slice(0, 140)}`); } if (newTweets.length) lastSeenId = topId; } } } catch (err) { console.error(`Error: ${err.message}`); await new Promise((r) => setTimeout(r, POLL_INTERVAL * 2)); continue; } await new Promise((r) => setTimeout(r, POLL_INTERVAL)); } ``` This scales poorly for many accounts. Monitoring 50 accounts means 50 separate loops and 50x the API requests. That is where X Lists come in. *** ## Level 2: Monitor Many Accounts with a Single Request X Lists group up to 5,000 accounts. `/list-tweets` returns the merged latest tweets across all members in one API call. This is the default pattern for production multi-account monitoring. See [Lists & Communities](https://docs.sorsa.io/lists-and-communities) for more. ### Step 1: Create a Public X List 1. Go to [X Lists](https://x.com/i/lists) and create a list. 2. Add the accounts to monitor (up to 5,000). 3. Set the list to **Public**. Private lists are not accessible via the API. 4. Copy the **List ID** from the URL. For `https://x.com/i/lists/1234567890` the ID is `1234567890`. ### Step 2: Poll the List ```python theme={null} import requests import time API_KEY = "YOUR_API_KEY" LIST_ID = "YOUR_LIST_ID" POLL_INTERVAL = 5 URL = f"https://api.sorsa.io/v3/list-tweets?list_id={LIST_ID}" HEADERS = {"ApiKey": API_KEY, "Accept": "application/json"} def monitor_list(callback, interval=POLL_INTERVAL): """Poll an X List and call `callback` for each new tweet detected.""" last_seen_id = None print(f"Monitoring List {LIST_ID} (interval: {interval}s)") while True: try: resp = requests.get(URL, headers=HEADERS, timeout=10) resp.raise_for_status() tweets = resp.json().get("tweets", []) if not tweets: time.sleep(interval) continue top_id = max(int(t["id"]) for t in tweets) if last_seen_id is None: last_seen_id = top_id print(f"Baseline set: {last_seen_id}") else: new_tweets = [t for t in tweets if int(t["id"]) > last_seen_id] if new_tweets: for tweet in reversed(new_tweets): callback(tweet) last_seen_id = top_id except requests.exceptions.RequestException as e: print(f"Request error: {e}. Retrying in {interval * 2}s") time.sleep(interval * 2) continue time.sleep(interval) def on_new_tweet(tweet): user = tweet["user"] print(f"[NEW] @{user['username']}: {tweet['full_text'][:120]}") print( f" Likes: {tweet.get('likes_count', 0)} | " f"RTs: {tweet.get('retweet_count', 0)} | " f"Views: {tweet.get('view_count', 'N/A')}\n" ) if __name__ == "__main__": monitor_list(on_new_tweet) ``` **Efficiency gain.** Polling 50 accounts individually at a 10-second interval costs 50 x 8,640 = 432,000 requests per day. The same 50 accounts in one List polled at 10 seconds costs 8,640 requests per day. A 50x reduction. For more patterns like this, see [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage). > `/list-tweets` returns up to 20 tweets per page. If list members tweet faster than that within one poll interval, drop the interval to 2 to 3 seconds, or paginate via `next_cursor` until reaching a previously seen ID. *** ## Level 3: Monitor a Keyword or Hashtag Instead of tracking accounts, poll `/search-tweets` with `order: "latest"` for chronological results matching a query. ```python theme={null} import requests import time API_KEY = "YOUR_API_KEY" QUERY = '("your brand" OR @yourbrand) lang:en' POLL_INTERVAL = 10 URL = "https://api.sorsa.io/v3/search-tweets" HEADERS = {"ApiKey": API_KEY, "Content-Type": "application/json"} def monitor_keyword(query, callback, interval=10): last_seen_id = None print(f"Monitoring: {query} (interval: {interval}s)") while True: try: resp = requests.post( URL, headers=HEADERS, json={"query": query, "order": "latest"}, timeout=10, ) resp.raise_for_status() tweets = resp.json().get("tweets", []) if tweets: top_id = max(int(t["id"]) for t in tweets) if last_seen_id is None: last_seen_id = top_id print(f"Baseline set: {last_seen_id}") else: new_tweets = [t for t in tweets if int(t["id"]) > last_seen_id] for tweet in reversed(new_tweets): callback(tweet) if new_tweets: last_seen_id = top_id except requests.exceptions.RequestException as e: print(f"Error: {e}") time.sleep(interval * 2) continue time.sleep(interval) monitor_keyword(QUERY, on_new_tweet, interval=10) ``` Any [search operator](https://docs.sorsa.io/search-operators) works in the query string. To monitor high-engagement English mentions of your brand and exclude retweets: ```python theme={null} monitor_keyword('"your brand" min_faves:10 lang:en -filter:retweets', on_new_tweet) ``` *** ## Routing New Tweets to Slack, Discord, or Any HTTP Endpoint The polling loop is the producer; the callback decides what happens to each new tweet. Because the callback is just a function, the same monitor can route to anything that speaks HTTP. ### Slack via Incoming Webhook ```python theme={null} import requests SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK" def send_to_slack(tweet): user = tweet["user"] text = ( f"*New tweet from @{user['username']}*\n" f"{tweet['full_text']}\n" f"Likes: {tweet.get('likes_count', 0)} | " f"RTs: {tweet.get('retweet_count', 0)} | " f"Views: {tweet.get('view_count', 'N/A')}\n" f"https://x.com/{user['username']}/status/{tweet['id']}" ) requests.post(SLACK_WEBHOOK_URL, json={"text": text}) # Plug into any monitor: monitor_list(send_to_slack) # or: monitor_keyword("bitcoin lang:en min_faves:50", send_to_slack) ``` ### Discord ```python theme={null} DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR/WEBHOOK" def send_to_discord(tweet): user = tweet["user"] content = ( f"**@{user['username']}** just tweeted:\n" f"{tweet['full_text']}\n" f"https://x.com/{user['username']}/status/{tweet['id']}" ) requests.post(DISCORD_WEBHOOK_URL, json={"content": content}) ``` ### Telegram ```python theme={null} TELEGRAM_BOT_TOKEN = "YOUR_BOT_TOKEN" TELEGRAM_CHAT_ID = "YOUR_CHAT_ID" def send_to_telegram(tweet): user = tweet["user"] text = ( f"New tweet from @{user['username']}\n\n" f"{tweet['full_text']}\n\n" f"https://x.com/{user['username']}/status/{tweet['id']}" ) requests.post( f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage", json={"chat_id": TELEGRAM_CHAT_ID, "text": text}, ) ``` ### Any Custom HTTP Endpoint ```python theme={null} def send_to_internal_api(tweet): requests.post( "https://internal.example.com/events/twitter", json={ "tweet_id": tweet["id"], "username": tweet["user"]["username"], "text": tweet["full_text"], "metrics": { "likes": tweet.get("likes_count", 0), "retweets": tweet.get("retweet_count", 0), "views": tweet.get("view_count", 0), }, "url": f"https://x.com/{tweet['user']['username']}/status/{tweet['id']}", }, headers={"Authorization": "Bearer YOUR_INTERNAL_TOKEN"}, timeout=5, ) ``` *** ## API Usage Calculator The table assumes one page per polling cycle, a fixed schedule, and no retries. Multiply by the number of monitors and add extra pages and retry attempts. The sample loops sleep after each response, so their actual cycle duration also includes network and processing time. | Interval | Req/Hour | Req/Day | Req/Month (30d) | | :- | :- | :- | :- | | 1 second | 3,600 | 86,400 | 2,592,000 | | 5 seconds | 720 | 17,280 | 518,400 | | 10 seconds | 360 | 8,640 | 259,200 | | 30 seconds | 120 | 2,880 | 86,400 | | 1 minute | 60 | 1,440 | 43,200 | Choose an interval based on the latency your application can tolerate and the activity of the feed. A shorter interval increases requests but does not guarantee immediate availability in search results. The free 100 requests are enough to prototype and validate a monitor end to end. For sustained monitoring, match a plan to the monthly volume in the table above: a single loop at 30 to 60 seconds fits within Pro (100,000 requests per month), and a single loop at 10 seconds fits within Enterprise (500,000 requests per month). These figures are per monitor, so running several in parallel multiplies the total; size your plan to the combined volume. See [Pricing](https://api.sorsa.io/pricing) for full plan details. > For rate limits or volumes above the standard plans, [talk to sales](https://api.sorsa.io/talk-to-sales) for a custom quota, or ask in [Discord](https://discord.com/invite/uwAefKCj7X). *** ## Production Hardening The examples above work for development. For production, address these five concerns. ### 1. Persist `last_seen_id` Across Restarts If the script crashes and restarts without remembering its checkpoint, it either reprocesses old tweets (duplicate alerts) or silently skips the gap. Store the last seen ID in a file, database, or Redis. ```python theme={null} import json import os STATE_FILE = "monitor_state.json" def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE) as f: return json.load(f).get("last_seen_id") return None def save_state(last_seen_id): with open(STATE_FILE, "w") as f: json.dump({"last_seen_id": last_seen_id}, f) ``` Replace the monitor's initial `last_seen_id = None` with `last_seen_id = load_state()`. After processing or durably queueing all pages for a cycle, save the new checkpoint. Do not advance it after a failed page or failed delivery. On restart, paginate through the gap before accepting a newer checkpoint. ### 2. Exponential Backoff for Errors Network issues, rate limits (HTTP 429), and transient API errors will happen. Back off gradually with a cap rather than retrying immediately. See [Error Codes](https://docs.sorsa.io/error-codes) for the full reference. ```python theme={null} retry_delay = POLL_INTERVAL MAX_DELAY = 60 while True: try: resp = requests.get(URL, headers=HEADERS, timeout=10) if resp.status_code == 429: print(f"Rate limited. Backing off {retry_delay}s") time.sleep(retry_delay) retry_delay = min(retry_delay * 2, MAX_DELAY) continue resp.raise_for_status() retry_delay = POLL_INTERVAL # reset on success # process tweets except requests.exceptions.RequestException as e: print(f"Error: {e}") time.sleep(retry_delay) retry_delay = min(retry_delay * 2, MAX_DELAY) continue time.sleep(POLL_INTERVAL) ``` ### 3. Separate Polling from Processing Do not run expensive operations (NLP, database writes, external API calls) synchronously inside the polling loop. If a downstream system slows down, the loop falls behind schedule. Push new tweets into a queue and process them in a separate worker. ```python theme={null} from collections import deque import threading tweet_queue = deque() def polling_loop(): """Fast loop: poll and enqueue. No heavy work here.""" # Standard polling code, but instead of calling callback(tweet): # tweet_queue.append(tweet) pass def processing_worker(): """Separate thread: dequeue and dispatch.""" while True: if tweet_queue: tweet = tweet_queue.popleft() send_to_slack(tweet) save_to_database(tweet) else: time.sleep(0.1) threading.Thread(target=processing_worker, daemon=True).start() polling_loop() ``` For heavier workloads, replace the in-memory deque with Redis, RabbitMQ, SQS, or any message broker the stack already uses. ### 4. Monitor the Monitor Log each poll cycle: timestamp, new tweet count, response time, errors. Alert if the monitor has not completed a successful poll in the last N minutes; silent failures cause invisible data gaps in alerting pipelines. Operational status of the API is available at the [Sorsa Status Page](https://uptime.sorsa.io/status/v3). ### 5. Handle Edge Cases **Page overflow and late arrivals:** continue through `next_cursor` until you have covered the interval since the last checkpoint. Keep a small overlap and deduplicate stored IDs so delayed results are not discarded solely because their IDs are older. The first-page examples above do not implement this backfill. **Callback delivery:** check webhook response status and use bounded retries or a durable queue. A successful Sorsa request does not imply that Slack, Discord, or your database accepted the event. * **Deleted tweets:** if a tweet is deleted between fetch and callback, the URL will 404. Treat as expected. * **Protected accounts:** if a tracked user goes private, `/user-tweets` returns an empty list. Log and continue. * **Pinned tweets:** the first tweet in a `/user-tweets` response is often the pinned one, not the most recent. Do not use `tweets[0]` as the newest ID; take `max(int(t["id"]) for t in tweets)` instead (as the examples above do), or sort by `created_at`. * **Retweets:** `tweet["retweeted_status"]` is populated for retweets. Decide whether to include them or filter out. * **Reply restrictions:** `is_replies_limited` indicates the author restricted replies; useful signal for some monitoring use cases. *** ## Next Steps * [Search Operators](https://docs.sorsa.io/search-operators): advanced filters to reduce noise in keyword-based monitoring. * [Track Mentions](https://docs.sorsa.io/search-mentions): dedicated endpoint for @mentions with engagement filters. * [Rate Limits](https://docs.sorsa.io/rate-limits): handling 429 errors and request patterns. * [Pagination](https://docs.sorsa.io/pagination): backfill historical data alongside real-time monitoring. * [API Reference](https://docs.sorsa.io/api-reference-guide): full specification for `/list-tweets`, `/user-tweets`, `/search-tweets`, and all Sorsa API endpoints. # Response Format Source: https://docs.sorsa.io/response-format # API Response Format: User and Tweet Object Schemas Sorsa API returns all data as JSON. This page documents the response structure, core data objects, field types, and pagination model so you know exactly what to expect from every endpoint. *** ## Conventions Before diving into specific objects, here are the formatting rules that apply across the entire API. **IDs are strings.** All X/Twitter IDs (`id`, `conversation_id_str`, `in_reply_to_tweet_id`, etc.) are returned as strings, not integers. X uses Snowflake IDs, which are 64-bit numbers that exceed JavaScript's `Number.MAX_SAFE_INTEGER`. Returning them as strings prevents silent precision loss in browsers, Node.js, and any language that represents JSON numbers as floating-point. **Core timestamps are ISO 8601 strings.** User and Tweet `created_at` fields use values such as `2026-03-06T12:00:00Z`. Consult the endpoint schema for other date fields; do not assume a verification result or relationship date has the same format. **Booleans are strict.** Status flags like `verified`, `is_reply`, `protected`, and `can_dm` are always `true` or `false`, never `0`/`1` or `"true"`/`"false"`. **Null and missing fields.** Handle optional fields defensively: distinguish a missing value from a measured zero or `false`. For optional arrays such as `bio_urls` and `pinned_tweet_ids`, normalize null or missing values to an empty list when iterating (`user.get("bio_urls") or []` in Python; `user.bio_urls ?? []` in JavaScript). Compact response models omit fields present on a full User object. *** ## Response wrappers Endpoints that return a single object (like `/info` or `/tweet-info`) return the object directly at the top level. Endpoints that return lists use one of the following wrapper structures. **UsersResponse** - used by `/followers`, `/follows`, `/verified-followers`, `/retweeters`, `/search-users`, `/list-members`, and `/list-followers`. `/community-members` uses the same wrapper keys but returns compact `CommunityUser` objects. ```text theme={null} { "users": [ ... ], "next_cursor": "abc123" } ``` **TweetsResponse** - used by `/user-tweets`, `/comments`, `/quotes`, `/search-tweets`, `/mentions`, `/list-tweets`, `/community-tweets`, `/community-search-tweets` ```text theme={null} { "tweets": [ ... ], "next_cursor": "abc123" } ``` **FollowersResponse** - used by `/new-followers-7d`, `/new-following-7d`, and `/top-following`. `/top-followers` uses `TopFollowersResponse`, with compact profiles and a `score` field. ```text theme={null} { "users": [ ... ] } ``` Note: FollowersResponse does not include `next_cursor`. It returns the full result set in a single response. *** ## The User object The User object represents an X/Twitter account profile. It is returned directly by `/info`, as an array element in list endpoints, and nested inside every Tweet object as the `user` field. ```text theme={null} { "id": "44196397", "username": "elonmusk", "display_name": "Elon Musk", "description": "Mars & Cars, Chips & Dips", "location": "Earth", "created_at": "2009-06-02T20:12:29Z", "followers_count": 236021252, "followings_count": 1292, "favourites_count": 214650, "tweets_count": 98479, "media_count": 4374, "profile_image_url": "https://pbs.twimg.com/profile_images/.../photo_normal.jpg", "profile_background_image_url": "https://pbs.twimg.com/profile_banners/44196397/...", "bio_urls": ["https://example.com"], "pinned_tweet_ids": ["2028500984977330453"], "verified": true, "can_dm": false, "protected": false, "possibly_sensitive": false } ``` | Field | Type | Description | | :- | :- | :- | | `id` | string | Permanent numeric user ID (Snowflake) | | `username` | string | Current handle / screen name (without @) | | `display_name` | string | Public profile name | | `description` | string | Account bio | | `location` | string | User-specified location (free text) | | `created_at` | string | Account creation timestamp | | `followers_count` | integer | Number of followers | | `followings_count` | integer | Number of accounts followed | | `favourites_count` | integer | Total likes given by this account | | `tweets_count` | integer | Total tweets posted | | `media_count` | integer | Total media items posted | | `profile_image_url` | string | Avatar image URL | | `profile_background_image_url` | string | Banner image URL | | `bio_urls` | array of strings or null | URLs from the bio section | | `pinned_tweet_ids` | array of strings or null | IDs of pinned tweets | | `verified` | boolean | Verification status (Blue, Gold, or Gray checkmark) | | `can_dm` | boolean | Whether DMs are open | | `protected` | boolean | Whether the account is private | | `possibly_sensitive` | boolean | Whether the account is flagged as sensitive | *** ## The Follower object The Follower object extends the User object with one additional field. It is returned by `/new-followers-7d`, `/new-following-7d`, and `/top-following`. `/top-followers` returns a separate compact model with `score`. | Additional field | Type | Description | | :- | :- | :- | | `followerDate` | string | Date when the follow relationship was recorded | *** ## The Tweet object The Tweet object contains the full content, metadata, engagement metrics, and nested relationships for a single tweet. It is returned directly by `/tweet-info` and as array elements in tweet list endpoints. ```text theme={null} { "id": "1234567890123456789", "full_text": "This is the complete tweet text including mentions and links", "created_at": "2026-03-06T12:00:00Z", "lang": "en", "bookmark_count": 42, "likes_count": 1500, "quote_count": 23, "reply_count": 89, "retweet_count": 312, "view_count": 250000, "conversation_id_str": "1234567890123456789", "in_reply_to_tweet_id": null, "in_reply_to_username": null, "is_reply": false, "is_quote_status": false, "is_replies_limited": false, "entities": [ ... ], "user": { ... }, "quoted_status": null, "retweeted_status": null } ``` **Content fields** | Field | Type | Description | | :- | :- | :- | | `id` | string | Unique tweet ID (Snowflake) | | `full_text` | string | Complete tweet text | | `created_at` | string | Publication timestamp | | `lang` | string | Detected language code (e.g., "en", "ja", "es") | **Engagement metrics** | Field | Type | Description | | :- | :- | :- | | `likes_count` | integer | Total likes | | `retweet_count` | integer | Total retweets | | `reply_count` | integer | Total replies | | `quote_count` | integer | Total quote tweets | | `view_count` | integer | Total impressions | | `bookmark_count` | integer | Total bookmarks | **Thread and reply context** | Field | Type | Description | | :- | :- | :- | | `conversation_id_str` | string | ID of the root tweet in the thread | | `in_reply_to_tweet_id` | string or null | ID of the tweet this is replying to | | `in_reply_to_username` | string or null | Username of the account being replied to | | `is_reply` | boolean | Whether this tweet is a reply | | `is_quote_status` | boolean | Whether this tweet quotes another tweet | | `is_replies_limited` | boolean | Whether replies are restricted by the author | **Nested objects** | Field | Type | Description | | :- | :- | :- | | `user` | User object | Full profile of the tweet author | | `entities` | array of TweetEntity | Media and link attachments | | `quoted_status` | Tweet object or null | The quoted tweet (full object, recursive) | | `retweeted_status` | Tweet object or null | The original retweeted tweet (full object, recursive) | The `quoted_status` and `retweeted_status` fields contain complete Tweet objects, including their own `user`, `entities`, and engagement metrics. This means you get all related data in a single API call without needing to make follow-up requests. *** ## The TweetEntity object Each item in the `entities` array represents a media attachment or embedded link within a tweet. ```text theme={null} { "type": "photo", "link": "https://pbs.twimg.com/media/..._large.jpg", "preview": "https://pbs.twimg.com/media/..._small.jpg" } ``` | Field | Type | Description | | :- | :- | :- | | `type` | string | Entity type (e.g., `photo`, `video`, `animated_gif`) | | `link` | string | Direct URL to the full-resolution asset | | `preview` | string | Preview text or thumbnail URL | *** ## Cursor-based pagination Paginated list endpoints use cursor-based pagination. Batch lookups and the crypto analytics lists described above do not use a cursor. This approach is more reliable than offset-based pagination for dynamic feeds where new content is constantly being added. **How it works:** 1. Send your initial request without a cursor. 2. The response includes a `next_cursor` field along with the data. 3. To fetch the next page, include the `next_cursor` value in your next request. 4. When `next_cursor` is `null` or absent, you have reached the end of the data. **GET endpoints** accept the cursor as a query parameter: ```text theme={null} curl --request GET \ --url 'https://api.sorsa.io/v3/followers?username=elonmusk&next_cursor=abc123' \ --header 'ApiKey: YOUR_API_KEY' ``` **POST endpoints** accept the cursor in the JSON body: ```text theme={null} curl --request POST \ --url 'https://api.sorsa.io/v3/search-tweets' \ --header 'ApiKey: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{"query": "bitcoin", "next_cursor": "abc123"}' ``` **Python example: paginating through all followers** ```text theme={null} import time import requests API_KEY = "YOUR_API_KEY" all_users = [] cursor = None while True: params = {"username": "elonmusk"} if cursor: params["next_cursor"] = cursor response = requests.get( "https://api.sorsa.io/v3/followers", params=params, headers={"ApiKey": API_KEY}, timeout=30, ) response.raise_for_status() data = response.json() all_users.extend(data["users"]) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.05) # respect rate limit print(f"Fetched {len(all_users)} followers") ``` For a deeper look at pagination strategies and performance tips, see [Pagination](https://docs.sorsa.io/pagination). *** ## Next steps * [Pagination](https://docs.sorsa.io/pagination) - Advanced pagination patterns and best practices * [Error Codes](https://docs.sorsa.io/error-codes) - How error responses are structured * [API Reference](https://docs.sorsa.io/api-reference-guide) - Full endpoint schemas with request and response examples # Track Mentions Source: https://docs.sorsa.io/search-mentions The `/mentions` endpoint returns tweets that mention a specific handle. Use it to monitor brand mentions, route support inquiries, measure campaign engagement, and watch competitive activity. It offers the richest filter set of any Sorsa search endpoint: engagement thresholds and date bounds are first-class parameters in the request body. Results are paginated at up to 20 tweets per page. > **Note:** For a full walkthrough with production code, multi-channel monitoring patterns, and competitive analysis workflows, see [How to Track Twitter Mentions With API](https://api.sorsa.io/blog/twitter-mentions-api) on the blog. ## Quick Start ```bash theme={null} curl -X POST https://api.sorsa.io/v3/mentions \ -H "ApiKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "AppleSupport", "order": "latest", "min_likes": 10, "since_date": "2026-03-01" }' ``` > **Tip:** Prefer a UI? Run `/mentions` without writing code in the [API Playground](https://api.sorsa.io/playground). Every account starts with 100 free requests, no card required. *** ## Endpoint Reference ```text theme={null} POST https://api.sorsa.io/v3/mentions ``` | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `query` | string | Yes | The handle to track, without the @ symbol. Example: `"elonmusk"`. | | `order` | string | No | `"latest"` (default, newest first) or `"popular"` (engagement-ranked). | | `since_date` | string | No | Start date in `YYYY-MM-DD` format. | | `until_date` | string | No | End date in `YYYY-MM-DD` format. | | `min_likes` | integer | No | Minimum likes a mention must have. | | `min_retweets` | integer | No | Minimum retweets a mention must have. | | `min_replies` | integer | No | Minimum replies a mention must have. | | `next_cursor` | string | No | Pagination cursor from a previous response. | Each call counts as a single request against your quota, whether it returns one mention or twenty. *** ## Response ```json theme={null} { "tweets": [ { "id": "2031847200012345678", "full_text": "@AppleSupport My iPhone keeps restarting after the latest update. Anyone else?", "created_at": "2026-03-08T14:22:31Z", "lang": "en", "likes_count": 47, "retweet_count": 12, "reply_count": 8, "quote_count": 2, "view_count": 15200, "is_reply": false, "is_quote_status": false, "user": { "id": "9876543210", "username": "frustrated_user", "display_name": "Alex", "followers_count": 1240, "verified": false } } ], "next_cursor": "DAABCgABGSmiaxkA..." } ``` Each mention carries full engagement metrics and the embedded author profile. The `user` object holds the complete profile (trimmed above for readability), and all timestamps are ISO 8601. When `next_cursor` is present, more pages are available; when it is `null` or absent, you have reached the end. See [Pagination](https://docs.sorsa.io/pagination) for handling patterns and [Response Format](https://docs.sorsa.io/response-format) for the full field list. *** ## /mentions vs /search-tweets The two endpoints solve different problems: * Use `/mentions` for posts that tag a specific handle (`@brand`). It catches @-tags, replies, and references, with `min_likes`, `min_retweets`, `min_replies`, `since_date`, and `until_date` available as first-class parameters. * Use [`/search-tweets`](https://docs.sorsa.io/search-tweets) for keyword matches that do not include the handle. Searching `"Nike" -from:Nike lang:en` catches posts that name the brand in prose. Reach for it when you need Boolean logic, media filters, or other operators beyond what `/mentions` supports. For complete brand coverage, run both endpoints in parallel and deduplicate by tweet ID. *** ## Common Patterns ### Filter by engagement Pull only mentions that have reached an audience. Useful for reputation dashboards and PR monitoring. ```python theme={null} import requests API_KEY = "YOUR_API_KEY" URL = "https://api.sorsa.io/v3/mentions" body = {"query": "nike", "order": "popular", "min_likes": 100} resp = requests.post(URL, headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json=body) resp.raise_for_status() mentions = resp.json().get("tweets", []) ``` ### Pull every mention (support queue) Drop the engagement filters and sort chronologically to catch every mention, including zero-engagement ones. ```python theme={null} body = {"query": "YourBrandSupport", "order": "latest", "since_date": "2026-05-10"} resp = requests.post(URL, headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json=body) ``` ### Date-windowed campaign analysis Bound mentions to a campaign window with `since_date` and `until_date`, then loop through `next_cursor` until it runs out. ```python theme={null} import time def all_mentions(handle, since, until, max_pages=50): out, cursor = [], None for _ in range(max_pages): body = {"query": handle, "order": "latest", "since_date": since, "until_date": until} if cursor: body["next_cursor"] = cursor resp = requests.post(URL, headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json=body) resp.raise_for_status() data = resp.json() out.extend(data.get("tweets", [])) cursor = data.get("next_cursor") if not cursor: break time.sleep(0.1) return out ``` ### Polling for new mentions Track the newest tweet ID across iterations so you only surface mentions you have not seen before. Compare IDs numerically, since they are returned as strings. ```python theme={null} last_seen_id = None while True: resp = requests.post(URL, headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json={"query": "yourbrand", "order": "latest"}) resp.raise_for_status() tweets = resp.json().get("tweets", []) if tweets and last_seen_id is None: last_seen_id = max((t["id"] for t in tweets), key=int) elif tweets: new = [t for t in tweets if int(t["id"]) > int(last_seen_id)] # handle `new` mentions if new: last_seen_id = max((t["id"] for t in new), key=int) time.sleep(15) ``` This first-page example sets a baseline on startup and does not emit the existing page. On busy feeds, paginate through the gap before advancing the checkpoint; keep an overlap and deduplicate IDs to handle late results. Persist `last_seen_id` to disk or Redis so the loop survives restarts, and wrap the request in `try`/`except` with a backoff so a transient error does not kill the process. For the full real-time pattern with deduplication and backoff, see [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring). *** ## Common Pitfalls * **`min_likes` set too high for support use cases.** A bug report with 2 likes matters more than a meme with 500. For support queues, set `min_likes` to 0 and rely on keyword routing instead. * **Single-page reads on high-volume handles.** One request returns up to 20 mentions. For brands with hundreds of daily mentions, always paginate through `next_cursor`. Each page is one request against your quota, so budget accordingly. * **Treating `/mentions` as full coverage.** It only catches @-tagged posts. Combine it with `/search-tweets` to catch untagged brand references. * **Aggressive polling on low-volume accounts.** Match the polling interval to mention volume: every 15 seconds for high-traffic brands, every minute or two for smaller accounts. The rate limit is 20 req/s across all plans ([Rate Limits](https://docs.sorsa.io/rate-limits)). * **Not persisting state across restarts.** Without a durable checkpoint, a restarted monitor either re-alerts on old mentions or skips the gap. *** ## Next Steps * [Search Tweets](https://docs.sorsa.io/search-tweets) - keyword-based search for untagged mentions. * [Search Operators](https://docs.sorsa.io/search-operators) - operators for complex queries (media, geo, Boolean). * [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring) - polling architecture with deduplication and backoff. * [Historical Data](https://docs.sorsa.io/historical-data) - retrieving older tweets by date range. * [API Reference](https://docs.sorsa.io/api-reference/search/search-mentions) - full endpoint specification. # Search Operators Source: https://docs.sorsa.io/search-operators X search operators are special keywords and symbols that filter tweets by author, date, engagement, media type, language, location, and other attributes. Most operators on this page work in the `query` field of the [Search Tweets](https://docs.sorsa.io/api-reference/search/search-tweets) endpoint and on x.com. A few are marked UI only: those depend on a signed-in account (your follows, your location, your network) and do not work through the API. > **Note:** For a complete guide with copy-paste recipes, production code examples (Python and JavaScript with pagination), and a side-by-side comparison against the official X API v2 operator set, see the [Twitter Search Operators Complete Cheat Sheet](https://api.sorsa.io/blog/twitter-search-operators) on the blog. ## Syntax basics * Spaces between terms act as **implicit AND** * `OR` must be **uppercase** * Use a leading dash (`-`) to **exclude** a term, phrase, or operator * Group expressions with **parentheses** * Wrap exact phrases in **double quotes** Operators can be combined freely up to roughly 22-23 operators per query. AND binds tighter than OR: `cat OR black dog` is evaluated as `cat OR (black dog)`. Use parentheses to eliminate ambiguity. ## Free Visual Query Builder If you don't want to assemble operator strings by hand, use the [Sorsa Search Builder](https://api.sorsa.io/playground/search-builder). It is a free, no-login tool with a graphical interface where you toggle filters and preview the generated query string before integrating it into your code. *** ## 1. Keyword and Boolean Logic | Operator | Description | Example | | :- | :- | :- | | `keyword keyword` | Tweets containing both terms (implicit AND). | `nasa esa` | | `keyword OR keyword` | Tweets containing either term. `OR` must be uppercase. | `bitcoin OR ethereum` | | `"exact phrase"` | Tweets containing the exact phrase. Also prevents auto-correction. | `"state of the art"` | | `-keyword` | Excludes tweets containing the term, phrase, or operator. | `crypto -scam` | | `( )` | Groups terms for complex Boolean logic. | `(AI OR "machine learning") lang:en` | | `"word * word"` | Wildcard inside a quoted phrase. The `*` replaces any single word. | `"this is the * time"` | | `+word` | Forces exact match, preventing auto-correction and stemming. | `+radiooooo` | | `#hashtag` | Matches a specific hashtag. | `#tgif` | | `$cashtag` | Matches a stock or crypto symbol. | `$TSLA` | Plurals match their singulars and vice versa. Operators match against tweet body, the author's display name, screen name, and expanded URLs inside the tweet. *** ## 2. User and Account Filters | Operator | Description | Example | | :- | :- | :- | | `from:username` | Tweets sent by a specific account (without the @). | `from:elonmusk` | | `to:username` | Tweets that are replies to a specific account. | `to:openai` | | `@username` | Tweets that mention a specific account anywhere in the text. | `@sorsa_app` | | `list:ID` | Tweets from members of a public X List. Use the numeric List ID from the URL. | `list:715919216927322112` | | `filter:verified` | Only from legacy verified accounts (pre-2023 blue checkmarks). | `AI filter:verified` | | `filter:blue_verified` | Only from X Premium (paid Blue) subscribers. | `crypto filter:blue_verified` | | `filter:follows` | Only from accounts you follow. Web UI only, cannot be negated. | `filter:follows` | | `filter:social` | From your algorithmically expanded network. Works on "Top" results, not "Latest". | `filter:social` | *** ## 3. Engagement Gating | Operator | Description | Example | | :- | :- | :- | | `min_faves:N` | Minimum number of likes. | `AI min_faves:100` | | `min_retweets:N` | Minimum number of retweets. | `crypto min_retweets:50` | | `min_replies:N` | Minimum number of replies. | `"product launch" min_replies:20` | | `-min_faves:N` | Maximum likes (negated form). | `bitcoin -min_faves:1000` | | `-min_retweets:N` | Maximum retweets. | `news -min_retweets:500` | | `-min_replies:N` | Maximum replies. | `tech -min_replies:100` | | `filter:has_engagement` | Tweets with at least one interaction. Can be negated to find zero-engagement tweets. | `from:username filter:has_engagement` | Counts become approximate for very large values (1,000+). *** ## 4. Media and Content Type ### Media filters | Operator | Description | | :- | :- | | `filter:media` | All media types (images, video, GIFs). | | `filter:images` | All images, including third-party links. | | `filter:twimg` | Only native X images (`pic.twitter.com` links). | | `filter:videos` | All video types: native X video, YouTube embeds, etc. | | `filter:native_video` | Only X-owned video (native uploads, legacy Vine, legacy Periscope). | | `filter:consumer_video` | X native video only (excludes pro/Amplify). | | `filter:pro_video` | X pro video (Amplify) only. | | `filter:spaces` | X Spaces audio content. | | `filter:links` | Tweets containing any URL. Includes media URLs, use `-filter:media` to isolate non-media links. | | `card_name:animated_gif` | Specifically matches GIFs. | ### Tweet type filters | Operator | Description | | :- | :- | | `filter:replies` | Only tweets that are replies to another tweet. | | `-filter:replies` | Exclude replies (show only top-level original tweets). | | `filter:nativeretweets` | Only native retweets (created via the retweet button). | | `include:nativeretweets` | Include native retweets in results (excluded by default). | | `filter:retweets` | Old-style RT retweets plus quote tweets. | | `-filter:retweets` | Exclude retweets entirely. | | `filter:quote` | Only quote tweets. | | `quoted_tweet_id:ID` | Quotes of a specific tweet by its ID. | | `quoted_user_id:ID` | All quotes of a specific user by their user ID. | | `conversation_id:ID` | All tweets in a thread (direct replies and nested replies). | ### Special content filters | Operator | Description | | :- | :- | | `card_name:poll2choice_text_only` | 2-choice text polls. | | `card_name:poll3choice_text_only` | 3-choice text polls. | | `card_name:poll4choice_text_only` | 4-choice text polls. | | `card_name:poll2choice_image` | 2-choice image polls. | | `filter:news` | Tweets linking to recognized news domains. | | `filter:safe` | Excludes NSFW or potentially sensitive content. Not a guarantee. | | `filter:hashtags` | Only tweets containing at least one hashtag. | | `filter:mentions` | Only tweets containing any @mention. | *** ## 5. Date, Time, and Snowflake IDs | Operator | Format | Description | | :- | :- | :- | | `since:YYYY-MM-DD` | `since:2026-01-01` | Tweets posted on or after this date (inclusive). | | `until:YYYY-MM-DD` | `until:2026-03-01` | Tweets posted before this date (not inclusive). | | `since:YYYY-MM-DD_HH:MM:SS_UTC` | `since:2026-03-05_12:00:00_UTC` | Precision timestamp with timezone. | | `since_time:UNIX` | `since_time:1142974200` | After a specific Unix timestamp (seconds). | | `until_time:UNIX` | `until_time:1142974215` | Before a specific Unix timestamp. | | `within_time:Xd` | `within_time:2d` | Within the last X days. Also supports `h`, `m`, `s`. | | `since_id:ID` | `since_id:1234567890` | After a specific Snowflake ID (not inclusive). | | `max_id:ID` | `max_id:1234567890` | At or before a specific Snowflake ID (inclusive). | **Snowflake ID conversion.** Every tweet ID encodes its creation timestamp: ```text theme={null} millisecond_epoch = (tweet_id >> 22) + 1288834974657 ``` For pulling historical sets, see [Historical Data](https://docs.sorsa.io/historical-data). *** ## 6. Geographical Filters | Operator | Description | Example | | :- | :- | :- | | `near:"city"` | Geotagged near a named place. Supports phrases. | `near:"San Francisco"` | | `near:me` | Near your current location (UI only). | `near:me` | | `within:Xkm` | Radius limit for `near:`. Accepts `km` or `mi`. | `earthquake near:Tokyo within:50km` | | `geocode:lat,long,radius` | Pinpoint precision using coordinates. | `geocode:37.77,-122.41,5km` | | `place:ID` | Search by X Place Object ID. | `place:96683cc9126741d1` | Only an estimated 1-2% of tweets carry precise geolocation data. If a tweet does not have coordinates, the API falls back to reverse-geocoding the user's profile location. *** ## 7. Language and Source ### Language Standard ISO 639-1 codes (`lang:en`, `lang:es`, `lang:fr`, `lang:de`, `lang:ja`, `lang:ru`, etc.) plus the following X-specific codes: | Code | Meaning | | :- | :- | | `lang:und` | Undefined language (emoji-only or media-only tweets). | | `lang:qme` | Tweets with media links only (since 2022). | | `lang:qst` | Very short text tweets. | | `lang:qht` | Tweets with hashtags only. | | `lang:qam` | Tweets with mentions only. | | `lang:qct` | Tweets with cashtags only. | | `lang:zxx` | Tweets with media or Twitter Card only, no text. | ### Source (posting client) | Operator | Description | Example | | :- | :- | :- | | `source:client_name` | Filter by the app used to post. Use underscores for spaces. | `source:Twitter_for_iPhone` | Common values: `Twitter_for_iPhone`, `Twitter_for_Android`, `Twitter_Web_App`, `TweetDeck`, `twitter_ads`. *** ## 8. Card and URL Operators | Operator | Description | | :- | :- | | `card_domain:domain` | Matches the domain in a Twitter Card. Mostly equivalent to `url:`. | | `card_url:domain` | Similar to `card_domain:` but may return different results. | | `card_name:audio` | Tweets with Player Cards (Spotify, SoundCloud, etc.). | | `card_name:player` | Tweets with any Player Card. | | `card_name:summary` | Small-image summary cards. | | `card_name:summary_large_image` | Large-image summary cards. | | `card_name:promo_website` | Promoted website cards. | | `card_name:promo_image_convo` | Conversational ad cards with images. | | `card_name:promo_video_convo` | Conversational ad cards with video. | | `url:domain` | Matches URLs. Works well for domains and subdomains. Replace hyphens with underscores (e.g., `url:t_mobile.com`). | `card_name:` typically only matches tweets from the last 7-8 days. *** ## Building queries A practical construction order that scales: 1. **Group core keywords:** `(bitcoin OR ethereum OR $BTC)` 2. **Add content constraints:** `lang:en`, `filter:images`, `-filter:replies` 3. **Set engagement thresholds:** `min_faves:50`, `min_retweets:10` 4. **Exclude noise:** `-from:spambot`, `-scam`, `-filter:retweets` 5. **Add time bounds:** `since:2026-01-01 until:2026-03-01` For full code examples (Python and JavaScript with pagination and rate-limit handling) and 14 production-ready query recipes, see the [full guide on the blog](https://api.sorsa.io/blog/twitter-search-operators). *** ## Known limitations * **Operator cap:** roughly 22-23 operators maximum per query. * **Geo coverage is thin:** only 1-2% of tweets have precise location data. * **`card_name:` is time-limited** to the past 7-8 days. * **Private and suspended accounts** are excluded from search results. * **Language detection is imperfect** for short tweets, code, or emoji-heavy posts. * **Not all tweets are indexed.** Tweets flagged for platform violations may be excluded. * **Auto-correction happens silently** in some cases. Use `+word` or `"word"` to force exact matching. * **URL matching** works for domains and subdomains but is unreliable for long URL paths. *** ## Source This reference draws on the maintained [twitter-advanced-search repository](https://github.com/igorbrigadir/twitter-advanced-search) by Igor Brigadir, the industry-standard source on undocumented X search behavior. *** ## Next Steps * [Search Tweets](https://docs.sorsa.io/search-tweets) - complete guide to the `/search-tweets` endpoint. * [Track Mentions](https://docs.sorsa.io/search-mentions) - strategies for tracking @mentions of any account. * [Pagination](https://docs.sorsa.io/pagination) - paginating through large result sets. * [Search Builder](https://api.sorsa.io/playground/search-builder) - free visual query builder. * [Full Cheat Sheet on the Blog](https://api.sorsa.io/blog/twitter-search-operators) - recipes, code examples, and X API v2 comparison. # Search Tweets Source: https://docs.sorsa.io/search-tweets POST endpoint for keyword and operator-based tweet search on X (Twitter). The `/search-tweets` endpoint runs keyword and operator-based queries against the public X (Twitter) search index and returns matching tweets with full author profiles and engagement metrics. For end-to-end usage patterns, query templates, and working code, see the [How to Search Tweets via API](https://api.sorsa.io/blog/twitter-search-api) guide on the blog. ## Endpoint ```text theme={null} POST https://api.sorsa.io/v3/search-tweets ``` ## Authentication Pass your API key in the `ApiKey` header. HTTP header names are case-insensitive; the key value must match your issued key. ```text theme={null} ApiKey: YOUR_API_KEY ``` See [Authentication](https://docs.sorsa.io/authentication) for details. ## Request body | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `query` | string | Yes | Search keywords and operators. Supports the full set of native X search operators. | | `order` | string | No | `"popular"` (default) for the Top tab equivalent, or `"latest"` for chronological order. | | `next_cursor` | string | No | Pagination cursor returned in the previous response. Omit on the first request. | ### Example request ```bash theme={null} curl -X POST https://api.sorsa.io/v3/search-tweets \ -H "ApiKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "artificial intelligence lang:en", "order": "latest" }' ``` ## Response ```json theme={null} { "tweets": [ { "id": "2029914600217473314", "full_text": "The latest breakthroughs in AI are reshaping automation.", "created_at": "2026-03-06T13:38:49Z", "lang": "en", "conversation_id_str": "2029914600217473314", "likes_count": 142, "retweet_count": 38, "reply_count": 12, "quote_count": 5, "view_count": 28400, "bookmark_count": 19, "is_reply": false, "is_quote_status": false, "entities": [], "user": { "id": "1422280682240450563", "username": "tech_insider", "display_name": "Tech Insider", "followers_count": 84200, "verified": true } } ], "next_cursor": "DAABCgABGSmiaxkAAgoAAgjEJ..." } ``` Each tweet includes the full author profile inline. No additional lookup is required to attach user data to tweets. > For the complete field reference, see [Response Format](https://docs.sorsa.io/response-format). ## Query operators The `query` field accepts the full set of native X search operators, including: * **Keywords and exact phrases:** `"climate change"` * **User filters:** `from:`, `to:`, `@mention` * **Engagement filters:** `min_faves:`, `min_retweets:`, `min_replies:` * **Content filters:** `filter:media`, `filter:images`, `filter:videos`, `filter:links` (prefix with `-` to exclude) * **Language and date:** `lang:en`, `since:2026-01-01`, `until:2026-03-01` * **Boolean logic:** `OR`, parentheses for grouping, `-` for exclusion > Full operator reference: [Search Operators](https://docs.sorsa.io/search-operators). ## Pagination The response includes a `next_cursor` field. To fetch the next page, send the same request with `next_cursor` set to that value. When `next_cursor` is `null` or absent, the result set is exhausted. > See [Pagination](https://docs.sorsa.io/pagination) for cursor-based pagination patterns and edge cases. ## Rate limit 20 requests per second per API key, universal across all plans. Exceeding the limit returns `429 Too Many Requests`. See [Rate Limits](https://docs.sorsa.io/rate-limits). ## Error codes | Code | Meaning | | :- | :- | | `200` | OK | | `400` | Bad request (invalid parameters) | | `401` | Unauthorized (missing or invalid API key) | | `403` | Forbidden (quota exhausted or subscription expired) | | `429` | Too many requests (rate limit exceeded) | | `500` | Internal server error | > See [Error Codes](https://docs.sorsa.io/error-codes) for the full list. ## Related endpoints * [Track Mentions](https://docs.sorsa.io/api-reference/search/search-mentions) - purpose-built `@handle` tracking with a richer body-level filter set * [User Tweets](https://docs.sorsa.io/api-reference/tweets/user-tweets) - timeline of a single user * [Search Users](https://docs.sorsa.io/api-reference/search/search-users) - find accounts by keyword # Sorsa Score & Crypto Analytics Source: https://docs.sorsa.io/sorsa-score-and-crypto-analytics # Sorsa Score and Crypto Social Graph Analytics Sorsa maintains an internal database of crypto-related X accounts - projects, influencers, KOLs, venture capital firms, and their employees. The endpoints in this section tap into that database to provide analytics that go beyond standard X profile data: an influence score that measures an account's standing within the crypto ecosystem, follower categorization by type (influencer, project, VC), and tracking of recent follow activity among crypto accounts. These endpoints are specifically designed for crypto due diligence, influencer vetting, project discovery, and community analysis. If you are evaluating whether a crypto project is legitimate, assessing an influencer's real weight in the space, or tracking which VCs are paying attention to emerging projects, this is the data layer you need. > **Free to start:** Every endpoint here works on your first 100 requests: one-time, no credit card, no expiry. A full due-diligence pass is about four calls per account (Score, Score change, follower breakdown, top followers), so 100 requests covers roughly 25 accounts before you need a plan. > **Note:** The Sorsa Score and related analytics are also available through the [Sorsa web app](https://api.sorsa.io/) with a visual interface. The API endpoints documented here provide programmatic access to the same underlying data for integration into your own tools and workflows. *** ## What is the Sorsa Score? The Sorsa Score is a numeric metric that reflects how many influential crypto accounts follow a given account, and how influential those followers are. It is not based on total follower count, content quality, profile appearance, or verification status. It is based purely on the crypto social graph: who follows you, and how much weight they carry. Key characteristics: * **Quality matters more than quantity.** A few followers with a Score of 1000+ contribute more than dozens of followers with a Score of 200. The score rewards genuine recognition from established figures, not mass-following. * **Follow-for-follow and mass-following accounts are excluded.** Sorsa detects and filters out accounts that artificially inflate their follower lists. These accounts do not contribute to Score calculations. * **Content, profile design, and blue checkmarks do not directly affect the Score.** However, high-quality content tends to attract influential followers over time, so there is often a correlation. * **The Score is dynamic.** It changes as influential accounts follow or unfollow. The `/score-changes` endpoint tracks this movement over weekly and monthly windows. A high Sorsa Score signals that an account is genuinely recognized within the crypto ecosystem. A low score on an account claiming to be a major player is a red flag worth investigating. *** ## Endpoints Overview | Endpoint | What it returns | | :- | :- | | `GET /score` | The current Sorsa Score for an account | | `GET /score-changes` | Score delta over the last week and month | | `GET /followers-stats` | Follower count broken down by category: influencers, projects, VCs | | `GET /top-followers` | Top 20 followers ranked by Sorsa Score | | `GET /top-following` | Top 20 accounts the user follows, ranked by Score | | `GET /new-followers-7d` | Crypto accounts that followed the user in the last 7 days | | `GET /new-following-7d` | Crypto accounts the user started following in the last 7 days | All endpoints accept `username`, `user_id`, or `user_link` as input (exactly one of the three required). All use the GET method. *** ## Get the Sorsa Score **Endpoint:** `GET /v3/score` ```bash theme={null} curl "https://api.sorsa.io/v3/score?username=VitalikButerin" \ -H "ApiKey: YOUR_API_KEY" ``` ```json theme={null} {"score": 1843.7} ``` ```python theme={null} import requests API_KEY = "YOUR_API_KEY" def get_score(username): resp = requests.get( "https://api.sorsa.io/v3/score", headers={"ApiKey": API_KEY}, params={"username": username}, ) resp.raise_for_status() return resp.json()["score"] print(f"Vitalik's Sorsa Score: {get_score('VitalikButerin')}") ``` Like the other endpoints in this section, `/score` accepts `username`, `user_id`, or `user_link` (provide exactly one). Higher scores indicate stronger recognition among crypto influencers, projects, and VCs. Accounts with very large follower bases may take slightly longer to return on the first request. *** ## Track Score Changes Over Time **Endpoint:** `GET /v3/score-changes` Returns how much the Score has changed in the last week and month. Useful for detecting momentum - an account gaining Score rapidly may be an emerging project worth watching; an account losing Score may be falling out of favor. ```python theme={null} def get_score_changes(username): resp = requests.get( "https://api.sorsa.io/v3/score-changes", headers={"ApiKey": API_KEY}, params={"username": username}, ) resp.raise_for_status() return resp.json() changes = get_score_changes("some_crypto_project") print(f"Week delta: {changes['week_delta']:+}") print(f"Month delta: {changes['month_delta']:+}") ``` Response: ```json theme={null} { "week_delta": 12, "month_delta": 24 } ``` A positive `week_delta` means the account gained Score in the past 7 days (more influential accounts followed them). A negative delta means influential followers unfollowed or lost their own Score. **Requirement:** The account must already be tracked in the Sorsa database. New or previously untracked accounts will not have historical Score data. *** ## Follower Breakdown by Category **Endpoint:** `GET /v3/followers-stats` Breaks down an account's followers into three categories from Sorsa's database: influencers (individual crypto KOLs), projects (crypto project accounts), and venture capitals (VCs and their employees). ```python theme={null} def get_follower_breakdown(username): resp = requests.get( "https://api.sorsa.io/v3/followers-stats", headers={"ApiKey": API_KEY}, params={"username": username}, ) resp.raise_for_status() return resp.json() stats = get_follower_breakdown("some_crypto_project") print(f"Total followers (Sorsa DB): {stats['followers_count']}") print(f" Influencers: {stats['influencers_count']}") print(f" Projects: {stats['projects_count']}") print(f" VCs: {stats['venture_capitals_count']}") ``` Response: ```json theme={null} { "followers_count": 16, "influencers_count": 12, "projects_count": 3, "venture_capitals_count": 1, "user_protected": false } ``` Note that `followers_count` here refers to the number of followers from Sorsa's crypto database, not the account's total X follower count. An account with 50,000 X followers might have 200 crypto-relevant followers tracked by Sorsa. This breakdown is particularly useful for due diligence: a project claiming to be VC-backed should have VC accounts in their follower stats. A project with zero VC followers and zero project followers despite claiming partnerships is a warning sign. *** ## Top 20 Followers and Following by Score **Endpoint:** `GET /v3/top-followers` Returns the 20 followers with the highest Sorsa Score. This tells you which of the most influential crypto accounts are watching this profile. **Endpoint:** `GET /v3/top-following` Returns the 20 accounts the user follows that have the highest Score. This reveals whose content and activity the account considers worth tracking. ```python theme={null} def get_top_followers(username): resp = requests.get( "https://api.sorsa.io/v3/top-followers", headers={"ApiKey": API_KEY}, params={"username": username}, ) resp.raise_for_status() return resp.json().get("users", []) def get_top_following(username): resp = requests.get( "https://api.sorsa.io/v3/top-following", headers={"ApiKey": API_KEY}, params={"username": username}, ) resp.raise_for_status() return resp.json().get("users", []) # Who are the biggest names following this project? top = get_top_followers("some_crypto_project") print("Top followers by Sorsa Score:") for u in top[:10]: print(f" @{u['username']} (Score {u.get('score', 0)}, {u['followers_count']:,} followers)") print(f" {u.get('description', '')[:60]}") ``` The two endpoints return slightly different shapes: * `/top-followers` returns a `TopFollowersResponse`: an array of compact follower profiles, each carrying a `score` field (that follower's own Sorsa Score) so you can gauge how influential each one is. These profiles are compact and omit some fields present on a full profile, such as `location` and `bio_urls`. * `/top-following` returns a `FollowersResponse`: `Follower` objects with the standard profile fields plus `followerDate` (when the follow relationship was established). Note there is no `score` field on this response. If you need the full profile for a top follower (bio links, location, media count), pass its handle to `/info-batch`. *** ## New Followers and Following (Last 7 Days) **Endpoint:** `GET /v3/new-followers-7d` Returns crypto accounts from Sorsa's database that started following the specified user in the last 7 days. **Endpoint:** `GET /v3/new-following-7d` Returns crypto accounts from Sorsa's database that the specified user started following in the last 7 days. ```python theme={null} def get_new_followers_7d(username): resp = requests.get( "https://api.sorsa.io/v3/new-followers-7d", headers={"ApiKey": API_KEY}, params={"username": username}, ) resp.raise_for_status() return resp.json().get("users", []) def get_new_following_7d(username): resp = requests.get( "https://api.sorsa.io/v3/new-following-7d", headers={"ApiKey": API_KEY}, params={"username": username}, ) resp.raise_for_status() return resp.json().get("users", []) new_followers = get_new_followers_7d("some_crypto_project") print(f"New crypto followers this week: {len(new_followers)}") for u in new_followers: print(f" @{u['username']} followed on {u.get('followerDate', 'unknown')}") ``` Both 7-day endpoints return a `FollowersResponse` (`Follower` objects with a `followerDate`). ### Database Dependency Both 7-day endpoints have important constraints to understand: 1. **The target account must already be in Sorsa's database.** If you query an account Sorsa has not been tracking, there is no historical data to compare against, so the endpoint cannot determine what is "new." 2. **Only follows involving accounts in Sorsa's database are shown.** If a random non-crypto X account follows the target, it will not appear in these results. These endpoints track movement within the crypto social graph specifically - crypto accounts following other crypto accounts. This makes the data highly relevant for crypto analysis (you see exactly which known crypto players are making new connections) but it is not a substitute for the general `/followers` endpoint if you need the complete follower list including non-crypto accounts. See [Followers & Following](https://docs.sorsa.io/followers-and-following) for the general-purpose approach. *** ## Practical Applications ### Project Due Diligence When evaluating a crypto project, combine several endpoints to build a trust profile: ```python theme={null} def due_diligence_report(username): """Quick due diligence check for a crypto project.""" score = get_score(username) changes = get_score_changes(username) stats = get_follower_breakdown(username) top = get_top_followers(username) print(f"Due Diligence: @{username}") print(f"{'='*40}") print(f"Sorsa Score: {score}") print(f" Week change: {changes['week_delta']:+}") print(f" Month change:{changes['month_delta']:+}") print() print(f"Crypto followers: {stats['followers_count']}") print(f" Influencers: {stats['influencers_count']}") print(f" Projects: {stats['projects_count']}") print(f" VCs: {stats['venture_capitals_count']}") print() if top: print(f"Top followers by Score:") for u in top[:5]: print(f" @{u['username']} (Score {u.get('score', 0)}, {u['followers_count']:,} followers)") else: print("No significant crypto followers found - investigate further.") # Red flags flags = [] if score < 10: flags.append("Very low Score - minimal recognition in crypto") if stats["venture_capitals_count"] == 0 and stats["projects_count"] == 0: flags.append("No VC or project followers - claims of partnerships may be false") if changes["month_delta"] < -20: flags.append("Score dropping fast - influential followers are leaving") if flags: print(f"\nRed flags:") for f in flags: print(f" - {f}") return score, stats, top due_diligence_report("some_crypto_project") ``` ### Comparing Projects Side by Side ```python theme={null} projects = ["project_a", "project_b", "project_c"] print(f"{'Project':<20} {'Score':>7} {'Week':>6} {'Influencers':>12} {'Projects':>9} {'VCs':>5}") print("-" * 65) for handle in projects: score = get_score(handle) changes = get_score_changes(handle) stats = get_follower_breakdown(handle) print(f"@{handle:<19} {score:>7.1f} {changes['week_delta']:>+6} " f"{stats['influencers_count']:>12} {stats['projects_count']:>9} " f"{stats['venture_capitals_count']:>5}") ``` ### Tracking VC Activity Monitor which projects a known VC account starts following - this can signal upcoming investments or partnerships: ```python theme={null} vc_accounts = ["a16z_crypto", "paradigm", "polychain"] for vc in vc_accounts: new_follows = get_new_following_7d(vc) if new_follows: print(f"@{vc} started following {len(new_follows)} new crypto accounts this week:") for u in new_follows: print(f" @{u['username']} (followed {u.get('followerDate', 'recently')})") else: print(f"@{vc}: no new crypto follows this week") print() ``` ### Early Project Discovery Find projects that are gaining Score momentum - a rising Score means influential accounts are taking notice: ```python theme={null} watchlist = ["new_project_1", "new_project_2", "new_project_3", "new_project_4"] rising = [] for handle in watchlist: try: score = get_score(handle) changes = get_score_changes(handle) if changes["week_delta"] > 5: rising.append({ "handle": handle, "score": score, "week_delta": changes["week_delta"], }) except Exception: continue rising.sort(key=lambda x: x["week_delta"], reverse=True) print("Rising projects (Score gained this week):") for r in rising: print(f" @{r['handle']}: Score {r['score']} ({r['week_delta']:+} this week)") ``` *** ## Exporting to CSV ```python theme={null} import csv import time def export_crypto_analysis(handles, output_file="crypto_analysis.csv"): """Export Score and follower stats for a list of accounts.""" fields = ["username", "score", "week_delta", "month_delta", "crypto_followers", "influencers", "projects", "vcs"] with open(output_file, "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=fields) writer.writeheader() for handle in handles: try: score = get_score(handle) changes = get_score_changes(handle) stats = get_follower_breakdown(handle) writer.writerow({ "username": handle, "score": score, "week_delta": changes["week_delta"], "month_delta": changes["month_delta"], "crypto_followers": stats["followers_count"], "influencers": stats["influencers_count"], "projects": stats["projects_count"], "vcs": stats["venture_capitals_count"], }) except Exception as e: print(f"Error for @{handle}: {e}") time.sleep(0.15) # 3 API calls per account print(f"Exported {len(handles)} accounts to {output_file}") ``` *** ## Next Steps * [Competitor Analysis](https://docs.sorsa.io/Competitor-Analysis) - combine Sorsa Score data with general profile and content analysis. * [Followers & Following](https://docs.sorsa.io/followers-and-following) - extract the full follower list (not just crypto-tracked accounts). * [Target Audience Discovery](https://docs.sorsa.io/target-audiences-Discovery) - find crypto audiences via community scraping and bio search. * [Marketing Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification) - verify engagement actions for crypto marketing campaigns. * [API Reference](https://docs.sorsa.io/api-reference-guide) - full specification for all Sorsa Score and crypto analytics endpoints. # Support Source: https://docs.sorsa.io/support If something is not working as expected, here is how to get help. *** ## Check Before Contacting Support **Review the error message.** Most API errors include a `message` field that points directly to the problem. See [Error Codes](https://docs.sorsa.io/error-codes) for a full reference on 400, 401, 403, 404, 429, and 500 responses with troubleshooting steps. **Check the API status page.** If you are experiencing timeouts or 500 errors across multiple endpoints, the issue may be on our side. Check the live status at [uptime.sorsa.io/status/v3](https://uptime.sorsa.io/status/v3) before opening a ticket. **Check your quota.** If you are getting 403 errors, your request quota may be exhausted. Verify your remaining balance on the [Dashboard](https://api.sorsa.io/overview) or by calling [`GET /key-usage-info`](https://docs.sorsa.io/api-reference/technical-endpoints/api-key-usage). *** ## Contact via Discord (Fastest) Our Discord server is the quickest way to reach the support team. 1. Join the server: [discord.com/invite/uwAefKCj7X](https://discord.com/invite/uwAefKCj7X) 2. Complete verification in the **#verification** channel by clicking the hat emoji. 3. Go to the **#open-ticket** channel and click the **Support** button to create a private ticket. A team member will respond in your ticket thread. *** ## Contact via Email If you prefer email, reach us at [**contacts@sorsa.io**](mailto:contacts@sorsa.io). Include the following so we can help faster: * The endpoint URL you are calling * The HTTP status code and error message you received * Your request body * The approximate time the issue occurred, including timezone Remove API keys and authorization headers before sharing requests or logs. Include a minimal reproducible request and the response status; never send your key. *** ## Useful Links * [Error Codes](https://docs.sorsa.io/error-codes) - troubleshoot common API errors * [API Status Page](https://uptime.sorsa.io/status/v3) - check if endpoints are operational * [Rate Limits](https://docs.sorsa.io/rate-limits) - understand the 20 req/s limit and retry strategies * [Dashboard](https://api.sorsa.io/overview) - check your quota and manage API keys # Target Audience Discovery Source: https://docs.sorsa.io/target-audiences-Discovery Find relevant X accounts through profile search, followers, communities, and tweet engagement. Find relevant people on X (Twitter) using six discovery techniques. Each starts from a different signal: profile keywords, followers, Community membership, recent tweets, verification status, or engagement with a specific post. Combine the results by user ID to build a deduplicated audience. For a narrative walkthrough, see [How to Find Your Target Audience on Twitter Using the API](https://api.sorsa.io/blog/twitter-audience-discovery). ## Choose a technique | Question | Endpoint | Result | | :- | :- | :- | | Who describes themselves with a relevant role or keyword? | `POST /search-users` | User profiles | | Who follows an account in my space? | `GET /followers` | Up to 200 profiles per page | | Who joined a Community around my topic? | `POST /community-members` | Compact member profiles | | Who is discussing my topic? | `POST /search-tweets` | Up to 20 tweets per page, with author profiles | | Which verified accounts follow a target? | `GET /verified-followers` | Up to 200 profiles per page | | Who amplifies a specific post? | `POST /retweeters`, `POST /quotes` | Retweeter profiles or quote tweets | Page sizes vary. Use `next_cursor` to continue; do not treat a short page as the end. ## Setup and shared pagination All examples use `https://api.sorsa.io/v3` and require the `ApiKey` header. Run Python examples in the same script after this setup. Install `requests` with `python -m pip install requests`, then set the `SORSA_API_KEY` environment variable. JavaScript examples require a server-side runtime with `fetch`, such as Node.js 18 or later. ```python theme={null} import os import time import requests API_KEY = os.environ["SORSA_API_KEY"] BASE_URL = "https://api.sorsa.io/v3" def fetch_pages(method, endpoint, payload, result_key, max_pages=10): """Fetch a bounded number of pages; raise on HTTP errors.""" items = [] cursor = None seen_cursors = set() for _ in range(max_pages): values = dict(payload) if cursor: values["next_cursor"] = cursor options = {"params": values} if method == "GET" else {"json": values} response = requests.request( method, f"{BASE_URL}{endpoint}", headers={"ApiKey": API_KEY}, timeout=30, **options, ) response.raise_for_status() data = response.json() items.extend(data.get(result_key) or []) cursor = data.get("next_cursor") if not cursor: break if cursor in seen_cursors: raise RuntimeError("Pagination returned a repeated cursor") seen_cursors.add(cursor) time.sleep(0.1) return items ``` `max_pages` limits request usage; reaching it can leave more results unread. These examples stop on HTTP errors. For production jobs, add bounded retries for `429` and transient server errors using [Error Codes](https://docs.sorsa.io/error-codes), and coordinate all workers that share a key under the [rate limit](https://docs.sorsa.io/rate-limits). See [Authentication](https://docs.sorsa.io/authentication) and [Pagination](https://docs.sorsa.io/pagination) for shared mechanics. ## Technique 1: Bio Keyword Search **Endpoint:** `POST /v3/search-users` Search for accounts by keyword or phrase, such as a role, title, or interest. Inspect the returned bio, display name, and handle to decide whether each result matches your audience. ```json theme={null} { "query": "Product Manager" } ``` | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `query` | string | Yes | Search keyword or phrase. | | `next_cursor` | string | No | Cursor from the previous response; omit on the first request. | ### Python ```python theme={null} def find_users_by_bio(query, max_pages=10): return fetch_pages("POST", "/search-users", {"query": query}, "users", max_pages) bio_results = find_users_by_bio("machine learning engineer") qualified = [ u for u in bio_results if (u.get("followers_count") or 0) >= 1000 and (u.get("tweets_count") or 0) >= 100 and not u.get("protected", False) ] ``` ### JavaScript ```javascript theme={null} const API_KEY = process.env.SORSA_API_KEY; if (!API_KEY) throw new Error("Set SORSA_API_KEY before running this example"); async function findUsersByBio(query, maxPages = 10) { const users = []; const seenCursors = new Set(); let cursor = null; for (let i = 0; i < maxPages; i++) { const body = { query }; if (cursor) body.next_cursor = cursor; const response = await fetch("https://api.sorsa.io/v3/search-users", { method: "POST", headers: { ApiKey: API_KEY, "Content-Type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(30000), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); users.push(...(data.users || [])); cursor = data.next_cursor; if (!cursor) break; if (seenCursors.has(cursor)) throw new Error("Repeated pagination cursor"); seenCursors.add(cursor); await new Promise((resolve) => setTimeout(resolve, 100)); } return users; } ``` ## Technique 2: Competitor Follower Extraction **Endpoint:** `GET /v3/followers` Retrieve followers of a relevant public account, up to 200 profiles per request. Provide one of `username` (without `@`), `user_id` (a string), or `user_link` (a full profile URL). Pass an optional `next_cursor` to continue. ```text theme={null} GET https://api.sorsa.io/v3/followers?username=competitor_handle ``` ```python theme={null} def get_followers(username, max_pages=10): return fetch_pages("GET", "/followers", {"username": username}, "users", max_pages) followers = get_followers("competitor_handle", max_pages=20) ``` ### Overlap across multiple seed accounts Count each user once per seed account. Replace the placeholder handles before running: ```python theme={null} from collections import Counter competitors = ["competitor_a", "competitor_b", "competitor_c"] follower_sets = { handle: {u["id"] for u in get_followers(handle, max_pages=10)} for handle in competitors } counts = Counter(uid for ids in follower_sets.values() for uid in ids) overlap = {uid for uid, count in counts.items() if count >= 2} ``` This measures overlap in the pages you retrieved, not necessarily the full follower lists. See [Followers and Following](https://docs.sorsa.io/followers-and-following) for a deeper walkthrough. ## Technique 3: Community Member Discovery > **Check availability first:** This section documents the Community request format. Confirm current data availability with [support](https://docs.sorsa.io/support) before adding it to a new workflow; see [Lists and Communities](https://docs.sorsa.io/lists-and-communities). **Endpoint:** `POST /v3/community-members` Retrieve the members of an X Community. Membership is a useful interest signal, but does not establish current activity or purchase intent. ```json theme={null} { "community_link": "1966045657589813686" } ``` `community_link` accepts the numeric ID as a string or a full Community URL. ```python theme={null} def get_community_members(community_id, max_pages=20): return fetch_pages( "POST", "/community-members", {"community_link": community_id}, "users", max_pages, ) ``` The response contains compact profiles: `id`, `username`, `display_name`, `profile_image_url`, `verified`, and `protected`. Before filtering on bio or follower counts, enrich the IDs through [User Profile (Batch)](https://docs.sorsa.io/api-reference/users-data/user-profile-batch), up to 100 IDs per call. See [Lists and Communities](https://docs.sorsa.io/lists-and-communities) for related endpoints. ## Technique 4: Intent-Based Tweet Mining **Endpoint:** `POST /v3/search-tweets` Search recent discussions and extract unique authors. Keep each full user object so it can be combined with profile and follower results later. ```python theme={null} def find_active_voices(query, min_followers=100, max_pages=10): tweets = fetch_pages( "POST", "/search-tweets", {"query": query, "order": "latest"}, "tweets", max_pages, ) voices = {} for tweet in tweets: user = tweet.get("user") if not user or not user.get("id"): continue if (user.get("followers_count") or 0) < min_followers: continue if user["id"] not in voices: voices[user["id"]] = { **user, "sample_tweet": (tweet.get("full_text") or "")[:160], } return list(voices.values()) intent_voices = find_active_voices( '("need a CRM" OR "looking for a CRM") lang:en -filter:retweets', ) ``` ### Common query patterns Replace bracketed placeholders with your category, handle, tool, or topic. Parentheses keep shared filters attached to both sides of `OR`. | Goal | Query | | :- | :- | | Buying intent | `("need a [category]" OR "looking for [category]") lang:en -filter:retweets` | | Competitor frustration | `"[competitor]" (frustrated OR broken OR "switching from") -from:[competitor]` | | Migration intent | `("migrating from [tool]" OR "switching from [tool]") lang:en` | | Recommendation requests | `("any recommendation" OR "anyone use") [topic] lang:en` | | Pain-point discussion | `("struggling with" OR "how do you handle") [topic] lang:en` | For a defined observation window, add `since:` and `until:` filters. Review the matching posts before treating a keyword match as buying intent. See [Search Operators](https://docs.sorsa.io/search-operators) and [Search Tweets](https://docs.sorsa.io/search-tweets). ## Technique 5: Verified Follower Analysis **Endpoint:** `GET /v3/verified-followers` Retrieve verified followers using the same identifier and pagination pattern as `/followers`. Verification status is a segment attribute; assess relevance separately. ```python theme={null} def get_verified_followers(username, max_pages=10): return fetch_pages( "GET", "/verified-followers", {"username": username}, "users", max_pages, ) verified = get_verified_followers("openai") verified.sort(key=lambda u: u.get("followers_count") or 0, reverse=True) ``` ## Technique 6: Retweeters and Quoters **Endpoints:** `POST /v3/retweeters`, `POST /v3/quotes` `/retweeters` returns user profiles. `/quotes` returns quote tweet objects, with the quoting user in `user` and their commentary in `full_text`. ```python theme={null} def get_retweeters(tweet_link, max_pages=10): return fetch_pages( "POST", "/retweeters", {"tweet_link": tweet_link}, "users", max_pages, ) def get_quoters(tweet_link, max_pages=10): quote_tweets = fetch_pages( "POST", "/quotes", {"tweet_link": tweet_link}, "tweets", max_pages, ) return list({ tweet["user"]["id"]: tweet["user"] for tweet in quote_tweets if tweet.get("user") }.values()) ``` The helper `get_quoters` converts quote tweets into unique user profiles for the workflow below. If you need commentary, keep the original `quote_tweets` and analyze `full_text` before this conversion. ## Combining Techniques Combine lists of user objects by string ID, preserving the set of sources for each account. A higher source count means an account appeared in more of your selected inputs; it is a prioritization heuristic, not a confidence score. ```python theme={null} def score_by_source(by_source): index = {} for source, users in by_source.items(): for user in users: uid = user["id"] if uid not in index: index[uid] = {"user": dict(user), "sources": set()} else: # Fill gaps when one source returns a compact profile. for field, value in user.items(): if index[uid]["user"].get(field) is None and value is not None: index[uid]["user"][field] = value index[uid]["sources"].add(source) result = [ {**entry["user"], "source_count": len(entry["sources"]), "sources": sorted(entry["sources"])} for entry in index.values() ] return sorted(result, key=lambda u: (-u["source_count"], -(u.get("followers_count") or 0))) # Uses the results from Techniques 1, 2, and 4 above. combined = score_by_source({ "profile_search": bio_results, "competitor_followers": followers, "topic_discussion": intent_voices, }) ``` Add Community members after enriching their compact profiles. You can also add the user lists returned by `get_retweeters` and `get_quoters`. ## Filtering for Quality Use explicit selection criteria for your project. This filter checks profile completeness, account age, and basic counts. It does not detect bots or establish recent activity; inspect recent tweets when activity matters. ```python theme={null} from datetime import datetime, timezone, timedelta def is_quality_account(user, min_followers=500, min_tweets=100, max_following_ratio=10): if user.get("protected", False): return False followers = user.get("followers_count") or 0 if followers < min_followers or (user.get("tweets_count") or 0) < min_tweets: return False if (user.get("followings_count") or 0) > followers * max_following_ratio: return False if not (user.get("description") or "").strip(): return False created = user.get("created_at") if not created: return False try: dt = datetime.fromisoformat(created.replace("Z", "+00:00")) if dt.tzinfo is None: return False except (TypeError, ValueError): return False return dt <= datetime.now(timezone.utc) - timedelta(days=30) qualified = [user for user in combined if is_quality_account(user)] ``` Accounts with missing or unparseable creation dates are excluded by this example. Adjust that policy and the thresholds to your use case. ## Exporting to CSV Export user objects after deduplication and filtering. Convert tweet results to their `user` objects first; enrich compact Community profiles if you need the missing fields. ```python theme={null} import csv def export_users_to_csv(users, output_file="audience.csv"): fields = [ "user_id", "username", "display_name", "description", "followers_count", "followings_count", "tweets_count", "location", "verified", "created_at", ] with open(output_file, "w", newline="", encoding="utf-8") as file: writer = csv.DictWriter(file, fieldnames=fields) writer.writeheader() for user in users: row = {field: user.get(field, "") for field in fields} row["user_id"] = user["id"] row["description"] = (user.get("description") or "").replace("\n", " ") writer.writerow(row) export_users_to_csv(qualified) ``` Missing values remain blank rather than becoming zero. When importing the CSV into a spreadsheet, set the `user_id` column to text to preserve the full ID. ## Next Steps * [Search Tweets](https://docs.sorsa.io/search-tweets) — request parameters and tweet search examples. * [Search Operators](https://docs.sorsa.io/search-operators) — Boolean logic and filters. * [Followers and Following](https://docs.sorsa.io/followers-and-following) — follower analysis. * [Lists and Communities](https://docs.sorsa.io/lists-and-communities) — membership and feed endpoints. * [Competitor Analysis](https://docs.sorsa.io/Competitor-Analysis) — competitive intelligence workflows. * [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring) — polling and deduplication. * [Track Mentions](https://docs.sorsa.io/search-mentions) — mentions of brands and competitors. * [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) — batching and request budgeting. * [API Reference](https://docs.sorsa.io/api-reference-guide) — endpoint specifications. # Tweet Engagement Source: https://docs.sorsa.io/tweet-engagement Every tweet on X generates three types of public engagement: comments (replies), quote tweets, and retweets. The aggregate counts are visible on the tweet itself, but the actual people and content behind those numbers are not. Sorsa provides dedicated endpoints to extract all three: who replied and what they said, who quoted the tweet and what they added, and who retweeted it. This guide covers how to retrieve a tweet's full engagement data, from a high-level metrics snapshot down to the individual replies, quotes, and retweeters. > **Note:** For a fuller walkthrough with extra analysis examples and end-to-end workflows, see [Twitter Engagement API: Get Replies, Quotes & Retweeters](https://api.sorsa.io/blog/twitter-engagement-api) on the blog. *** ## Starting Point: Get a Tweet's Metrics Before drilling into individual engagement, you often want the overview. The `/tweet-info` endpoint returns the full tweet object including all engagement counters. ### Simplest Example ```bash theme={null} curl -X POST https://api.sorsa.io/v3/tweet-info \ -H "ApiKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"tweet_link": "https://x.com/elonmusk/status/1234567890"}' ``` ```python theme={null} import requests API_KEY = "YOUR_API_KEY" def get_tweet(tweet_link): resp = requests.post( "https://api.sorsa.io/v3/tweet-info", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json={"tweet_link": tweet_link}, ) resp.raise_for_status() return resp.json() tweet = get_tweet("https://x.com/elonmusk/status/1234567890") print(f"Text: {tweet['full_text'][:100]}...") print(f"Likes: {tweet.get('likes_count', 0):,}") print(f"Retweets: {tweet.get('retweet_count', 0):,}") print(f"Quotes: {tweet.get('quote_count', 0):,}") print(f"Replies: {tweet.get('reply_count', 0):,}") print(f"Views: {tweet.get('view_count', 0):,}") print(f"Bookmarks:{tweet.get('bookmark_count', 0):,}") ``` The `tweet_link` field accepts either a full tweet URL or just the numeric tweet ID. For multiple tweets at once, use `/tweet-info-bulk` with up to 100 tweet links per request (see [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage)). > **Tip:** New here? Every account starts with 100 free requests (no card, no expiry). You can also run any endpoint on this page without writing code in the [API Playground](https://api.sorsa.io/playground). *** ## Comments (Replies) **Endpoint:** `POST /v3/comments` Returns the replies posted under a specific tweet. Each page contains up to 20 comments, and each comment is a full tweet object with its own engagement metrics and author profile. ### Simplest Example ```python theme={null} def get_comments(tweet_link): resp = requests.post( "https://api.sorsa.io/v3/comments", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json={"tweet_link": tweet_link}, ) resp.raise_for_status() return resp.json() data = get_comments("https://x.com/elonmusk/status/1234567890") for comment in data.get("tweets", []): print(f"@{comment['user']['username']}: {comment['full_text'][:80]}") ``` ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `tweet_link` | string | Yes | Full URL of the tweet, or just the tweet ID. | | `order_by` | string | No | Sort order: `"Relevance"` (default), `"Recency"`, or `"Likes"`. | | `next_cursor` | string | No | Pagination cursor for fetching more comments. | Set `order_by` to `"Likes"` to have the API rank replies by engagement server-side. This is more efficient than fetching everything and sorting locally when you only need the top few replies, since page one already holds the most-liked comments. ### Paginating Through All Comments Tweets with hundreds of replies require pagination. The pattern is the same cursor-based loop used across all Sorsa endpoints: ```python theme={null} import time def get_all_comments(tweet_link, max_pages=20): """Fetch all comments under a tweet with pagination.""" all_comments = [] next_cursor = None for page in range(max_pages): body = {"tweet_link": tweet_link} if next_cursor: body["next_cursor"] = next_cursor resp = requests.post( "https://api.sorsa.io/v3/comments", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json=body, ) resp.raise_for_status() data = resp.json() comments = data.get("tweets", []) all_comments.extend(comments) next_cursor = data.get("next_cursor") if not next_cursor: break time.sleep(0.1) return all_comments comments = get_all_comments("https://x.com/elonmusk/status/1234567890") print(f"Collected {len(comments)} comments") ``` Each comment is a full tweet object: text, engagement metrics, and author profile. Common patterns include sorting by `likes_count` to find the most-engaged-with replies, filtering on `?` to extract questions, and feeding `full_text` into a sentiment classifier. ```python theme={null} # Find the most-liked comments top_comments = sorted(comments, key=lambda c: c.get("likes_count", 0), reverse=True) for c in top_comments[:5]: print(f"@{c['user']['username']} ({c['likes_count']} likes): {c['full_text'][:80]}") ``` *** ## Quote Tweets **Endpoint:** `POST /v3/quotes` Returns tweets that quoted (retweeted with comment) a specific tweet. Like comments, each quote is a full tweet object: you get the added commentary, engagement metrics, and author profile. ### Simplest Example ```python theme={null} def get_quotes(tweet_link): resp = requests.post( "https://api.sorsa.io/v3/quotes", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json={"tweet_link": tweet_link}, ) resp.raise_for_status() return resp.json() data = get_quotes("https://x.com/elonmusk/status/1234567890") for quote in data.get("tweets", []): print(f"@{quote['user']['username']} quoted: {quote['full_text'][:80]}") ``` ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `tweet_link` | string | Yes | Full URL of the original tweet, or just the tweet ID. | | `next_cursor` | string | No | Pagination cursor. | ### Paginating Through All Quotes ```python theme={null} def get_all_quotes(tweet_link, max_pages=20): """Fetch all quote tweets of a specific tweet.""" all_quotes = [] next_cursor = None for page in range(max_pages): body = {"tweet_link": tweet_link} if next_cursor: body["next_cursor"] = next_cursor resp = requests.post( "https://api.sorsa.io/v3/quotes", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json=body, ) resp.raise_for_status() data = resp.json() quotes = data.get("tweets", []) all_quotes.extend(quotes) next_cursor = data.get("next_cursor") if not next_cursor: break time.sleep(0.1) return all_quotes ``` Since each quote carries both author profile and added text, you can rank quoters by reach: ```python theme={null} quotes = get_all_quotes("https://x.com/brand/status/1234567890") # Find quotes that reached the largest audiences quotes.sort(key=lambda q: q["user"].get("followers_count", 0), reverse=True) for q in quotes[:5]: u = q["user"] print(f"@{u['username']} ({u['followers_count']:,} followers)") print(f" \"{q['full_text'][:80]}...\"\n") ``` *** ## Retweeters **Endpoint:** `POST /v3/retweeters` Returns the **users** who retweeted a specific tweet, newest first. Unlike `/comments` and `/quotes`, this endpoint returns a `UsersResponse` (array of user profiles), not a `TweetsResponse`. You get the retweeters' profiles, not tweet objects. ### Simplest Example ```python theme={null} def get_retweeters(tweet_link): resp = requests.post( "https://api.sorsa.io/v3/retweeters", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json={"tweet_link": tweet_link}, ) resp.raise_for_status() return resp.json() data = get_retweeters("https://x.com/elonmusk/status/1234567890") for user in data.get("users", []): print(f"@{user['username']} ({user['followers_count']} followers) retweeted") ``` ### Parameters | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `tweet_link` | string | Yes | Full URL of the tweet, or just the tweet ID. | | `next_cursor` | string | No | Pagination cursor. | ### Response Format Difference This is the key distinction to remember across the three engagement endpoints: | Endpoint | Returns | Response key | Contains | | :- | :- | :- | :- | | `/comments` | Tweets | `tweets` | Full tweet objects (text + author) | | `/quotes` | Tweets | `tweets` | Full tweet objects (text + author) | | `/retweeters` | Users | `users` | User profile objects only | Retweets do not have their own text (a retweet is just a redistribution of the original), so the endpoint returns the profile of each retweeter instead. ### Paginating Through All Retweeters ```python theme={null} def get_all_retweeters(tweet_link, max_pages=20): """Fetch all users who retweeted a tweet.""" all_users = [] next_cursor = None for page in range(max_pages): body = {"tweet_link": tweet_link} if next_cursor: body["next_cursor"] = next_cursor resp = requests.post( "https://api.sorsa.io/v3/retweeters", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json=body, ) resp.raise_for_status() data = resp.json() users = data.get("users", []) all_users.extend(users) next_cursor = data.get("next_cursor") if not next_cursor: break time.sleep(0.1) return all_users ``` You can sum retweeter follower counts as a potential audience indicator. This is not measured reach: audiences overlap, and following an account does not mean a person saw the tweet. ```python theme={null} retweeters = get_all_retweeters("https://x.com/brand/status/1234567890") total_reach = sum(u.get("followers_count", 0) for u in retweeters) verified_count = sum(1 for u in retweeters if u.get("verified")) print(f"Retweeters: {len(retweeters)}") print(f"Combined follower reach: {total_reach:,}") print(f"Verified retweeters: {verified_count}") ``` *** ## Full Engagement Breakdown for a Single Tweet Combining all three endpoints gives you a complete picture of a tweet's performance. Because it paginates every engagement type, this fans out into many requests (each page is one request against your quota), so reserve it for tweets you specifically need a full breakdown on: ```python theme={null} def full_engagement_report(tweet_link): """Generate a complete engagement report for a single tweet.""" tweet = get_tweet(tweet_link) print(f"Tweet by @{tweet['user']['username']}:") print(f" \"{tweet['full_text'][:100]}...\"") print(f" Likes: {tweet.get('likes_count', 0):,} | " f"Views: {tweet.get('view_count', 0):,}") print() comments = get_all_comments(tweet_link, max_pages=10) print(f"Comments: {len(comments)}") if comments: top_comment = max(comments, key=lambda c: c.get("likes_count", 0)) print(f" Most liked: @{top_comment['user']['username']} " f"({top_comment['likes_count']} likes)") print(f" \"{top_comment['full_text'][:80]}...\"") print() quotes = get_all_quotes(tweet_link, max_pages=10) print(f"Quotes: {len(quotes)}") if quotes: biggest_quoter = max(quotes, key=lambda q: q["user"].get("followers_count", 0)) print(f" Highest reach: @{biggest_quoter['user']['username']} " f"({biggest_quoter['user']['followers_count']:,} followers)") print(f" \"{biggest_quoter['full_text'][:80]}...\"") print() retweeters = get_all_retweeters(tweet_link, max_pages=10) total_reach = sum(u.get("followers_count", 0) for u in retweeters) print(f"Retweeters: {len(retweeters)}") print(f" Combined reach: {total_reach:,} followers") if retweeters: top_rt = max(retweeters, key=lambda u: u.get("followers_count", 0)) print(f" Biggest amplifier: @{top_rt['username']} " f"({top_rt['followers_count']:,} followers)") return { "tweet": tweet, "comments": comments, "quotes": quotes, "retweeters": retweeters, } report = full_engagement_report("https://x.com/brand/status/1234567890") ``` ### Sample Output ```text theme={null} Tweet by @brand: "We're excited to announce our Series B funding round of $50M..." Likes: 2,847 | Views: 892,000 Comments: 156 Most liked: @tech_journalist (89 likes) "Congrats! What's the plan for international expansion?..." Quotes: 43 Highest reach: @vc_partner (284,000 followers) "This team has been on our radar for two years. Well deserved...." Retweeters: 312 Combined reach: 4,218,000 followers Biggest amplifier: @industry_leader (892,000 followers) ``` *** ## Analyzing Multiple Tweets (Batch Pattern) When you need engagement data for a set of tweets (e.g., all posts from a campaign), fetch the tweet list first via `/user-tweets` or `/search-tweets`, then drill into each one: ```python theme={null} def compare_tweet_engagement(tweet_links): """Compare engagement breakdown across multiple tweets.""" results = [] for link in tweet_links: tweet = get_tweet(link) comments = get_all_comments(link, max_pages=3) quotes = get_all_quotes(link, max_pages=3) retweeters = get_all_retweeters(link, max_pages=3) rt_reach = sum(u.get("followers_count", 0) for u in retweeters) results.append({ "text": tweet["full_text"][:60], "likes": tweet.get("likes_count", 0), "comments": len(comments), "quotes": len(quotes), "retweets": len(retweeters), "retweet_reach": rt_reach, }) time.sleep(0.5) print(f"{'Tweet':<62} {'Likes':>6} {'Cmts':>5} {'Qts':>4} {'RTs':>4} {'RT Reach':>10}") print("-" * 100) for r in results: print(f"{r['text']:<62} {r['likes']:>6} {r['comments']:>5} " f"{r['quotes']:>4} {r['retweets']:>4} {r['retweet_reach']:>10,}") return results ``` > **Tip:** If you just need the aggregate metrics for many tweets without drilling into individual comments/quotes/retweeters, use `/tweet-info-bulk` to fetch up to 100 tweets in a single request. See [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) for more batch patterns. *** ## Exporting Engagement Data to CSV ```python theme={null} import csv def export_comments_to_csv(comments, output_file="comments.csv"): fields = [ "comment_id", "created_at", "full_text", "likes", "retweets", "author_username", "author_followers", "author_verified", ] with open(output_file, "w", newline="", encoding="utf-8") as f: writer = csv.DictWriter(f, fieldnames=fields) writer.writeheader() for c in comments: u = c.get("user", {}) writer.writerow({ "comment_id": c["id"], "created_at": c["created_at"], "full_text": c["full_text"], "likes": c.get("likes_count", 0), "retweets": c.get("retweet_count", 0), "author_username": u.get("username", ""), "author_followers": u.get("followers_count", 0), "author_verified": u.get("verified", False), }) print(f"Exported {len(comments)} comments to {output_file}") ``` The same pattern works for quotes (they are also tweet objects). For retweeters, export user fields instead of tweet fields. *** ## Verification Endpoints: Did a Specific User Engage? If you need to check whether a particular user commented on, quoted, or retweeted a tweet (common in campaign and giveaway verification), Sorsa provides dedicated verification endpoints that check a specific action. `/check-retweet` may require pagination, and `/check-quoted` returns a status rather than a boolean: * `/check-comment`: did a specific user reply to the tweet? * `/check-quoted`: did they quote it? * `/check-retweet`: did they retweet it? These are covered in depth in the [Marketing Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification) guide. *** ## Next Steps * [Search Tweets](https://docs.sorsa.io/search-tweets): find tweets by keyword, then drill into their engagement. * [Track Mentions](https://docs.sorsa.io/search-mentions): monitor mentions of your brand, then analyze engagement on the most-discussed ones. * [Competitor Analysis](https://docs.sorsa.io/Competitor-Analysis): compare engagement patterns across competitor content. * [Historical Data](https://docs.sorsa.io/historical-data): retrieve old tweets and analyze how their engagement played out. * [Marketing Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification): verify that specific users commented on or retweeted a tweet. * [API Reference](https://docs.sorsa.io/api-reference-guide): full specification for `/comments`, `/quotes`, `/retweeters`, `/tweet-info`, `/tweet-info-bulk`. # Use Cases Overview Source: https://docs.sorsa.io/use-cases-overview Sorsa API provides read-only access to public X (formerly Twitter) data: tweets, user profiles, follower graphs, engagement metrics, community data, and more. This page maps common use cases to the endpoints and guides that cover them, so you can jump straight to what you need. *** ## Social Listening & Brand Monitoring Track what people say about your brand, product, or industry in real time. Detect negative sentiment before it escalates, measure campaign impact, and understand public perception. **Key endpoints:** * `/search-tweets` - search for any keyword, hashtag, or phrase across all public tweets * `/mentions` - track every time a specific handle is tagged, with engagement filters to cut through noise * `/list-tweets` - monitor multiple accounts at once via X Lists **Guides:** [Search Tweets](https://docs.sorsa.io/search-tweets) | [Track Mentions](https://docs.sorsa.io/search-mentions) | [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring) | [Search Operators](https://docs.sorsa.io/search-operators) *** ## Competitor Intelligence Benchmark your competitors' profiles, analyze their content strategy, map their audience composition, and monitor what the public says about them. **Key endpoints:** * `/info` and `/info-batch` - profile snapshots (followers, tweets, bio) for one or many accounts * `/user-tweets` - pull a competitor's timeline to analyze content mix and top-performing posts * `/mentions` - see what users say about a competitor, filtered by engagement * `/followers` and `/verified-followers` - extract and analyze who follows a competitor * `/score` - compare crypto-ecosystem influence (for Web3/crypto accounts) **Guides:** [Competitor Analysis](https://docs.sorsa.io/Competitor-Analysis) | [Followers & Following](https://docs.sorsa.io/followers-and-following) | [Sorsa Score & Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) *** ## Lead Generation & Audience Discovery Find potential customers, partners, or community members by searching user profiles, extracting follower lists from relevant accounts, mining community membership, or identifying people actively discussing specific topics. **Key endpoints:** * `/search-users` - find accounts by bio keywords (job titles, interests, niches) * `/followers` - extract followers of a competitor or industry leader * `/community-members` - get member lists from topic-based X Communities * `/search-tweets` - find people actively tweeting about pain points your product solves **Guides:** [Target Audience Discovery](https://docs.sorsa.io/target-audiences-Discovery) | [Followers & Following](https://docs.sorsa.io/followers-and-following) | [Lists & Communities](https://docs.sorsa.io/lists-and-communities) *** ## Marketing Campaigns & Giveaway Verification Run engagement campaigns (follow, retweet, comment, quote, join community) and verify that each participant actually completed the required actions. Automate reward distribution based on verified completion. **Key endpoints:** * `/check-follow` - did the user follow your account? * `/check-retweet` - did they retweet a specific post? * `/check-comment` - did they leave a comment? * `/check-quoted` - did they quote tweet? * `/check-community-member` - did they join your community? **Guide:** [Marketing Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification) *** ## Historical Research & Data Collection Access the archive of public tweets dating back to 2006. Analyze past events, build training datasets, study how discourse evolved over time, or audit a brand's complete posting history. **Key endpoints:** * `/search-tweets` with `since:` and `until:` operators - keyword search within any historical window * `/user-tweets` - paginate through an account's complete timeline * `/tweet-info-bulk` - hydrate a list of tweet IDs with current engagement data **Guides:** [Historical Data](https://docs.sorsa.io/historical-data) | [Search Operators](https://docs.sorsa.io/search-operators) *** ## Real-Time Monitoring & Alerts Detect new tweets from specific accounts or matching specific keywords within seconds of posting. Route alerts to Slack, Discord, databases, or custom webhooks. **Key endpoints:** * `/list-tweets` - monitor a whole X List (up to 5,000 accounts) with one request per poll cycle * `/user-tweets` - track a single VIP account * `/search-tweets` with `order: "latest"` - monitor a keyword or hashtag in real time * `/mentions` - catch new @mentions as they arrive **Guide:** [Real-Time Monitoring](https://docs.sorsa.io/real-time-monitoring) *** ## Tweet & Post Analysis Analyze engagement on specific tweets: who replied, who quoted, who retweeted, and what they said. Measure reach, identify top commenters, and benchmark content performance. **Key endpoints:** * `/tweet-info` and `/tweet-info-bulk` - get full metrics for one or many tweets * `/comments` - all replies under a tweet * `/quotes` - all quote tweets * `/retweeters` - all users who retweeted * `/article` - full content of X Articles (long-form posts) **Guides:** [Tweet Engagement](https://docs.sorsa.io/tweet-engagement) | [X Articles](https://docs.sorsa.io/x-articles) *** ## Audience Geography & Demographics Understand where an account's audience is located. Map follower distribution by country for ad targeting, influencer vetting, or market expansion research. **Key endpoints:** * `/followers` - extract the follower list * `/about` - look up the country associated with each account **Guide:** [Audience Geography](https://docs.sorsa.io/Audience-Geography) *** ## Crypto & Web3 Analytics Evaluate the real influence of crypto accounts using Sorsa's proprietary scoring system, which measures recognition from influential crypto KOLs, projects, and VCs. Track score momentum, identify rising projects, and detect inorganic growth. **Key endpoints:** * `/score` and `/score-changes` - influence score and weekly/monthly deltas * `/followers-stats` - follower breakdown by category (influencers, projects, VCs) * `/top-followers` and `/top-following` - top 20 by score * `/new-followers-7d` and `/new-following-7d` - recent follow activity among crypto accounts **Guide:** [Sorsa Score & Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) *** ## AI & LLM Training Data Collect large tweet datasets for fine-tuning language models, training sentiment classifiers, or building retrieval-augmented generation (RAG) pipelines. Sorsa returns clean JSON that can be piped directly into ML workflows. **Key endpoints:** * `/search-tweets` - collect tweets by topic, language, and date range at scale * `/user-tweets` - pull complete timelines for corpus building * `/comments` and `/quotes` - collect conversational data (question-answer pairs, debates) **Guides:** [Search Tweets](https://docs.sorsa.io/search-tweets) | [Historical Data](https://docs.sorsa.io/historical-data) | [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) *** ## SaaS & Analytics Platform Development Build your own social analytics tool, dashboard, or monitoring platform on top of Sorsa's data layer. The flat pricing model (one API call counts as one request, regardless of how much data it returns) and 20 req/s rate limit make it feasible to serve multiple end users from a single API key. **Key endpoints:** All endpoints are available on every plan. See the [API Reference](https://docs.sorsa.io/api-reference-guide) for the full specification. **Guides:** [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) | [Pricing](https://docs.sorsa.io/pricing) | [Rate Limits](https://docs.sorsa.io/rate-limits) *** ## Quick Endpoint Finder If you already know what data you need, find the right endpoint here: | I need... | Endpoint | Guide | | :- | :- | :- | | Tweets matching a keyword | `/search-tweets` | [Search Tweets](https://docs.sorsa.io/search-tweets) | | Mentions of an account | `/mentions` | [Track Mentions](https://docs.sorsa.io/search-mentions) | | A user's profile data | `/info` or `/info-batch` | [API Reference](https://docs.sorsa.io/api-reference-guide) | | An account's followers | `/followers` | [Followers & Following](https://docs.sorsa.io/followers-and-following) | | Who an account follows | `/follows` | [Followers & Following](https://docs.sorsa.io/followers-and-following) | | A user's tweet history | `/user-tweets` | [Historical Data](https://docs.sorsa.io/historical-data) | | Replies to a tweet | `/comments` | [Tweet Engagement](https://docs.sorsa.io/tweet-engagement) | | Quote tweets of a tweet | `/quotes` | [Tweet Engagement](https://docs.sorsa.io/tweet-engagement) | | Who retweeted a tweet | `/retweeters` | [Tweet Engagement](https://docs.sorsa.io/tweet-engagement) | | Full metrics of a tweet | `/tweet-info` | [Tweet Engagement](https://docs.sorsa.io/tweet-engagement) | | Metrics for 100 tweets at once | `/tweet-info-bulk` | [Optimizing API Usage](https://docs.sorsa.io/optimizing-api-usage) | | Tweets from an X List | `/list-tweets` | [Lists & Communities](https://docs.sorsa.io/lists-and-communities) | | Members of an X List | `/list-members` | [Lists & Communities](https://docs.sorsa.io/lists-and-communities) | | Members of a Community | `/community-members` | [Lists & Communities](https://docs.sorsa.io/lists-and-communities) | | Tweets in a Community | `/community-tweets` | [Lists & Communities](https://docs.sorsa.io/lists-and-communities) | | Whether a user follows an account | `/check-follow` | [Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification) | | Whether a user retweeted | `/check-retweet` | [Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification) | | Whether a user commented | `/check-comment` | [Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification) | | Whether a user quoted | `/check-quoted` | [Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification) | | Community membership check | `/check-community-member` | [Campaign Verification](https://docs.sorsa.io/Marketing-Campaign-Verification) | | An account's country | `/about` | [Audience Geography](https://docs.sorsa.io/Audience-Geography) | | Sorsa Score (crypto influence) | `/score` | [Sorsa Score & Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) | | Username to User ID | `/username-to-id` | [ID Conversion](https://docs.sorsa.io/ID-Conversion) | | User ID to Username | `/id-to-username` | [ID Conversion](https://docs.sorsa.io/ID-Conversion) | | Profile URL to User ID | `/link-to-id` | [ID Conversion](https://docs.sorsa.io/ID-Conversion) | | API key usage stats | `/key-usage-info` | [Pricing](https://docs.sorsa.io/pricing) | | X Article content | `/article` | [X Articles](https://docs.sorsa.io/x-articles) | | Users matching a bio keyword | `/search-users` | [Target Audience Discovery](https://docs.sorsa.io/target-audiences-Discovery) | | Verified followers of an account | `/verified-followers` | [Followers & Following](https://docs.sorsa.io/followers-and-following) | | Followers of an X List | `/list-followers` | [Lists & Communities](https://docs.sorsa.io/lists-and-communities) | | Search tweets within a Community | `/community-search-tweets` | [Lists & Communities](https://docs.sorsa.io/lists-and-communities) | | Follower breakdown by category | `/followers-stats` | [Sorsa Score & Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) | | Score changes over time | `/score-changes` | [Sorsa Score & Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) | | Top followers by Sorsa Score | `/top-followers` | [Sorsa Score & Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) | | Top following by Sorsa Score | `/top-following` | [Sorsa Score & Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) | | New followers in last 7 days | `/new-followers-7d` | [Sorsa Score & Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) | | New following in last 7 days | `/new-following-7d` | [Sorsa Score & Crypto Analytics](https://docs.sorsa.io/sorsa-score-and-crypto-analytics) | *** ## Not Sure Where to Start? 1. **Get your API key** - follow the [Quickstart](https://docs.sorsa.io/quickstart) guide. Every new account includes 100 free requests, with no credit card required and no expiry. 2. **Try a search** - paste this into your terminal and see data in 10 seconds: ```bash theme={null} curl -X POST https://api.sorsa.io/v3/search-tweets \ -H "ApiKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "your topic here", "order": "popular"}' ``` 3. **Explore without code** - use the [API Playground](https://api.sorsa.io/playground) to test any endpoint through a visual interface. 4. **Pick a guide** from the list above that matches your use case. 5. **Check pricing** - see [Pricing](https://docs.sorsa.io/pricing) to estimate your monthly cost. # X Articles Source: https://docs.sorsa.io/x-articles Retrieve the full content and metadata of any public X Article in a single request. An Article is a long-form post on X with a cover image, rich text body (up to \~100,000 characters), and engagement metrics separate from its announcement tweet. > **Free to try:** `/article` works on your first 100 requests: one-time, no credit card, no expiry. An article fetch is a single request with no per-character charge, however long the body, so that is up to 100 full articles at no cost. > **Note:** For a fuller guide on working with long-form X posts, see [X Articles API: Retrieve Long-Form X Posts](https://api.sorsa.io/blog/x-articles-api) on the blog. *** ## Quickstart ```bash theme={null} curl -X POST https://api.sorsa.io/v3/article \ -H "ApiKey: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"tweet_link": "https://x.com/SorsaApp/status/1234567890"}' ``` ```python theme={null} import requests API_KEY = "YOUR_API_KEY" def get_article(tweet_link): resp = requests.post( "https://api.sorsa.io/v3/article", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json={"tweet_link": tweet_link}, timeout=30, ) resp.raise_for_status() return resp.json() article = get_article("https://x.com/SorsaApp/status/1234567890") print(f"Author: @{article['author']['username']}") print(f"Published: {article['published_at']}") print(f"Views: {article['views_count']:,}") print(f"Body length: {len(article['full_text'])} characters") ``` *** ## Endpoint ```text theme={null} POST /v3/article ``` ### Request Body | Parameter | Type | Required | Description | | :- | :- | :- | :- | | `tweet_link` | string | Yes | Full URL of the announcement tweet, or just the numeric tweet ID. | ### Response ```json theme={null} { "full_text": "this isn't a cosmetic rebrand. it's a response to how crypto twitter actually works in 2026...", "preview_text": "this isn't a cosmetic rebrand. it's a response to how crypto twitter actually works in 2026.\nthe old model was simple...", "cover_image_url": "https://pbs.twimg.com/media/G-t2hYTaIAAstc8.jpg", "published_at": "2026-01-15T16:24:02Z", "views_count": 36538, "likes_count": 315, "bookmark_count": 38, "quote_count": 37, "reply_count": 80, "retweet_count": 41, "author": { "id": "1934538036466810880", "username": "SorsaApp", "display_name": "Sorsa", "description": "Crypto social analytics made simple...", "followers_count": 6050, "verified": false } } ``` ### Response Fields | Field | Type | Description | | :- | :- | :- | | `full_text` | string | Complete article body. Can be tens of thousands of characters. | | `preview_text` | string | Truncated snippet shown in the timeline before "Read more." | | `cover_image_url` | string \| null | URL of the cover image. `null` if no cover was set. | | `published_at` | string (ISO 8601) | Publication timestamp (distinct from the announcement tweet's `created_at`). | | `likes_count` | integer | Likes. | | `retweet_count` | integer | Retweets. | | `reply_count` | integer | Replies. | | `quote_count` | integer | Quote tweets. | | `bookmark_count` | integer | Bookmarks. | | `views_count` | integer | Total impressions. | | `author` | object | Full author profile (same fields as the standard User object). | > **Field naming.** The engagement fields on the article response (`likes_count`, `retweet_count`, `reply_count`, `quote_count`, `bookmark_count`) match the standard Tweet object, with one exception: impressions come back as `views_count` here, versus `view_count` (singular) on a tweet. If you run tweets and articles through the same pipeline, normalize that one key at ingestion. See [Response Format](https://docs.sorsa.io/response-format) for the full schema comparison. *** ## Detecting an Article vs a Regular Tweet When you know the content type, call the matching endpoint directly. For mixed input, the helper below tries `/article` and falls back on a `404` or an empty article body. A `404` can also mean the resource is unavailable; the fallback may fail too. Authentication, quota, rate-limit, and server errors are propagated rather than silently treated as ordinary tweets. ```python theme={null} def get_content(tweet_link): """Fetch a tweet or article, returning the appropriate object.""" try: article = get_article(tweet_link) if article.get("full_text"): return {"type": "article", "data": article} except requests.HTTPError as error: if error.response is None or error.response.status_code != 404: raise resp = requests.post( "https://api.sorsa.io/v3/tweet-info", headers={"ApiKey": API_KEY, "Content-Type": "application/json"}, json={"tweet_link": tweet_link}, timeout=30, ) resp.raise_for_status() return {"type": "tweet", "data": resp.json()} ``` *** ## Next Steps * [Search Tweets](https://docs.sorsa.io/search-tweets): find articles by keyword. * [Tweet Engagement](https://docs.sorsa.io/tweet-engagement): get comments, quotes, and retweets on an article tweet. * [Historical Data](https://docs.sorsa.io/historical-data): retrieve articles published months or years ago. * [API Reference](https://docs.sorsa.io/api-reference-guide): full specification for `/article` and all Sorsa API endpoints.