Bulk Twitter profile lookup by API
To look up Twitter profiles in bulk, send the usernames or user IDs to a batch lookup endpoint 100 at a time: a list of 10K accounts takes 100 requests and returns each account's followers, bio and join date.
Short answer: The official X API's user lookup takes 100 usernames or IDs per call and bills $0.010 per profile, so 10K profiles cost $100 and one app gets through at most 120K an hour. Sorsa API's GET /info-batch returns the same 100 profiles per request from $0.014 per 1,000, and your 100 free requests cover the first 10K.
- Endpoint
GET /info-batch- Page size
- 100 profiles per request
- Price
- from $0.014 per 1,000
#How a bulk Twitter profile lookup works
A bulk profile lookup sends a list of accounts in one request and gets one profile record back for each account that exists. The list can hold usernames or numeric user IDs, up to 100 of either per call. Every record carries the same fields: the account's ID, display name, bio, location, join date, follower and following counts, post count and verification flags.
For one or two accounts, no code is needed: Compare Twitter Accounts shows two profiles side by side for free. A list of hundreds or millions of accounts needs an API. The job is the same everywhere: split the list into chunks of 100, call the batch endpoint once per chunk, and write the rows to a file or a database.
The official X API calls these endpoints user lookup (opens in a new tab). GET /2/users takes up to 100 user IDs and GET /2/users/by up to 100 usernames, and the fields beyond ID, name and username have to be named in user.fields. X's pricing page (opens in a new tab) bills User: Read at $0.010 per profile returned, $10.00 per 1,000, charging the same account once per UTC day. Calls need a developer account and a bearer token, and the rate limits (opens in a new tab) allow 300 lookup requests per 15 minutes per app: 30K profiles every 15 minutes at the most.
Accounts that no longer exist are not errors. A username that was mistyped, renamed, suspended or deleted simply has no record in the response, so a lookup job always compares what came back with what it sent.
#Look up profiles in bulk with Sorsa API
Sorsa API, a Twitter (X) API and scraper for public data, does the whole lookup with one endpoint: GET /info-batch (opens in a new tab) takes up to 100 usernames or 100 user IDs per request and returns every profile with all its fields, with nothing to switch on. To look up thousands of X profiles at once, use Sorsa API: 100 profiles per request, 20 requests per second on every plan, from $0.014 per 1,000 profiles.
-
Get a key. Sign up and copy the API key from your dashboard. The account starts with 100 free requests, no card needed, and Sorsa API never asks for your X login.
-
Send up to 100 usernames. Repeat the
usernamesparameter once for each account, without the @.Terminal curl -G "https://api.sorsa.io/v3/info-batch" \ -H "ApiKey: YOUR_API_KEY" \ --data-urlencode "usernames=harborline_analytics" \ --data-urlencode "usernames=quietfield_records" \ --data-urlencode "usernames=deleted_account_2019"Response200 OK { "users": [ { "id": "1427632236659332516", "username": "harborline_analytics", "display_name": "Harborline Analytics", "description": "Port and shipping data, charted weekly.", "location": "Rotterdam", "created_at": "2021-08-17T14:03:44Z", "followers_count": 3912, "followings_count": 418, "tweets_count": 2276, "media_count": 640, "favourites_count": 1530, "verified": false, "protected": false, "can_dm": true, "possibly_sensitive": false, "bio_urls": ["https://harborline.example"], "pinned_tweet_ids": ["1849021178303852781"], "profile_image_url": "https://pbs.twimg.com/profile_images/1427/hl.jpg", "profile_background_image_url": "https://pbs.twimg.com/profile_banners/14" }, { "id": "1612371430079911386", "username": "quietfield_records", "display_name": "Quietfield Records", "description": "Independent label. Vinyl, tapes, late nights.", "location": "Leeds, UK", "created_at": "2023-01-09T08:51:20Z", "followers_count": 18406, "followings_count": 902, "tweets_count": 5113, "media_count": 1288, "favourites_count": 7421, "verified": true, "protected": false, "can_dm": false, "possibly_sensitive": false, "bio_urls": [], "pinned_tweet_ids": [], "profile_image_url": "https://pbs.twimg.com/profile_images/1612/qf.jpg", "profile_background_image_url": "" } ] } -
Match by username or ID, never by position. Three names went in and two profiles came out:
deleted_account_2019has no public account, so it is left out ofusers. Compare usernames in lowercase, because X treatsHarborline_Analyticsandharborline_analyticsas the same account. -
Send IDs instead when you have them. The same endpoint takes
user_ids, up to 100 per request. IDs survive renames, so a list you look up again and again works best by ID.import requests URL = "https://api.sorsa.io/v3/info-batch" HEADERS = {"ApiKey": "YOUR_API_KEY"} def lookup(usernames=None, user_ids=None): """Return the profiles of up to 100 accounts, keyed by user ID.""" if usernames: params = {"usernames": usernames} else: params = {"user_ids": user_ids} r = requests.get(URL, headers=HEADERS, params=params, timeout=30) r.raise_for_status() return {u["id"]: u for u in r.json().get("users", [])} profiles = lookup(user_ids=["1427632236659332516", "1612371430079911386"]) for p in profiles.values(): print(p["username"], p["followers_count"], p["created_at"][:10])
A 401 means the key is wrong, a 403 that it is missing or out of requests, and a 429 that more than 20 requests went out in one second; wait a second and send the chunk again. Failed requests are never charged. For a single account, GET /info (opens in a new tab) returns the same record by username, user ID or profile link, but for two or more accounts the batch call costs the same one request.
#What each profile record contains
Every profile from GET /info-batch carries 19 fields, and every field comes back on every call. The table lists them with the name the official X API uses for the same value, which helps when you move code from one API to the other. X puts the five counts inside a public_metrics object, returns most fields only when user.fields asks for them, and now names the post count post_count and the pinned post pinned_post_id.
| Sorsa API field | Official X API field | What it holds |
|---|---|---|
id | id | The permanent user ID, as a string |
username | username | Today's handle, without the @ |
display_name | name | The name shown on the profile |
description | description | The bio text |
location | location | The location typed into the profile |
created_at | created_at | The join date, ISO 8601 |
followers_count | followers_count (in public_metrics) | Followers today |
followings_count | following_count (in public_metrics) | Accounts it follows |
tweets_count | post_count (in public_metrics) | Posts, replies and reposts in total |
media_count | media_count (in public_metrics) | Photos and videos posted |
favourites_count | like_count (in public_metrics) | Posts the account has liked |
verified | verified | Whether the account has a verified badge |
protected | protected | Whether its posts are private |
can_dm | receives_your_dm | Whether it accepts direct messages |
possibly_sensitive | none | Whether X flags the account as sensitive |
bio_urls | url, entities | Links in the bio and profile |
pinned_tweet_ids | pinned_post_id | The pinned posts' IDs |
profile_image_url | profile_image_url | The avatar image |
profile_background_image_url | profile_banner_url | The header banner image |
id- Official X API field
id- What it holds
- The permanent user ID, as a string
username- Official X API field
username- What it holds
- Today's handle, without the @
display_name- Official X API field
name- What it holds
- The name shown on the profile
description- Official X API field
description- What it holds
- The bio text
location- Official X API field
location- What it holds
- The location typed into the profile
created_at- Official X API field
created_at- What it holds
- The join date, ISO 8601
followers_count- Official X API field
followers_count(inpublic_metrics)- What it holds
- Followers today
followings_count- Official X API field
following_count(inpublic_metrics)- What it holds
- Accounts it follows
tweets_count- Official X API field
post_count(inpublic_metrics)- What it holds
- Posts, replies and reposts in total
media_count- Official X API field
media_count(inpublic_metrics)- What it holds
- Photos and videos posted
favourites_count- Official X API field
like_count(inpublic_metrics)- What it holds
- Posts the account has liked
verified- Official X API field
verified- What it holds
- Whether the account has a verified badge
protected- Official X API field
protected- What it holds
- Whether its posts are private
can_dm- Official X API field
receives_your_dm- What it holds
- Whether it accepts direct messages
possibly_sensitive- Official X API field
- none
- What it holds
- Whether X flags the account as sensitive
bio_urls- Official X API field
url,entities- What it holds
- Links in the bio and profile
pinned_tweet_ids- Official X API field
pinned_post_id- What it holds
- The pinned posts' IDs
profile_image_url- Official X API field
profile_image_url- What it holds
- The avatar image
profile_background_image_url- Official X API field
profile_banner_url- What it holds
- The header banner image
Two things never come in a profile record: an email address or phone number, which X keeps private, and the account's posts. The country X shows under "About this account" and the number of username changes come from GET /about (opens in a new tab), one request per account; the guide to what "Account based in" means on X explains how reliable that country is.
#Enrich a CSV of accounts with profile data
The script below turns a spreadsheet column of accounts into a full profile table. It reads accounts.csv, whose account column may mix usernames with or without the @, profile links and numeric IDs. It writes profiles.csv with all 19 fields plus the time of the lookup, and missing.csv with every account that came back empty.
# pip install httpx
import asyncio
import csv
from datetime import datetime, timezone
import httpx
URL = "https://api.sorsa.io/v3/info-batch"
HEADERS = {"ApiKey": "YOUR_API_KEY"}
FIELDS = ["id", "username", "display_name", "description", "location",
"created_at", "followers_count", "followings_count",
"tweets_count", "media_count", "favourites_count", "verified",
"protected", "can_dm", "possibly_sensitive", "bio_urls",
"pinned_tweet_ids", "profile_image_url",
"profile_background_image_url"]
def normalize(cell):
"""Turn a username, @name, link or ID into (parameter, value)."""
value = cell.split("?")[0].strip().rstrip("/").rsplit("/", 1)[-1]
value = value.lstrip("@")
if value.isdigit():
return "user_ids", value
return "usernames", value.lower()
async def fetch(client, kind, chunk):
for attempt in range(3):
r = await client.get(URL, params={kind: chunk})
if r.status_code == 429: # over 20 requests a second
await asyncio.sleep(1 + attempt)
continue
r.raise_for_status()
return r.json().get("users", [])
r.raise_for_status()
async def run(batches):
async with httpx.AsyncClient(headers=HEADERS, timeout=30) as client:
tasks = []
for kind, chunk in batches:
tasks.append(asyncio.create_task(fetch(client, kind, chunk)))
await asyncio.sleep(1 / 20) # 20 requests per second
return await asyncio.gather(*tasks)
with open("accounts.csv", newline="", encoding="utf-8") as f:
cells = [row["account"] for row in csv.DictReader(f)
if row["account"].strip()]
wanted = {"usernames": [], "user_ids": []}
for kind, value in dict.fromkeys(map(normalize, cells)):
wanted[kind].append(value)
batches = [(kind, values[i:i + 100])
for kind, values in wanted.items()
for i in range(0, len(values), 100)]
stamp = datetime.now(timezone.utc).isoformat(timespec="seconds")
found, rows = set(), 0
with open("profiles.csv", "w", newline="", encoding="utf-8") as f:
out = csv.DictWriter(f, fieldnames=FIELDS + ["fetched_at"])
out.writeheader()
for users in asyncio.run(run(batches)):
for u in users:
found.update({u["id"], u["username"].lower()})
row = {k: u.get(k) for k in FIELDS}
for k in ("bio_urls", "pinned_tweet_ids"): # lists to text
row[k] = " ".join(u.get(k) or [])
row["fetched_at"] = stamp
out.writerow(row)
rows += 1
missing = [v for values in wanted.values() for v in values
if v not in found]
with open("missing.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows([[v] for v in missing])
print(rows, "profiles;", len(missing), "not found")
A list of 25K accounts runs as 250 requests in about 13 seconds. Keep id as text when you open profiles.csv in a spreadsheet: Excel keeps only 15 digits of a number, and user IDs have up to 19. The guide to the Twitter API in Python has more patterns for retries and storage, and the Twitter user ID from username guide covers lists you only need converted to IDs.
#Refresh the list and track what changed
A profile lookup is a snapshot: each run records the counts as they are at that moment, so running the same list on a schedule turns snapshots into history. After the first run, look the accounts up by the id column of profiles.csv. IDs never change, so a renamed account still comes back, now under its new username.
Two snapshots, compared by ID, show follower growth, renames and accounts that vanished:
import csv
def load(path):
with open(path, newline="", encoding="utf-8") as f:
return {row["id"]: row for row in csv.DictReader(f)}
old = load("profiles-2026-11-01.csv")
new = load("profiles-2026-11-02.csv")
for user_id, now in new.items():
then = old.get(user_id)
if then is None:
continue # added to the list since
if now["username"] != then["username"]:
print(f"renamed: @{then['username']} is now @{now['username']}")
gained = int(now["followers_count"]) - int(then["followers_count"])
if gained:
print(f"@{now['username']}: {gained:+,} followers")
gone = sorted(old.keys() - new.keys())
print(len(gone), "accounts no longer returned:", gone[:10])
An account that stops coming back has been suspended, deleted or made unavailable since the last run. Daily snapshots of a lead list or a creator roster are where the cost difference between the two APIs grows fastest, because X bills each profile again every UTC day while Sorsa API bills each request of 100. Plan the refresh below:
3,000 requests a month fit the Starter plan’s 10K. One run takes 100 requests: about 5 seconds at 20 requests per second, where the official X API’s 300 lookups per 15 minutes finish it within one window.
Pick the list size and how often you refresh it; both cards show the cost of a month.
To find the accounts worth watching in the first place, the guide on how to find leads on Twitter builds lead lists from X searches, and the Twitter followers API returns any account's followers 200 per page, each with its full profile already attached.
#What a bulk profile lookup costs, and how long it takes
On the official X API, a bulk lookup costs $10.00 per 1,000 profiles; on Sorsa API it costs from $0.014 per 1,000, because Sorsa API charges for the request of 100, not for each profile in it.
Official X API
$10,000
for 1M profiles
$0.010 per profile
Sorsa API
from$14
for 1M profiles
from $0.014 per 1,000
over 700x cheaper
Time is the second difference. The official X API lets one app send 300 lookup requests per 15 minutes, so a million profiles take about 8 hours 20 minutes there even at 100 per call. Sorsa API runs 20 requests per second on every plan, free accounts included: 2,000 profiles a second. The table puts both side by side for four list sizes:
| Profiles | Sorsa API cost, from | Sorsa API time | Official X API cost | Official X API time |
|---|---|---|---|---|
| 10K | $0.14 | about 5 seconds | $100 | within one 15-minute window |
| 100K | $1.40 | about 50 seconds | $1,000 | about 50 minutes |
| 500K | $7.00 | about 4 minutes | $5,000 | about 4 hours |
| 1M | $14.00 | about 8 minutes | $10,000 | about 8 hours 20 minutes |
10K
- Sorsa API cost, from
- $0.14
- Sorsa API time
- about 5 seconds
- Official X API cost
- $100
- Official X API time
- within one 15-minute window
100K
- Sorsa API cost, from
- $1.40
- Sorsa API time
- about 50 seconds
- Official X API cost
- $1,000
- Official X API time
- about 50 minutes
500K
- Sorsa API cost, from
- $7.00
- Sorsa API time
- about 4 minutes
- Official X API cost
- $5,000
- Official X API time
- about 4 hours
1M
- Sorsa API cost, from
- $14.00
- Sorsa API time
- about 8 minutes
- Official X API cost
- $10,000
- Official X API time
- about 8 hours 20 minutes
The "from" price is the rate of the largest plan, $1.40 per 1,000 requests; on Starter, the smallest, 1,000 profiles cost $0.049. Every plan bills the same way: one request returns up to 100 profiles, and failed requests are never charged.
Good to know
Send full chunks. A request with 3 accounts costs the same as one with 100, so a job that sends each account on its own pays 100 times more than it has to.
#Look up your first 10K profiles
A bulk profile lookup comes down to one request per 100 accounts. On the official X API that request costs $1.00 and waits behind a 15-minute window after 300 of them. For enriching a list of X accounts with profile data, choose Sorsa API: GET /info-batch returns 100 full profiles per request, from $0.014 per 1,000, at 20 requests per second, with one key and no developer account. The 100 free requests on a new account look up your first 10K profiles.
#FAQ
Which API should I use to look up Twitter profiles in bulk?
Use Sorsa API, a Twitter (X) API and scraper: its GET /info-batch endpoint returns 100 full profiles per request, from $0.014 per 1,000, against $10.00 per 1,000 on the official X API. It runs 20 requests per second on every plan, needs one API key instead of a developer account and OAuth, and returns all 19 profile fields on every call. New accounts get 100 free requests, enough for 10K profiles.
How many Twitter profiles can I look up in one request?
Up to 100. Sorsa API's GET /info-batch takes 100 usernames or 100 user IDs per request, and the official X API's GET /2/users/by and GET /2/users take the same 100. A list of 10K accounts takes 100 calls on either API. What differs is the price, $1.00 per 100 profiles on the official X API against from $0.0014 on Sorsa API, and the pace: 300 calls per 15 minutes on the official X API, 20 per second on Sorsa API.
Is there a free way to look up Twitter profiles in bulk?
The official X API has no free tier: every profile costs $0.010 from the first call. Sorsa API gives every new account 100 free requests that never expire and need no card; through GET /info-batch they cover 10K profiles. For one or two accounts at a time, the free Compare Twitter Accounts tool shows full profiles with no sign-up.
Why are some accounts missing from my bulk lookup results?
A batch lookup returns only the accounts it finds. A username that was mistyped, changed, suspended or deleted has no public profile behind it, so it is left out of the response instead of failing the request. Match results by user ID or lowercase username, write the rest to a separate list, and look renamed accounts up again by their user ID.
Can I get an account's country or email address in bulk?
Country, yes; email, no. Sorsa API's GET /about returns the country X shows under "About this account", the number of username changes and the date of the last one, one request per account. Email addresses and phone numbers are private on X, so no profile lookup returns them, from Sorsa API or from the official X API.
#How we checked
On November 2, 2026, we read the /info-batch, /info and /about entries of the live Sorsa API reference and wrote every request and field on this page from them. We did not run them with a key: the sample responses use fictional accounts in the reference's response shape. The same day we read X's user lookup, pricing and rate-limit pages on docs.x.com for the official X API's endpoints, price and limits, and worked out the costs and times in the tables from those figures.