Key Takeaway: A Twitter (X) user ID is a permanent 64-bit number assigned at account creation, while a username (handle) can be changed at any time. To convert between them, send the handle, the ID, or the profile URL to a dedicated lookup endpoint. The ID is what you should store in any database, because it survives renames.
If you have ever stored @brand_name as a primary key and then watched that account rebrand to @new_brand, you already know why this article exists. Twitter (now X) lets users change their handle whenever they want, and once @brand_name is released, anyone can claim it. The numeric user ID is the only identifier that stays attached to the original account for life.
This guide covers when you need to convert between usernames, user IDs, and profile URLs, why the technical design of these identifiers matters, and how to do conversions programmatically at any scale. The free no-code option is Sorsa's ID Converter playground: paste a handle, ID, or profile URL and get the result instantly without an API key. For batch or pipeline work, the second half of this article shows the Sorsa API endpoints and Python patterns we use in production.
Disclosure: Sorsa API is our product. The comparison with the official X API in this article reflects pricing as of May 2026; verify current rates against your own workload before committing.
Table of Contents
- Why User IDs Are the Only Identifier You Can Trust
- How Twitter User IDs Are Generated
- The Three Conversion Operations
- How Much Does ID Conversion Cost in 2026?
- Converting with the Sorsa API
- Batch Conversion: Usernames to IDs
- Normalizing Mixed Input
- Detecting Handle Changes
- Getting Started
- Frequently Asked Questions
Why User IDs Are the Only Identifier You Can Trust
A username on X is mutable. A user can change @old_name to @new_name today, and tomorrow someone else can grab @old_name. If your application treats the handle as a primary key, two things break at once: every stored reference points to the wrong account, and a third party may now be holding the handle you used to associate with the original user.
Research on Twitter username churn found that roughly 10% of monitored accounts change handles over time (Jain & Kumaraguru, IIIT-Delhi). In our own work helping companies migrate off the official X API after the 2023 pricing overhaul, this was one of the most common silent data corruption issues we found. Several clients had monitoring dashboards storing handles as foreign keys. When a tracked competitor rebranded, the dashboard kept happily collecting tweets from a completely unrelated account that had picked up the old handle.
The user ID is a 64-bit integer assigned at account creation. It cannot be changed, cannot be transferred, is unique across the entire platform, and is the join key every reliable X data pipeline is built on:
- Handle changes do not break your system. Whether the account renames once or fifty times, the ID points to the same profile.
- Indexes are faster. Integer indexing is more storage-efficient than variable-length strings, and lookups by numeric primary key beat lookups by username at every scale.
- Cross-time joins are clean. When matching accounts across datasets collected at different times, IDs are the only safe key. A handle match across years may match two completely different people.
- Several API endpoints require IDs. Sorsa's
/info-batchacceptsuser_ids, list and community endpoints reference accounts by numeric ID, and most official X API endpoints likewise expect numeric IDs.
If you take only one thing from this article: store IDs in your database, not handles. Treat the handle as cached display data that can become stale.
How Twitter User IDs Are Generated
Twitter built its ID generation system, Snowflake, in 2010 to solve a distributed-systems problem. At Twitter scale, asking a central database "what is the next ID?" for every new tweet, message, or account would not work. Snowflake lets any server mint globally unique 64-bit integers independently, with no coordination, by combining three values:
- Timestamp (41 bits): milliseconds since the Twitter epoch (
2010-11-04 01:42:54.657 UTC). - Worker / machine ID (10 bits): identifies the specific server that generated the ID.
- Sequence number (12 bits): increments within the same millisecond, supporting up to 4,096 IDs per machine per millisecond.
Add a sign bit and you get 64 bits total. Because the timestamp sits in the most significant bits, Snowflake IDs are roughly sortable by time, which is why tweet IDs and account IDs are time-ordered when you compare them.
The user ID nuance no one mentions
Here is a detail most ID converter pages miss: user IDs were not migrated to the Snowflake format until around 2020. Before that, X kept assigning sequential integer user IDs even after Snowflake launched for tweets in 2010. That is why Jack Dorsey's account is 12, and why older accounts have short, small IDs (think 9 or 10 digits) while accounts created after 2020 have 19-digit Snowflake IDs.
The practical consequence: you cannot extract a registration date from an old user ID, even though you can extract a creation timestamp from any tweet ID. If you need a join date for an older account, fetch the created_at field from the user profile directly.
The Three Conversion Operations
There are exactly three conversion operations you ever need:
| You have | You want | What to do |
|---|---|---|
Username (handle, with or without @) | Numeric user ID | Resolve handle to ID |
| Numeric user ID | Current username | Resolve ID to handle |
Profile URL (x.com/username or twitter.com/username) | Numeric user ID | Extract handle from URL, resolve to ID |
Each operation is a single API call. None of them require OAuth, callback URLs, or app approval if you use a third-party API like Sorsa. The Python and curl examples for all three are in the code section below.
One thing worth noting: there is no equivalent operation for tweets. Tweet IDs are visible in the URL (x.com/user/status/1234567890), so you just parse them out of the URL string. No lookup needed.
When to use the /info endpoint instead
If you need both the user ID and the full profile (display name, follower count, bio, avatar URL), do not call /username-to-id first and then /info. Just call /info?username=... directly. It returns the complete profile, including the id field, in a single request. This is one of the most common optimization mistakes we see in code reviews. See the optimizing API usage guide for more patterns like this.
How Much Does ID Conversion Cost in 2026?
Pricing has shifted significantly this year. In February 2026 X moved its API to a pay-per-use credit model as the default for new developers, replacing the old Basic/Pro tiers. User lookup on the official X API now costs approximately $0.005 per resource returned (one user lookup, one resource). For one-off conversions inside an internal tool, that is fine. For batch or recurring jobs, the cost adds up fast: resolving 100,000 handles costs around $500 on the official API.
On Sorsa, the three conversion endpoints each count as one request:
| Sorsa Starter | Sorsa Pro | Official X API (pay-per-use) | |
|---|---|---|---|
| Cost per conversion | $0.0049 | $0.00199 | ~$0.005 |
| 100,000 conversions | ~$490 included in $49/mo plan | ~$199/mo flat | ~$500 in pay-per-use credits |
| Authentication | API key header | API key header | OAuth 2.0 |
| Setup time | About 30 seconds | About 30 seconds | App approval required |
For the full breakdown of how the X API pricing model changed this year, see our Twitter API pricing 2026 article.
The remaining sections are the production-ready code. All examples use Python with the requests library and assume you have a Sorsa API key. If you do not have one, the free playground works for one-off conversions without an account, or sign up at api.sorsa.io for an API key.
Converting with the Sorsa API
Authentication is a single header: ApiKey: YOUR_API_KEY. No OAuth flow, no scopes, no callback. Full details on the authentication page.
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY}
def username_to_id(handle: str) -> str:
"""Resolve a handle (without @) to its permanent numeric user ID."""
resp = requests.get(f"{BASE}/username-to-id/{handle}", headers=HEADERS)
resp.raise_for_status()
return resp.json()["id"]
def id_to_username(user_id: str) -> str:
"""Resolve a numeric user ID to the account's current handle."""
resp = requests.get(f"{BASE}/id-to-username/{user_id}", headers=HEADERS)
resp.raise_for_status()
return resp.json()["handle"]
def link_to_id(profile_url: str) -> str:
"""Extract the numeric user ID from a full profile URL."""
resp = requests.get(
f"{BASE}/link-to-id",
headers=HEADERS,
params={"link": profile_url},
)
resp.raise_for_status()
return resp.json()["id"]
# Examples
print(username_to_id("elonmusk")) # "44196397"
print(id_to_username("44196397")) # "elonmusk"
print(link_to_id("https://x.com/elonmusk")) # "44196397"
For a curl equivalent of the first call:
curl "https://api.sorsa.io/v3/username-to-id/elonmusk" \
-H "ApiKey: YOUR_API_KEY"
# {"id": "44196397"}
Batch Conversion: Usernames to IDs
When you have a spreadsheet of competitor handles, a CRM export, or a list of accounts to seed a monitoring pipeline, you need to convert them all to IDs in one pass. The pattern below adds retry logic, soft rate limiting (Sorsa allows 20 req/s per key), and graceful handling of suspended or deleted accounts.
import requests
import time
from typing import Iterable
API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY}
def batch_username_to_id(handles: Iterable[str], pause: float = 0.05) -> dict[str, str | None]:
"""
Resolve a list of handles to user IDs.
Returns a dict mapping handle -> id (or None if the lookup failed).
Soft rate limit: ~20 req/s, well under Sorsa's per-key cap.
"""
results: dict[str, str | None] = {}
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) # Back off and retry once
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:<10} -> {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.
Normalizing Mixed Input
A common situation: your application accepts account references from end users or upstream systems in whatever format the source happens to produce. Some entries are bare handles, some are @-prefixed, some are full URLs, some are already numeric IDs. The normalizer below collapses all four shapes into a stable user ID without making extra API calls when the input is already an ID.
def normalize_to_id(value: str) -> str:
"""
Accept any of:
- "elonmusk"
- "@elonmusk"
- "https://x.com/elonmusk"
- "https://twitter.com/elonmusk"
- "44196397"
Returns the numeric user ID as a string.
"""
value = value.strip().lstrip("@")
# Already a numeric ID, no API call needed
if value.isdigit():
return value
# Profile URL
if "x.com/" in value or "twitter.com/" in value:
return link_to_id(value)
# Bare username
return username_to_id(value)
# All four return the same ID
for source in ["elonmusk", "@elonmusk", "https://x.com/elonmusk", "44196397"]:
print(f"{source:<35} -> {normalize_to_id(source)}")
Use this as the first step in any ingestion pipeline. It guarantees that downstream logic always works with the stable identifier, regardless of how messy the input is.
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. This is the workflow for refreshing a database that may have stale display names.
def detect_renames(records: list[dict]) -> list[dict]:
"""
records: list of {"user_id": str, "stored_handle": str}
Returns: list of {"user_id", "old_handle", "new_handle"} for accounts that have renamed.
"""
changes = []
for record in records:
try:
current = id_to_username(record["user_id"])
except requests.HTTPError:
continue # Account 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
db_records = [
{"user_id": "44196397", "stored_handle": "elonmusk"},
{"user_id": "1234567890", "stored_handle": "old_brand_name"},
]
for change in detect_renames(db_records):
print(f"Renamed: @{change['old_handle']} -> @{change['new_handle']} (ID {change['user_id']})")
For a richer rename audit, the /about endpoint returns username_change_count and last_username_change_at. That tells you not just the current handle but how many times an account has renamed and when the most recent change happened.
Getting Started
Three ways to try this:
- No-code, no signup. Open the ID Converter playground and paste any handle, ID, or profile URL. Useful for one-off lookups and for verifying that a specific account exists before you write any code.
- API key. Sign up at api.sorsa.io, drop your key into the
ApiKeyheader, and the Python examples above run as-is. The Starter plan at $49/month covers 10,000 requests, enough for most batch conversion jobs. - Already on the official X API? See our migration guide for a side-by-side endpoint mapping and a sample migration plan.
The full API reference documents every endpoint, parameter, and response field for the four utility endpoints covered here plus 34 others.
Frequently Asked Questions
What is a Twitter (X) user ID?
A user ID is a unique 64-bit integer assigned to an X account at creation. It is permanent: it cannot be changed, transferred, or reassigned. Unlike the username (handle), which can be edited at any time, the user ID stays attached to the same account for the lifetime of that account.
How do I find my own Twitter user ID?
The X interface does not display your user ID anywhere. To find it, paste your handle or profile URL into a converter like the Sorsa ID Converter, or send a request to a lookup endpoint. You can also download your X data archive, which includes your user ID in the account metadata.
Can a Twitter user ID change?
No. The user ID is assigned at account creation and is permanent for the lifetime of the account. Renaming the account, changing the display name, switching email addresses, or even being suspended and then reinstated does not change the user ID. The only way the relationship between a handle and an ID changes is when the handle itself is changed and the old handle becomes available for someone else to claim.
Why are some Twitter user IDs short and others very long?
X assigned sequential integer IDs to early accounts (which is why Jack Dorsey's account is 12) and only migrated user IDs to the Snowflake 64-bit format around 2020. Accounts created before the migration have short, low IDs. Accounts created after the migration have 19-digit Snowflake IDs that encode a creation timestamp in the upper bits.
Can I decode a registration date from a user ID?
For accounts created after the Snowflake migration around 2020, yes: shift the ID right by 22 bits, add the Twitter epoch (1288834974657), and you get the creation timestamp in milliseconds. For older accounts with sequential pre-Snowflake IDs, no. You will need to fetch the profile and read the created_at field directly.
Is there a free Twitter ID converter that does not require an API key?
Yes. The Sorsa ID Converter playground handles all three operations (handle to ID, ID to handle, profile URL to ID) in the browser without an API key or signup. It is intended for one-off lookups. For batch jobs, recurring pipelines, or anything that needs to run unattended, an API key is the better fit.
How can developers convert thousands of usernames to IDs at scale?
For high-volume conversions, the Sorsa API exposes three dedicated endpoints (/username-to-id, /id-to-username, /link-to-id) at a flat 1 request per conversion. On the Pro plan at $199/month for 100,000 requests, the per-conversion cost is around $0.002. Throughput is capped at 20 requests per second per API key, which is high enough that the bottleneck is usually your own code rather than the API. The batch Python pattern earlier in this article is what we use internally.
What is the difference between a user ID and a tweet ID?
Both are 64-bit Snowflake-style integers, but they live in separate namespaces and identify different things. A user ID identifies an account; a tweet ID (also called a status ID) identifies a single post. Tweet IDs are visible directly in the tweet URL (x.com/user/status/{tweet_id}), so no conversion is needed for them. User IDs are not exposed anywhere in the X interface, which is why dedicated lookup endpoints exist.
Daniel Kolbassen is a data engineer and API infrastructure consultant with 12+ years of experience building data pipelines around social media platforms. He has worked with the X (formerly 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.