Twitter (X) Username Lookup — A Developer's API Reference

By Alex Chen•4 min read

Username → user object resolution is one of the most common API calls in any Twitter (X) workflow — enrichment pipelines, user-page rendering, ID-mapping for stored handles, competitor-tracking watchlists. This reference walks the single-lookup + batch patterns with runnable Python + honest cost math.

Pricing references URL-cited to each provider.

01 — Section

Why username lookup matters

X's internal ID for an account is a numeric string (like 44196397 for @elonmusk). Most API operations use the numeric ID; user-visible handles are strings. Anything that stores handles + queries data needs a resolver from handle → numeric ID + profile data.

Common use cases: user-profile page rendering, converting a handle list to IDs for subsequent API calls, enrichment (given handle, retrieve bio + follower count), watchlist validation (does this handle still exist? still verified?).

02 — Section

Single lookup — twitterapi.io

Auth via X-API-Key. Pass userName parameter. The call returns HTTP 200 either way: a handle that doesn't exist comes back as status: "error" in the JSON body, and a suspended account as data.unavailable: true.

Pricing per twitterapi.io/pricing: $0.00018 per lookup.

python
import os, requests

HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
BASE = "https://api.twitterapi.io"

def lookup(handle: str) -> dict | None:
    r = requests.get(
        f"{BASE}/twitter/user/info",
        headers=HEADERS, params={"userName": handle}, timeout=10,
    )
    r.raise_for_status()
    body = r.json()
    data = body.get("data") or {}
    if body.get("status") != "success" or data.get("unavailable"):
        return None  # not found, or suspended (data.get("unavailableReason"))
    return data

user = lookup("nasa")
if user:
    print(f"@{user['userName']} → id={user['id']}, followers={user['followers']:,}")
else:
    print("handle not found or unavailable")
03 — Section

Single lookup — X official

Auth via bearer token from X Developer Console.

Pricing per docs.x.com/x-api/getting-started/pricing: $0.010 per lookup.

python
# pip install tweepy
import tweepy

client = tweepy.Client(bearer_token="YOUR_X_BEARER")

def lookup_x(handle: str):
    try:
        user = client.get_user(username=handle, user_fields=["public_metrics", "description", "verified"])
        return user.data
    except tweepy.NotFound:
        return None

u = lookup_x("nasa")
if u: print(f"@{u.username} → id={u.id}, followers={u.public_metrics['followers_count']:,}")
04 — Section

Batch lookup — 100 handles at once

For workflows polling many usernames, single-lookup loop is inefficient. Batch endpoint patterns:

X official: /2/users/by?usernames=handle1,handle2,...,handle100 (up to 100 per call).

twitterapi.io: single-lookup loop; if you need batch performance, parallelize the calls (concurrent HTTP fine given per-key throughput headroom).

For very large watchlists (10K+ handles), parallel single-lookup at twitterapi.io still lands single-digit-dollars total. X official 100-per-batch reduces API-call count but per-user cost is unchanged.

python
# Batch lookup via concurrent single-calls (twitterapi.io)
import os, requests
from concurrent.futures import ThreadPoolExecutor

HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}

def lookup(handle: str) -> dict | None:
    r = requests.get(
        "https://api.twitterapi.io/twitter/user/info",
        headers=HEADERS, params={"userName": handle}, timeout=10,
    )
    r.raise_for_status()
    body = r.json()
    data = body.get("data") or {}
    if body.get("status") != "success" or data.get("unavailable"):
        return None  # not found, or suspended (data.get("unavailableReason"))
    return data

HANDLES = ["nasa", "github", "vercel", "anthropic", "openai"] * 20  # 100 handles

with ThreadPoolExecutor(max_workers=10) as ex:
    results = list(ex.map(lookup, HANDLES))

resolved = [r for r in results if r]
print(f"resolved {len(resolved)}/{len(HANDLES)}")
# Cost per twitterapi.io/pricing:
#   100 lookups × $0.00018 = $0.018 for the full batch
05 — Section

Side-by-side — 2 lookup paths

Dimensiontwitterapi.ioX official
Endpoint/twitter/user/info?userName=<handle>/2/users/by/username/<handle>
Batch endpointparallel single-calls/2/users/by?usernames=... (100 max)
Per-lookup cost$0.00018 (twitterapi.io/pricing)$0.010 (docs.x.com)
AuthX-API-KeyBearer + X Dev Console
Missing handleHTTP 200 with status: "error" in the bodyHTTP 200 with an errors array (see docs.x.com)
Best forany lookup workflow, cost-consciousX-native, already on Dev Console

Per-lookup cost ratio ~55× makes twitterapi.io the default for any polling workflow across many usernames.

06 — Section

Common lookup workflows

Watchlist validation: nightly cron pulls current status of N handles. Detect suspensions (data.unavailable) / deletions (status: "error") + verification changes.

