Skip to content

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.

Twitter ID Finder

Free, one account at a time, no sign-up. Example: the username jack has the user ID 12.

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.

  1. Get a key. Sign up and copy your API key from the dashboard. It comes with 100 free requests and needs no card.

  2. 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"
    }
    
  3. 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 named handle, and GET /link-to-id (opens in a new tab) takes a full x.com or twitter.com profile link.

  4. 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"))  # 1427632236659332516
    
    You have Sorsa API endpoint You get A username harborline_analytics GET /username-to-id one request each id 1427632236659332516 A profile link x.com/harborline_analytics GET /link-to-id one request each id 1427632236659332516 A user ID 1427632236659332516 GET /id-to-username one request each handle harborline_analytics Up to 100 usernames or up to 100 user IDs GET /info-batch 100 per request id + username and the whole profile
    One 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:

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=copperkettle_maps"
Response200 OK
{
  "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.

Python
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.

Python
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:

InputRead as
jack, with or without the @Username
https://twitter.com/jack?s=20Username
https://mobile.twitter.com/jackUsername
https://x.com/jack/status/20Username of the post's author when the link was copied
https://x.com/i/user/12User ID
https://twitter.com/intent/user?user_id=12User ID
12 or 1427632236659332516User ID
https://x.com/home, a hashtag, an emailNot 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
  • 12 or 1427632236659332516

    Read as
    User ID
  • https://x.com/home, a hashtag, an email

    Read as
    Not an account
Python
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_INTEGER is 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 and 1427632236659332516 turns into 1427632236659332600.
  • Excel. Excel stores numbers with 15 digits of precision and shows big ones in scientific notation, so the same ID opens as 1.42763E+18 and is stored as 1427632236659330000 (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 BIGINT or TEXT in PostgreSQL, BIGINT UNSIGNED or VARCHAR(20) in MySQL, a string in MongoDB and TEXT in SQLite. A 32-bit INT overflows 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 BIGINT 1427632236659332516 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 INT Overflows 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.

Usernames to convert

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.

The same 1M usernames: about 8 hours 20 minutes and $10,000 on the official X API, about 8 minutes 20 seconds and from $14 on Sorsa API.

On every criterion this job depends on, Sorsa API comes out ahead of the official X API:

CriterionSorsa APIOfficial X API
Price per 1,000 usernamesfrom $0.014$10.00
Time for 1M usernamesabout 8 minutesabout 8 hours 20 minutes
Setupone API key in a headerdeveloper account, app, bearer token
IDs back to usernamessame endpoint, same price$0.010 per user
Your X loginnever asked forthe 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.

By the Sorsa API team. We build and run Sorsa API, the read-only Twitter/X API and scraper, in production since 2022 (formerly TweetScout API) with 6B+ requests served. About Sorsa API

What changed
  • Rewritten from scratch for developers: bulk conversion through GET /info-batch with prices and times, cleanup of mixed input, renames, keeping IDs as strings, and the difference between an X Number and a user ID.
  • First published.