Key Takeaway. Twitter audience geography is the per-account country distribution of an X profile's followers, derived from the platform's "Account Based In" metadata rather than the self-reported bio location. You can build it by pulling a follower list, looking up each follower's country, and aggregating the results. This works for your own account and any public account.
X (formerly Twitter) used to expose audience country data inside the native analytics dashboard for your own account, but it has never let you see it for anyone else's account. There is no public button that tells you which countries an influencer's followers actually live in, or how a competitor's audience splits across markets. The official X API only exposes geography at the tweet level (rare, since most users do not geotag), or through the Profile Geo enrichment in the Enterprise tier, which most teams cannot afford.
The Sorsa API solves this with a single endpoint, /about, that returns the country associated with any public X account. In this guide I will walk through where that country value actually comes from inside X, why it is more reliable than parsing the bio "Location" field, how to combine it with the /followers endpoint to build a full geographic profile of any account's audience, and four production pipelines you can adapt to your stack. The code is in Python, but the same logic translates to any HTTP client.
Having worked with the Twitter API since the v1.1 era and migrated more than 40 client pipelines off the official API after the 2023 pricing changes, audience geography is one of the most common asks I get from growth and research teams. It is also one of the easiest to get wrong.
Table of Contents
- Where X actually stores geography
- Why audience geography is hard to get
- Four real use cases
- How accurate is the country data
- The /about endpoint reference
- Pipeline 1: Single account lookup
- Pipeline 2: Full audience geography
- Pipeline 3: Competitor comparison
- Pipeline 4: Bot detection by country pattern
- Exporting and visualising
- How much does this cost
- Getting started
- FAQ
Where X actually stores geography
To do this correctly you need to know that X carries three different geographic signals for an account, and they are not interchangeable. Most articles on this topic conflate them.
1. Bio "Location" field. The free-form text users type into their profile. It is unstandardised, often a joke ("the internet", "my mom's basement"), often empty, and never validated. Scraping this field is what every legacy social-listening tool did pre-2023, and it is also why their geographic charts were never accurate. About 30 to 40 percent of accounts leave it blank or fill it with non-geographic text.
2. Tweet-level Place data. When a user geotags a tweet, the payload includes a Place object with a country code. According to the official X documentation, this is opt-in at the tweet level and the precise-location feature was removed from the iOS and Android apps in 2019. Less than 3 percent of tweets carry geo metadata, so this is useless for audience-level analysis.
3. Account Based In. Around late 2024 X started surfacing an internal field called account_based_in on the public profile "About" tab. This is the country X has associated with the account based on platform-level signals (IP, device data, payment methods, behaviour), not on anything the user typed. It is the same field that powers the country labels you see on the public "About" tab in the X mobile app, and it is the source the recent crop of country-badge browser extensions read from X's GraphQL AboutAccountQuery.
The Sorsa /about endpoint returns this third signal. That matters because it is the most reliable of the three for aggregate analysis: it is filled in for the vast majority of active accounts, it is platform-derived rather than self-reported, and it does not depend on the user enabling tweet-level geotagging.
Why audience geography is hard to get
If the data exists, why doesn't every analytics tool show it? Three reasons.
First, the official X API does not expose account_based_in as a queryable field outside of the user's own analytics. You can get it for a single profile by parsing the public About tab, but the API surface for it is not documented. The Enterprise tier offers Profile Geo enrichment as a separate paid add-on, with enterprise-only operators like profile_country for filtering tweets by author country, but that is a five-figure annual contract and it works at the tweet stream level rather than at the audience level.
Second, the volume problem. To know where a competitor's audience lives, you need to look up the country for every follower, not just the account itself. For an account with 100,000 followers, that is 100,000 lookups (plus a few hundred follower-list pages). The official API rate limits, even on paid tiers, make this impractical. Profile lookups on the v2 API are capped at 900 requests per 15-minute window on most paid tiers, which means a single competitor analysis would take days.
Third, UI tools like Circleboom, TweepsMap and Fedica only operate on your own connected account because they authenticate via OAuth as you. They cannot show another account's audience because the OAuth scope does not allow it. This is a fundamental limitation of any tool that uses the X login flow.
Sorsa sidesteps all three constraints. It exposes account_based_in directly via /about, runs on flat-rate monthly plans rather than per-request billing, and works for any public profile without requiring a user-side OAuth grant. For pricing details, see our Twitter API pricing breakdown for 2026.
Four real use cases
I will work through four scenarios where audience geography actually drives a decision. The pipelines in the second half of this article correspond directly to these.
Influencer vetting before a regional campaign
A consumer-tech brand we worked with last year was about to spend mid-five-figures on a US-focused launch campaign with six creators. Each creator claimed to reach "US tech professionals". When we ran the audit, two of the six had less than 40 percent of their followers actually based in the US. The largest creator (1.2M followers) was at 28 percent. That single audit saved roughly $35K in misallocated spend and shifted the budget toward two smaller creators with much tighter US concentrations.
This is the canonical use case. Anyone running paid influencer marketing should be vetting audience geography before signing the contract, not after.
Market expansion research
Before committing to localised content, a paid-ads budget, or a regional hire, look at what your audience already looks like. A B2B SaaS client of ours discovered through this kind of audit that 11 percent of their organic X followers were based in Germany despite zero German-language content and zero ads run there. That signal was enough to justify a German-language landing page and a small Munich-based community event, both of which paid back inside two quarters.
The reverse pattern is also useful: if a region returns near-zero followers, you know you are starting from scratch and should budget for awareness rather than conversion.
Inorganic-growth detection
If a small or mid-sized account suddenly gains thousands of followers from a country with no obvious connection to its activity, that is a strong inorganic-growth signal. A political-research lab we did some consulting for flags accounts whose follower country distribution shifts more than 15 percentage points in 30 days; almost every flagged account turns out to have either bought followers or been the target of an amplification campaign.
For brands, the same logic catches bought-follower contests and astroturf campaigns by competitors.
Cross-border conversation mapping
For academic and policy research, you can take any topic, pull recent tweets via /search-tweets or /mentions, then run the unique authors through /about to map how the conversation distributes geographically. We have seen this used for studying diaspora communities, misinformation flows, and cross-border policy discussions. The country distribution of who is actually talking about a topic, weighted by engagement, tells you a lot about whose conversation it really is.
How accurate is the country data
account_based_in is platform-derived from technical signals, so it is much more reliable than bio parsing. But it is not perfect, and it is worth being honest about the limits.
A small percentage of accounts use consistent VPNs and will be tagged with the VPN exit country rather than where the user lives. For individual lookups this matters; for aggregate audience analysis at sample sizes of 500 followers or more, VPN noise averages out and the overall distribution is reliable within a few percentage points.
Some accounts return "Unknown". This happens when the platform does not have enough data to assign a country with confidence, typically for very new accounts or accounts with very little activity. In a healthy audience sample of 1,000 followers, expect 1 to 5 percent unknowns.
The country reflects the account's current geographic association, which can change if the user genuinely relocates or switches their VPN. For long-running cohort analysis, treat the data as a snapshot rather than a stable identity.
For market sizing, ad targeting decisions, influencer vetting, and competitive benchmarking, accuracy is more than enough. For forensic single-account identification, treat any one lookup as a hypothesis rather than a conclusion.
The /about endpoint reference
Now the code. Authentication is a single header: ApiKey: YOUR_API_KEY. Full authentication details are in the Sorsa authentication docs.
curl "https://api.sorsa.io/v3/about?username=elonmusk" \
-H "ApiKey: YOUR_API_KEY"
Response:
{
"country": "United States",
"username_change_count": 1,
"last_username_change_at": "2021-01-01T00:00:00Z"
}
You can identify the account by any one of three parameters: username (handle without @), user_id (numeric stable ID), or user_link (full profile URL). For aggregate workflows I always use user_id because it does not change if the user rebrands their handle. The two bonus fields, username_change_count and last_username_change_at, are useful for stability scoring (more on that below).
One request equals one quota call on any Sorsa plan: there are no per-endpoint multipliers.
Pipeline 1: Single account lookup
The simplest case. Useful as a building block and for ad-hoc verification.
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.sorsa.io/v3"
def get_account_geo(identifier):
"""
Look up country and stability metadata for a single account.
Accepts a username, numeric user_id, or full profile URL.
"""
if identifier.startswith("http"):
params = {"user_link": identifier}
elif identifier.isdigit():
params = {"user_id": identifier}
else:
params = {"username": identifier.lstrip("@")}
resp = requests.get(
f"{BASE_URL}/about",
headers={"ApiKey": API_KEY},
params=params,
timeout=15,
)
resp.raise_for_status()
return resp.json()
data = get_account_geo("elonmusk")
print(f"Country: {data['country']}")
print(f"Handle changes: {data['username_change_count']}")
print(f"Last change: {data['last_username_change_at']}")
Output:
Country: United States
Handle changes: 1
Last change: 2021-01-01T00:00:00Z
This is the unit cost test. One call, one country. Everything in the next three pipelines is built on top of it.
Pipeline 2: Full audience geography
The core workflow. Pull the follower list of any public account, look up each follower's country, aggregate. The follower list comes from GET /followers, which returns up to 200 profiles per request (see Followers & Following for full pagination details).
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):
"""Paginate /followers up to max_pages * 200 profiles."""
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):
"""Return the country for a single user_id, or 'Unknown' on lookup failure."""
try:
resp = requests.get(
f"{BASE_URL}/about",
headers=HEADERS,
params={"user_id": user_id},
timeout=15,
)
if resp.status_code == 200:
return resp.json().get("country") or "Unknown"
return "Unknown"
except requests.RequestException:
return "Unknown"
def audience_geography(username, max_follower_pages=5, throttle=0.05):
"""
Build a Counter of {country: count} for the account's followers.
max_follower_pages of 5 means up to 1,000 sampled followers.
"""
followers = get_followers(username, max_pages=max_follower_pages)
countries = Counter()
for i, user in enumerate(followers, 1):
countries[get_country(user["id"])] += 1
if i % 100 == 0:
print(f" processed {i}/{len(followers)}")
time.sleep(throttle) # stays well inside 20 req/s
return countries, len(followers)
def print_distribution(countries, total, top_n=15):
print(f"\nTotal followers analysed: {total}")
print("-" * 50)
for country, count in countries.most_common(top_n):
pct = count / total * 100
bar = "#" * int(pct / 2)
print(f" {country:<25} {count:>5} ({pct:>5.1f}%) {bar}")
if __name__ == "__main__":
dist, total = audience_geography("competitor_handle", max_follower_pages=5)
print_distribution(dist, total)
Sample output:
Total followers analysed: 842
--------------------------------------------------
United States 287 ( 34.1%) #################
India 124 ( 14.7%) #######
United Kingdom 68 ( 8.1%) ####
Nigeria 54 ( 6.4%) ###
Canada 41 ( 4.9%) ##
Germany 38 ( 4.5%) ##
Brazil 31 ( 3.7%) #
France 27 ( 3.2%) #
Turkey 22 ( 2.6%) #
Indonesia 19 ( 2.3%) #
Unknown 15 ( 1.8%)
A few production notes from running this at scale.
Sample size. For accounts up to about 10,000 followers, sample everything. Above that, a random sample of 1,000 to 2,000 followers gives a country distribution accurate within roughly 2 percentage points for the top markets. Doubling the sample to 5,000 gets you another ~0.5pp of accuracy and costs five times more. Pick the trade-off that fits your decision.
Sampling bias. The /followers endpoint returns followers in reverse chronological order (newest first). For accounts that recently bought followers or had a viral spike, the first 1,000 followers will skew toward that event. If you care about the historical baseline, paginate deep or sample randomly across the pagination space.
Throttle. Sorsa allows 20 req/s on every plan. A time.sleep(0.05) between /about calls keeps you comfortably under that ceiling. If you need higher throughput, use a thread pool with a semaphore capped at 18 concurrent requests rather than going sequential.
Pipeline 3: Competitor comparison
The audit only becomes a decision when you compare two or more accounts side by side. This is the version I run for clients doing market positioning work.
def compare_geography(handles, max_follower_pages=3):
"""Run audience geography on multiple accounts; return comparison dict."""
results = {}
for handle in handles:
print(f"\nAnalysing @{handle}...")
dist, total = audience_geography(handle, max_follower_pages=max_follower_pages)
results[handle] = {"countries": dist, "total": total}
# union of top countries across all accounts
combined = Counter()
for r in results.values():
combined.update(r["countries"])
top_countries = [c for c, _ in combined.most_common(10)]
# print the comparison table
col_w = 12
header = f"{'Country':<22}" + "".join(f"@{h:<{col_w}}" for h in handles)
print("\n" + header)
print("-" * len(header))
for country in top_countries:
row = f"{country:<22}"
for h in handles:
t = results[h]["total"] or 1
pct = results[h]["countries"].get(country, 0) / t * 100
row += f"{pct:>6.1f}%{'':6}"
print(row)
return results
compare_geography(["your_brand", "competitor_a", "competitor_b"])
Output:
Country @your_brand @competitor_a @competitor_b
------------------------------------------------------------------
United States 12.4% 34.1% 28.7%
India 38.6% 14.7% 11.2%
United Kingdom 4.1% 8.1% 15.4%
Nigeria 3.2% 6.4% 2.1%
Germany 1.8% 4.5% 9.3%
Brazil 8.7% 3.7% 4.4%
Now you can read a strategy off the table. If you are @your_brand, your audience is strongly Indian and Brazilian while your two competitors are US- and UK-heavy. That tells you either to invest more in the markets you already own, or that you have a gap in your two largest English-language markets that competitors are filling. Either is actionable. For the wider competitive workflow this slots into, see our competitor analysis guide.
Pipeline 4: Bot detection by country pattern
A second high-value pattern. Take any list of accounts (campaign participants, recent followers, retweeters of a viral tweet) and flag the ones whose geography is suspicious for the context.
The intuition: a US local-politics account that suddenly picks up 5,000 followers, of whom 60 percent return "Unknown" country and 25 percent are concentrated in a single unrelated country, is almost certainly the target of either bought followers or a coordinated amplification campaign. Real organic audiences for a US local account look nothing like that.
def flag_inorganic_signals(user_ids, expected_top_country="United States",
expected_top_threshold=0.30,
unknown_threshold=0.20):
"""
Score a cohort of accounts. Returns a dict with flags and the underlying distribution.
- expected_top_threshold: minimum share that should be in the expected country
- unknown_threshold: maximum tolerable share of Unknown country
"""
countries = Counter()
handle_changes = []
for uid in user_ids:
resp = requests.get(
f"{BASE_URL}/about",
headers=HEADERS,
params={"user_id": uid},
timeout=15,
)
if resp.status_code != 200:
countries["Unknown"] += 1
continue
data = resp.json()
countries[data.get("country") or "Unknown"] += 1
handle_changes.append(data.get("username_change_count", 0))
time.sleep(0.05)
total = sum(countries.values()) or 1
top_country, top_count = countries.most_common(1)[0]
unknown_share = countries.get("Unknown", 0) / total
expected_share = countries.get(expected_top_country, 0) / total
high_churn_share = sum(1 for c in handle_changes if c >= 3) / (len(handle_changes) or 1)
flags = []
if expected_share < expected_top_threshold:
flags.append(f"low {expected_top_country} share: {expected_share:.1%}")
if unknown_share > unknown_threshold:
flags.append(f"high Unknown share: {unknown_share:.1%}")
if high_churn_share > 0.15:
flags.append(f"high handle-churn share: {high_churn_share:.1%}")
if top_country != expected_top_country and (top_count / total) > 0.25:
flags.append(f"unexpected top country: {top_country} ({top_count / total:.1%})")
return {
"distribution": dict(countries),
"flags": flags,
"unknown_share": unknown_share,
"expected_share": expected_share,
"high_churn_share": high_churn_share,
}
# Example: feed it the user_ids of recent followers, retweeters, or campaign participants
audit = flag_inorganic_signals(recent_follower_ids, expected_top_country="United States")
for flag in audit["flags"]:
print(f" flag: {flag}")
This pattern is the basis of the campaign-verification workflows in our marketing campaign verification guide, and it pairs naturally with verification endpoints like /check-follow and /check-retweet (see the Twitter follow checker writeup for the verification half of the puzzle).
The username_change_count field is the bonus signal here. Accounts that have changed handle three or more times are disproportionately likely to be repurposed, sold, or spam. It is not conclusive on its own (some legitimate accounts rebrand), but it is a useful third axis alongside country and Unknown share.
Exporting and visualising
For reporting, dump the distribution to CSV and pipe it into whatever you use for dashboards. The output works directly with Google Sheets, Looker Studio, Tableau, or any geo-heatmap library.
import csv
def export_to_csv(countries, total, path="audience_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)])
print(f"wrote {path}")
For a choropleth heatmap in Python, the country names returned by /about match standard English country names, so they map cleanly to ISO codes via pycountry or directly into Plotly's choropleth with locationmode="country names".
How much does this cost
Audience geography is a high-volume workflow because you are doing one /about call per follower. Concrete numbers on the Sorsa Pro plan (100,000 requests for $199/mo, $0.00199 per request):
| Workload | Requests | Cost on Pro |
|---|---|---|
| Single account lookup | 1 | $0.002 |
| 1,000-follower sample audit | ~1,005 | ~$2.00 |
| 10,000-follower full audit | ~10,050 | ~$20.00 |
| Three-competitor comparison, 1,000 each | ~3,015 | ~$6.00 |
| Monthly recurring 1,000-sample audits, 20 accounts | ~20,100 | covered by Pro |
For pricing details across all plans see the Sorsa pricing page and our 2026 Twitter API pricing article. One-off audits for a single account easily fit inside the Starter plan ($49/mo, 10,000 requests); regular agency or analytics work usually lands on Pro.
The optimisation lever: if you are doing campaign verification rather than full audience profiling, you do not need to look up every follower. You only need to look up the accounts that completed the campaign action (followed, retweeted, commented). That is usually two orders of magnitude smaller than the full follower list.
Getting started
The fastest way to try this is to grab a key from the Sorsa dashboard, run the single-account snippet from Pipeline 1 against any handle, then scale up. The endpoint reference for /about is at docs.sorsa.io, and the full audience geography workflow has a dedicated page at the audience geography docs. If you want to test interactively before writing any code, the API playground lets you hit /about from a browser form.
If you are extracting followers for the audit, the extract Twitter followers guide has the full pagination patterns and edge cases.
Disclosure. Sorsa API is our product. I have tried to keep this guide focused on the underlying problem and the data, not on the marketing. The pipelines work against any HTTP client and the principles apply regardless of which provider you choose, but the country lookup itself requires an API that exposes
account_based_in, which is not in the official X v2 spec.
FAQ
Can you see what country a Twitter account is based in without an API?
Yes for a single account, no at scale. X surfaces the country on the public "About" tab of each profile in the mobile app and through the internal AboutAccountQuery GraphQL endpoint. Several browser extensions (x-origin, Country Revealer for X, x-country-badge) read it from there and display it as a flag next to usernames. That works for one account at a time. For aggregate analysis across thousands of followers, you need an API like Sorsa's /about endpoint, which exposes the same account_based_in field at REST scale with a flat per-request cost.
Why is the bio "Location" field unreliable?
It is free-form text typed by the user, not validated, not standardised, and frequently empty or facetious. Roughly 30 to 40 percent of accounts leave it blank or fill it with non-geographic text ("the internet", "everywhere", a song lyric). Even when filled, "NYC" and "New York City" and "Big Apple" are three different strings that any naive analysis treats as different locations. The account_based_in value used by /about is platform-derived from technical signals and avoids all of these failure modes.
Does the official X API expose follower country data?
Not at the per-account level for arbitrary accounts on the v2 paid tiers. The Profile Geo enrichment, which adds country and region metadata to tweets based on the author's profile location, is an Enterprise-only add-on with five-figure pricing as documented in the X enterprise tutorials. The standard v2 user lookup endpoints do not return account_based_in. This is the gap Sorsa's /about fills: the same underlying signal, exposed as a flat REST call.
Is there a cheaper alternative to the official X API for audience geography?
Yes. The Sorsa API exposes account_based_in directly through /about on flat-rate plans starting at $49/mo. A full audience audit of a 1,000-follower sample costs about $2 on the Pro plan, against thousands per month for Enterprise Profile Geo access. The endpoint is read-only, requires no OAuth, and works on any public X account, not just your own.
How long does it take to run an audience geography audit?
For a 1,000-follower sample, with the Sorsa rate limit of 20 req/s, a sequential script finishes in about a minute. A 10,000-follower audit takes 8 to 12 minutes sequentially, or 1 to 2 minutes with a thread pool of 15 to 18 concurrent workers. The follower-list pagination is a small fraction of total time; the per-follower /about calls dominate.
What happens when the country field returns "Unknown"?
It means X did not have enough signal to assign a country with confidence, usually for very new or very low-activity accounts. In a healthy organic audience sample, expect 1 to 5 percent unknowns. A noticeably higher Unknown share (above 15 to 20 percent) in a cohort of recent followers or campaign participants is itself a useful signal: it correlates with inorganic-growth patterns and bought-follower vendors.
Can I detect bought followers using audience geography?
Yes, with caveats. Bought-follower vendors usually source from a narrow set of countries with low oversight, and their accounts often have low activity, leading to a distinctive cohort signature: high Unknown share, an unexpected top country with a sudden share spike, and elevated handle-churn rates. Pipeline 4 in this guide implements the heuristic. It is not perfect (sophisticated farms can mask country), but it catches the bulk of cheap inorganic growth.
Which Twitter API alternative supports audience geography lookups?
Sorsa API is one of the few alternatives that exposes the account_based_in field via a documented REST endpoint (/about), pairs it with bulk follower extraction at 200 profiles per request, and uses flat-rate monthly pricing rather than per-call billing. For a broader view of the alternative landscape see our Twitter API alternatives writeup; for the specific cost analysis see the 2026 pricing guide.
Daniel Kolbassen is a data engineer and API infrastructure consultant based in Austin, TX with 12+ years of experience building data pipelines around social platforms. He has worked with the X/Twitter API since the v1.1 era and has helped over 40 companies restructure their data infrastructure after the 2023 pricing overhaul. He writes about APIs, scraping, and social data engineering at api.sorsa.io/blog.