ID resolution for downstream API calls: input is a handle list, downstream calls need numeric IDs. One lookup pass → build handle→ID map → downstream calls use IDs.

Enrichment for existing user records: your database has handles + you want current follower counts / bio. Batch lookup → merge back to your records.

Handle-exists check: before signup form validates or user-tagging feature accepts a handle, quick lookup confirms account exists.

Bot / spam detection: lookup + inspect followers / following / createdAt / description patterns. Very low follower + high following + new account + generic bio = likely spam.

07 — Section

Edge cases

Case sensitivity: handles are case-insensitive at lookup — @NASA, @nasa, @Nasa all resolve to the same account. The API returns the account's canonical case.

Renames: handles can change. Stored handles in your database may go stale. If lookup returns different id than expected for a known handle, the handle was reassigned to a new account (the old account changed handle or was deleted + handle re-registered).

Rate-limit on batches: parallel calls at 10-20 workers is safe for twitterapi.io. Higher can bump into per-key throughput ceilings.

Numeric-only handles: not supported — X handles are alphanumeric + underscore. If your input list has pure-numeric strings they're not valid handles.

Choosing a handle rather than resolving one: if your input list is a set of candidate usernames you are deciding between, the same lookup call doubles as an availability check. The guide to usernames for Twitter (X) covers X's handle rules, idea frameworks, and a script that classifies a candidate list as taken, suspended, or likely free.

python
# Practical example: watchlist validation + enrichment nightly cron.
import os, requests, json
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from datetime import datetime, timezone

HEADERS = {"X-API-Key": os.environ["TWITTERAPI_IO_KEY"]}
BASE = "https://api.twitterapi.io"

WATCHLIST = Path("watchlist.txt").read_text().strip().split("\n") if Path("watchlist.txt").exists() else [
    "nasa", "github", "vercel", "anthropic", "openai", "elonmusk",
]

def lookup(handle: str) -> tuple[str, dict | None]:
    r = requests.get(f"{BASE}/twitter/user/info", headers=HEADERS, params={"userName": handle}, timeout=10)
    r.raise_for_status()
    body = r.json()
    data = body.get("data") or {}
    if body.get("status") != "success" or data.get("unavailable"):
        return (handle, None)  # not found, or suspended (data.get("unavailableReason"))
    return (handle, data)

now = datetime.now(timezone.utc).isoformat()
results = []
with ThreadPoolExecutor(max_workers=10) as ex:
    for handle, user in ex.map(lookup, WATCHLIST):
        if user is None:
            results.append({"captured_at": now, "handle": handle, "status": "NOT_FOUND_OR_UNAVAILABLE"})
        else:
            results.append({
                "captured_at": now,
                "handle": user["userName"],
                "id": user.get("id"),
                "followers": user.get("followers", 0),
                "verified": user.get("isBlueVerified", False),
                "status": "OK",
            })

out = Path("watchlist_snapshot.jsonl")
with open(out, "a") as f:
    for r in results:
        f.write(json.dumps(r) + "\n")

n_ok = sum(1 for r in results if r["status"] == "OK")
n_missing = len(results) - n_ok
print(f"snapshot: {n_ok} live, {n_missing} not found or unavailable")
# Cost per twitterapi.io/pricing:
#   len(WATCHLIST) × $0.00018 per snapshot
#   Daily × 30 = ~$0.02-0.20/month for typical watchlists
08 — Questions

Questions readers ask

Can I look up multiple usernames in one call?

twitterapi.io: single-lookup per call; use concurrent parallel calls for batch. X official: /2/users/by supports up to 100 usernames per call. For most workloads, twitterapi.io concurrent still ends up cheaper + faster per completed lookup.

What happens if the account was suspended or deleted?

The API still returns HTTP 200 and reports it in the body: a deleted or renamed handle comes back as status: "error" with the message user not found, and a suspended account as status: "success" with data.unavailable: true and an unavailableReason. Handle both in your data with a None return or explicit 'not found' marker. For long-standing watchlists, periodically re-check — accounts get suspended, deleted, or renamed regularly.

How do I detect a handle rename?

Compare stored id vs returned id for the same handle over time. Same handle string with a different numeric ID = the handle was reassigned. The old account either changed handle or was deleted + the handle re-registered.

Can I do lookups without authentication?

Both providers require auth. twitterapi.io needs X-API-Key; X official needs bearer. No anonymous public API for user lookup exists in 2026.

What about looking up by numeric ID instead of username?

Both providers support ID-based lookup. twitterapi.io: /twitter/user/info?userId=<id>. X official: /2/users/<id>. Same pricing structure per provider.

Are usernames case-sensitive when I make the API call?

No — both APIs are case-insensitive at query. ?userName=NASA and ?userName=nasa resolve to the same account. The returned userName field uses the account's canonical case.

09 — Further reading

Continue

Sources & further reading
More from this series
Build it

Stop reading. Start building.

Starter credits cover real testing on real data. Google sign-in, no card, no application queue.

Get an API key