Twitter username to user ID by API
X never shows a user ID on a profile, so code gets it by sending the username to a user lookup endpoint, which returns the account's permanent ID as a string of up to 19 digits; batch lookups take 100 usernames per request.
Short answer: The official X API's GET /2/users/by takes 100 usernames per call, bills $0.010 per user and needs a developer account and a bearer token; the same lookups turn IDs back into usernames. Sorsa API converts lists through GET /info-batch, 100 usernames per request from $0.014 per 1,000, and your 100 free requests cover the first 10K.
- Endpoint
GET /info-batch- Page size
- 100 usernames per request
- Price
- from $0.014 per 1,000
#What a Twitter user ID is, and how to find one
A Twitter user ID is the permanent number X gives an account the moment it is created. The username after the @ is a label of 1 to 15 letters, digits or _ that its owner can change at any time, and once it is free, anyone can claim it. The ID never changes and never moves to another account, which is why apps, exports and databases point at accounts by ID. Developers also call it the account ID, the numeric ID or the UID.
For one account, the free Twitter ID Finder shows the ID behind any username or profile link, and turns an ID back into the account's current username. It needs no sign-up.
The form opens the free Twitter ID Finder with your username, link or ID filled in.
Early accounts got short IDs: Jack Dorsey's @jack is 12. Accounts opened in recent years have IDs of 18 or 19 digits. X doesn't print the number anywhere in its app, but your own ID is in your data archive: request it under Settings and privacy, Your account, Download an archive of your data, then open data/account.js and read accountId.
A user ID is not a post ID. The long number after /status/ in a post link names the post, and the Tweet ID to Date Converter reads the date inside it. Send a post ID to a user lookup and it finds no account.
#Convert a username to a user ID with Sorsa API
Sorsa API, a Twitter (X) API and scraper for public data, has an endpoint for each direction, and each conversion is one request: from $1.40 per 1,000 conversions. Every call takes your key in the ApiKey header and returns the ID as a JSON string, so no digit gets lost on the way.
-
Get a key. Sign up and copy your API key from the dashboard. It comes with 100 free requests and needs no card.
-
Send the username. Pass it without the @ to
GET /username-to-id(opens in a new tab).Terminal curl "https://api.sorsa.io/v3/username-to-id/harborline_analytics" \ -H "ApiKey: YOUR_API_KEY"Response200 OK { "id": "1427632236659332516" } -
Go the other way when you need to.
GET /id-to-username(opens in a new tab) returns the account's current username in a field namedhandle, andGET /link-to-id(opens in a new tab) takes a full x.com or twitter.com profile link. -
Wrap the three calls. The same conversions in Python and Node.js:
import requests BASE = "https://api.sorsa.io/v3" HEADERS = {"ApiKey": "YOUR_API_KEY"} def username_to_id(username): r = requests.get(f"{BASE}/username-to-id/{username.lstrip('@')}", headers=HEADERS, timeout=30) r.raise_for_status() return r.json()["id"] # a string: keep it one def id_to_username(user_id): r = requests.get(f"{BASE}/id-to-username/{user_id}", headers=HEADERS, timeout=30) r.raise_for_status() return r.json()["handle"] # today's username, without the @ def link_to_id(link): r = requests.get(f"{BASE}/link-to-id", headers=HEADERS, params={"link": link}, timeout=30) r.raise_for_status() return r.json()["id"] print(username_to_id("harborline_analytics")) # 1427632236659332516One endpoint per input, and one request per call. A list of up to 100 goes through GET /info-batch in a single request.
A 404 means no public account holds that username or ID: it was mistyped, renamed, suspended or deleted. A 401 means the key is wrong, a 403 that it is missing or out of requests, and a 429 that you sent more than 20 requests in one second. Failed requests are never charged. When you need the profile as well, GET /info (opens in a new tab) returns the ID with the whole profile in the same single request.
#Convert a list: 100 usernames per request
For a list, skip the one-by-one endpoints: GET /info-batch (opens in a new tab) takes up to 100 usernames per request and returns each account's profile with its id. A list of 10K usernames takes 100 requests, costs from $0.14 and finishes in about 5 seconds at 20 requests per second. To convert Twitter usernames to IDs in bulk, use Sorsa API's GET /info-batch: 100 per request, from $0.014 per 1,000, against $10.00 per 1,000 on the official X API.
Repeat the usernames parameter once for each name:
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=copperkettle_maps"
{
"users": [
{
"id": "1427632236659332516",
"username": "harborline_analytics",
"display_name": "Harborline Analytics",
"created_at": "2021-08-17T14:03:44Z",
"followers_count": 3912,
"protected": false
},
{
"id": "1612371430079911386",
"username": "quietfield_records",
"display_name": "Quietfield Records",
"created_at": "2023-01-09T08:51:20Z",
"followers_count": 18406,
"protected": false
},
{
"id": "1804185541935281502",
"username": "copperkettle_maps",
"display_name": "Copperkettle Maps",
"created_at": "2024-06-21T16:12:08Z",
"followers_count": 671,
"protected": false
}
]
}
The response lists only the accounts it found. A username that matches no public account is left out, so match the results by username, never by position, and compare in lowercase: X treats Harborline_Analytics and harborline_analytics as the same name.
The script below reads accounts.csv with a username column, drops duplicates and stray @ signs, and writes accounts_with_ids.csv. Ten threads share one pacer that keeps the key at 20 requests per second, so a list of 1M usernames goes through in about 8 minutes.
import csv
import threading
import time
from concurrent.futures import ThreadPoolExecutor
import requests
URL = "https://api.sorsa.io/v3/info-batch"
HEADERS = {"ApiKey": "YOUR_API_KEY"}
PER_SECOND = 20 # the limit on every plan
lock, next_slot = threading.Lock(), [0.0]
def wait_turn():
"""Space requests 1/20 of a second apart across all threads."""
with lock:
now = time.monotonic()
slot = max(now, next_slot[0])
next_slot[0] = slot + 1 / PER_SECOND
time.sleep(slot - now)
def fetch(params):
for _ in range(3):
wait_turn()
r = requests.get(URL, headers=HEADERS, params=params, timeout=30)
if r.status_code != 429: # 429: over 20 requests a second
r.raise_for_status()
return r.json().get("users", [])
time.sleep(1)
r.raise_for_status()
def usernames_to_ids(usernames):
"""Map each username (lowercase) to its user ID, 100 per request."""
names = list(dict.fromkeys(u.strip().lstrip("@").lower()
for u in usernames if u.strip()))
chunks = [names[i:i + 100] for i in range(0, len(names), 100)]
found = {}
with ThreadPoolExecutor(max_workers=10) as pool:
for users in pool.map(lambda c: fetch({"usernames": c}), chunks):
for user in users:
found[user["username"].lower()] = user["id"]
missing = [n for n in names if n not in found]
return found, missing
with open("accounts.csv", newline="", encoding="utf-8") as f:
rows = [row["username"] for row in csv.DictReader(f)]
ids, missing = usernames_to_ids(rows)
with open("accounts_with_ids.csv", "w", newline="", encoding="utf-8") as f:
out = csv.writer(f)
out.writerow(["username", "user_id"])
for name, user_id in ids.items():
out.writerow([name, user_id]) # the ID stays a string
print(len(ids), "converted;", len(missing), "not found:", missing[:10])
For retries, cursors and async requests beyond this script, the guide to the Twitter API in Python has working patterns. Follower lists need no conversion at all: the Twitter followers API returns 200 followers per page, each with its id already in place.
#Turn user IDs back into usernames and catch renames
The same endpoint works backwards: send up to 100 stored IDs as user_ids, and each profile comes back with the username it uses today. Compare that with the username you saved: a difference is a rename, and an ID that doesn't come back belongs to an account that is suspended or deleted.
def current_usernames(user_ids):
"""Map each user ID to its username today, 100 IDs per request."""
now = {}
for i in range(0, len(user_ids), 100):
for user in fetch({"user_ids": user_ids[i:i + 100]}):
now[user["id"]] = user["username"]
return now
stored = {"1427632236659332516": "harborline_analytics",
"1612371430079911386": "quietfield_records"}
today = current_usernames(list(stored))
for user_id, old in stored.items():
new = today.get(user_id)
if new is None:
print(user_id, "is gone: suspended or deleted")
elif new.lower() != old.lower():
print(user_id, "renamed from", old, "to", new)
Run it on a schedule against the accounts you track, and a renamed competitor or creator never drops out of your data. Giveaway and quest entrants work the same way: store each entrant by ID, and the Twitter verification API checks their follows and reposts by that ID even after a rename.
For one account, GET /about (opens in a new tab) adds username_change_count and last_username_change_at: how many times it changed its username and when it last did. The free Twitter Username History Checker reads both facts without code. The old usernames themselves aren't public, because X doesn't publish them.
To send a person to an account that may rename, link to x.com/i/user/ followed by the ID: X opens the profile under its current username. The link x.com/i/user/12 lands on @jack.
#Clean up mixed input before you convert
Real lists mix formats: usernames with and without the @, x.com and twitter.com links with tracking parameters, mobile links, /i/user/ links and bare numbers. Sort each value before you call anything, so IDs skip the lookup and junk never costs a request.
Each common format reads as a username, a user ID or nothing:
| Input | Read as |
|---|---|
jack, with or without the @ | Username |
https://twitter.com/jack?s=20 | Username |
https://mobile.twitter.com/jack | Username |
https://x.com/jack/status/20 | Username of the post's author when the link was copied |
https://x.com/i/user/12 | User ID |
https://twitter.com/intent/user?user_id=12 | User ID |
12 or 1427632236659332516 | User ID |
https://x.com/home, a hashtag, an email | Not an account |
jack, with or without the @- Read as
- Username
https://twitter.com/jack?s=20- Read as
- Username
https://mobile.twitter.com/jack- Read as
- Username
https://x.com/jack/status/20- Read as
- Username of the post's author when the link was copied
https://x.com/i/user/12- Read as
- User ID
https://twitter.com/intent/user?user_id=12- Read as
- User ID
12or1427632236659332516- Read as
- User ID
https://x.com/home, a hashtag, an email- Read as
- Not an account
import re
USERNAME = re.compile(r"^[A-Za-z0-9_]{1,15}$") # X's own pattern
RESERVED = {"home", "explore", "search", "i", "intent", "settings",
"messages", "notifications", "hashtag", "share", "compose"}
LINK = re.compile(r"^(?:https?://)?(?:www\.|mobile\.)?"
r"(?:x|twitter)\.com/([^?#]+)", re.I)
def classify(value):
"""Return ("id", ...), ("username", ...) or ("bad", value)."""
v = value.strip()
if v.isdigit(): # digits alone: an ID
return ("id", v) if len(v) <= 19 else ("bad", value)
q = re.search(r"[?&]user_id=(\d{1,19})\b", v)
if q: # .../intent/user?user_id=
return ("id", q.group(1))
m = LINK.match(v)
if m:
parts = m.group(1).strip("/").split("/")
if parts[:2] == ["i", "user"]: # x.com/i/user/<ID>
ok = len(parts) > 2 and parts[2].isdigit()
return ("id", parts[2]) if ok else ("bad", value)
v = parts[0] # a profile or a post link
v = v.lstrip("@")
if USERNAME.match(v) and v.lower() not in RESERVED:
return ("username", v.lower())
return ("bad", value)
X's API reference (opens in a new tab) gives the username pattern as 1 to 15 letters, digits or _, and the pattern allows digits alone. So a bare number could be a username as well as an ID. The function reads bare digits as an ID and digits with an @ in front as a username: keep the @ in your input when you mean a name. The demo usernames elsewhere on this page run past 15 characters on purpose, so none of them can belong to a real account.
The username inside a post link is the author's name on the day the link was copied. If the account renamed since, that name finds nothing, or someone else who claimed it. When a dataset holds post links, look the posts up and take each author's ID from the post's data: no rename changes it.
#Store the ID as a string, never a number
Keep every Twitter ID a string from the API response to the database. A 19-digit ID is larger than the biggest whole number a JavaScript number or a spreadsheet cell holds exactly, so a number type quietly changes its last digits, and the changed ID points at no account.
- JavaScript.
Number.MAX_SAFE_INTEGERis 9007199254740991, 16 digits. X's own guide to IDs (opens in a new tab) tells developers to always use string IDs for this reason. Parse an unquoted ID and1427632236659332516turns into1427632236659332600. - Excel. Excel stores numbers with 15 digits of precision and shows big ones in scientific notation, so the same ID opens as
1.42763E+18and is stored as1427632236659330000(Microsoft Support (opens in a new tab)). Import the CSV through Data, From Text/CSV and set the ID column to Text before loading. - pandas. Read the file with
dtype={"user_id": str}, or the column turns into numbers too. - Databases. X recommends
BIGINTorTEXTin PostgreSQL,BIGINT UNSIGNEDorVARCHAR(20)in MySQL, a string in MongoDB andTEXTin SQLite. A 32-bitINToverflows past 2147483647.
Paste any ID below to see what each of these does to it.
-
As a string: JSON,
TEXT,VARCHAR(20)1427632236659332516 Exact -
As a 64-bit
BIGINT1427632236659332516 Exact - As a JavaScript number 1427632236659332600 Last 3 digits changed
- In an Excel cell (shows 1.42763E+18) 1427632236659330000 Last 4 digits zeroed
-
As a 32-bit
INTOverflows Max 2147483647
A string or a 64-bit integer keeps every digit; a JavaScript number and an Excel cell change the last ones.
The same rule holds for exports. When you export Twitter followers or any list of accounts to CSV, open the file with the ID column set to Text, or the IDs you saved stop matching the API's.
Good to know
Sorsa API returns every ID as a JSON string: user IDs, post IDs and the IDs inside lists such as
pinned_tweet_ids. The digits only change when your own code or spreadsheet turns them into numbers, so the fix is always on the reading side.
#What converting usernames costs, and how long it takes
Converting usernames to IDs costs $10.00 per 1,000 on the official X API and from $0.014 per 1,000 on Sorsa API. X bills every user a lookup returns (opens in a new tab) at $0.010, whether the call carries one username or 100. Sorsa API bills the request, and one GET /info-batch request carries 100 usernames.
Official X API
$10,000
for 1M usernames
$0.010 per user
Sorsa API
from$14
for 1M usernames
from $0.014 per 1,000
over 700x cheaper
Time is the other gap. X's rate limits (opens in a new tab) let one app make 300 user lookups per 15 minutes, so 1M usernames take about 8 hours 20 minutes even at 100 per call. Sorsa API runs 20 requests per second on every plan and converts the same list in about 8 minutes 20 seconds.
On every criterion this job depends on, Sorsa API comes out ahead of the official X API:
| Criterion | Sorsa API | Official X API |
|---|---|---|
| Price per 1,000 usernames | from $0.014 | $10.00 |
| Time for 1M usernames | about 8 minutes | about 8 hours 20 minutes |
| Setup | one API key in a header | developer account, app, bearer token |
| IDs back to usernames | same endpoint, same price | $0.010 per user |
| Your X login | never asked for | the developer account sits on your X account |
Price per 1,000 usernames
- Sorsa API
- from $0.014
- Official X API
- $10.00
Time for 1M usernames
- Sorsa API
- about 8 minutes
- Official X API
- about 8 hours 20 minutes
Setup
- Sorsa API
- one API key in a header
- Official X API
- developer account, app, bearer token
IDs back to usernames
- Sorsa API
- same endpoint, same price
- Official X API
- $0.010 per user
Your X login
- Sorsa API
- never asked for
- Official X API
- the developer account sits on your X account
The official X API's full price list, posts and followers included, is in our guide to X API pricing.
#Convert your first list
Sorsa API turns Twitter usernames into user IDs at 100 per request, from $0.014 per 1,000, and turns stored IDs back into today's usernames at the same price. It is a Twitter API without a developer account: one key in a header, no OAuth and no app review. Your 100 free requests convert the first 10K usernames, so paste the batch script above, add your key and run it on your list.
#FAQ
Which API should I use to convert Twitter usernames to user IDs?
Use Sorsa API. Its GET /info-batch converts 100 usernames per request from $0.014 per 1,000, against $10.00 per 1,000 on the official X API, and needs one API key instead of a developer account and a bearer token. It runs at 20 requests per second on every plan, so 1M usernames take about 8 minutes, and it never asks for your X login.
Can a Twitter user ID change?
No. X assigns the user ID when the account is created and keeps it through every rename, display-name change, new email or lifted suspension. What changes is the username that points to it: after a rename the old username is free, and a stranger can claim it, so a stored username can lead to a different person a month later.
Is an X Number the same as a Twitter user ID?
No. An X Number is a private contact code X added to Chat in September 2026: you create it, share it with people you want to hear from, and refresh it or turn it off whenever you like (X Help Center (opens in a new tab)). A user ID is the account's permanent public number. X never reveals anyone's X Number, so no lookup tool or API returns it.
Can I get an account's creation date from its user ID?
From an ID of 18 or 19 digits, yes: the creation time is part of the number. Shift the ID right by 22 bits, add 1288834974657 and pass the result to a JavaScript Date. The short IDs of the first accounts hold no date. Sorsa API's GET /info returns the exact created_at of any account, whatever the length of its ID.
Why does a username return no user ID?
Because no public account holds that username right now: it was mistyped, the account renamed, or it was suspended or deleted. X usernames are 1 to 15 letters, digits or _, so anything longer is a typo. A renamed account keeps its ID: if you stored it, GET /info-batch with user_ids returns the new username. Failed requests are never charged on Sorsa API.
#How we checked
We wrote every request on this page from the live Sorsa API reference on November 2, 2026; we didn't run them with a key. The same day we read X's pricing, rate-limit, user-lookup and ID pages on docs.x.com, its Help Center page on Chat and Microsoft's page on large numbers in Excel, and opened x.com/i/user/12 to confirm where it leads. The rounding examples come from the 64-bit floating-point numbers JavaScript uses.
Sources
- Sorsa API reference: Convert Username to User ID (opens in a new tab)
- Sorsa API reference: User Profile (Batch) (opens in a new tab)
- X API: Get Users by Usernames (opens in a new tab)
- X API pricing (opens in a new tab)
- X API rate limits (opens in a new tab)
- X IDs (opens in a new tab)
- X Help Center: About Chat (opens in a new tab)
- Microsoft Support: Keeping leading zeros and large numbers (opens in a new tab)