X API user lookup: six routes and a $1 request
Six routes return an X user profile, the live spec exposes 27 user fields, and X's own integration guide documents 11 of them with three dead parameter names.
On this page · 10 sections
- Quick answer
- Six routes, and the hub page lists five
- 27 fields in the spec, 11 in the guide
- The fields worth paying for
- Three dead names in every published example
- What a lookup actually costs
- The Owned Reads gap
- Rate limits: the 75 that is easy to miss
- Failure modes you should code for
- When the API is the wrong tool
The short version
- ▸Six routes return a user profile in API version 2.174: by id, by ids, by username, by usernames, the authenticated user, and user search. X's User Lookup hub lists five of them and leaves search out, because search has its own hub page nobody links to from the first one.
- ▸The live spec exposes 27 values in user.fields. The integration guide's field accordion documents 11, and one of the 11 is pinned_tweet_id, which is not in the enum at all. Fields like connection_status, receives_your_dm and active_follower_count exist and appear in no guide.
- ▸Every published example in the lookup guide uses three names the spec dropped: pinned_tweet_id, tweet.fields, and an includes.tweets response key. The generated Python SDK agrees with the spec, not the guide. The string pinned_tweet_id appears zero times in xdk 0.10.6.
- ▸User reads bill at $0.010 per user returned, the joint-highest read rate on the card. A 100-id batch is $1.00 per request. User search accepts max_results up to 1,000, so a single search request can return $10 of billable users.
- ▸GET /2/users/me is not on the Owned Reads table, so reading your own profile costs the full $0.010 instead of the $0.001 owned rate. It is also one of two lookup routes that reject an app-only Bearer token, and the only published user-search example uses one.
Quick answer
Six routes in X API v2 return a user profile, and the difference between them is money, not convenience. Reads bill at $0.010 per user returned, so a 100-handle batch is $1.00 a request and a user search set to max_results=1000 can return ten dollars of billable users in one call. The bigger problem is that X's hand-written integration guide has fallen three renames behind its own API description: every example it publishes uses pinned_tweet_id, tweet.fields and an includes.tweets response key, none of which exist in spec version 2.174.
Last updated: November 2026
Every claim below is checked against the live API description at api.x.com/2/openapi.json, version 2.174, X's own lookup, search, pricing and rate-limit pages, and the published Python SDK wheel. All read on 9 November 2026.
Six routes, and the hub page lists five
Here is the full set, with what each one actually takes.
| Route | Input | Cap | App-only token |
|---|---|---|---|
GET /2/users/{id} | one numeric id | 1 | yes |
GET /2/users?ids= | numeric ids | 100 | yes |
GET /2/users/by/username/{username} | one handle | 1 | yes |
GET /2/users/by?usernames= | handles | 100 | yes |
GET /2/users/me | the token | 1 | no |
GET /2/users/search?query= | a keyword | 1,000 per page | no |
X's User Lookup hub prints the first five in a table and never mentions the sixth. User search lives on a separate hub page that the lookup page does not link, and the lookup page is the one that ranks. That is the whole reason "how do I search X users by keyword" gets answered on reseller blogs instead of by X.
Next, look at the token column. Four routes list BearerToken in their security block, so app-only auth works. /2/users/me and /2/users/search do not, and the guide is explicit about /2/users/me in a warning box. It is silent about search, where the single published example request is this:
curl "https://api.x.com/2/users/search?query=python%20developer&user.fields=description,verified,public_metrics" \
-H "Authorization: Bearer $BEARER_TOKEN"
An app-only Bearer token is not in that route's security block. If you copied the one example X ships for user search, you copied an auth mode the spec says the endpoint does not take. Use a user access token from the OAuth 2.0 PKCE flow instead, as covered in how to get an X API key.
27 fields in the spec, 11 in the guide
By default a user lookup returns three keys: id, name, username. Everything else is opt-in through user.fields, and the enum in spec 2.174 holds 27 values:
active_follower_count, confirmed_email, connection_status, created_at, description, entities, id, is_identity_verified, location, name, parody, profile_banner_url, profile_image_url, protected, public_metrics, receives_your_chat, receives_your_dm, subscriber_count, subscribes_to_you, subscription, subscription_type, url, username, verified, verified_followers_count, verified_type, withheld.
X's integration guide documents eleven in its field accordion: created_at, description, entities, location, pinned_tweet_id, profile_image_url, protected, public_metrics, url, verified, withheld.
Ten of those eleven are real. The eleventh, pinned_tweet_id, is not in the enum. So the documented surface is ten of twenty-seven, and the missing seventeen include every field added since the platform stopped being called Twitter.
The fields worth paying for
Three of the undocumented ones change what you can build.
connection_status returns an array drawn from six values: blocking, follow_request_received, follow_request_sent, followed_by, following, muting. One lookup with a user token tells you whether an account follows you back. Without it you page a follower list at $0.010 a head, which is the expensive way to learn the same fact.
receives_your_dm is a boolean with a caveat written into the spec: it is omitted when the DM permission cannot be determined, so it is never reported as false in that case. Absent does not mean closed. A pipeline that treats a missing key as "cannot DM" will silently skip reachable people. The companion receives_your_chat is a string instead of a boolean and takes three values: encrypted, unencrypted, or none.
active_follower_count is the number of your followers who were active on X in the last 24 hours. The spec adds three conditions: it is returned only for the authenticated user with account analytics enabled, it is omitted entirely for accounts with 50 or fewer followers, and it is an estimate once an account passes 20,000 followers. That is a real engagement denominator, and it is in no guide page.
There is also confirmed_email, typed as a plain string with no description whatsoever. It is the only field in the enum X has not written a sentence about.
Three dead names in every published example
This is the part that costs people an afternoon. The guide's worked example reads:
curl "https://api.x.com/2/users/by/username/XDevelopers?user.fields=created_at,description,public_metrics,verified&expansions=pinned_tweet_id&tweet.fields=created_at,public_metrics" \
-H "Authorization: Bearer $BEARER_TOKEN"
Against spec 2.174, three things in that line are stale.
expansions=pinned_tweet_id. The expansions enum on every user route now holds exactly three values:affiliation,most_recent_post_id,pinned_post_id.tweet.fields. The companion group ispost.fields, with 31 values.tweet.fieldsis not a declared parameter on any user route.includes.tweetsin the sample response. TheExpansionsobject X returns is keyedcalls,media,places,polls,posts,topics,users. There is notweetskey.
The same guide's sample response also shows public_metrics.tweet_count. UserPublicMetrics requires four keys, and they are followers_count, following_count, post_count and listed_count, plus optional like_count and media_count. tweet_count is not among them. In the whole live spec, the only object that still carries tweet_count is Trend.
Which one is right? The SDK settles it, because it is generated from the same description the docs site renders. In xdk 0.10.6 for Python, published 25 July 2026, the string pinned_tweet_id appears zero times, UserPublicMetrics declares post_count as required with no tweet_count anywhere, and the expansion fields are pinned_post_id and most_recent_post_id. The generated client and the spec agree. The hand-written guide is the outlier. If you are picking a source to trust, the X API Python SDK and the raw description beat the prose.
One asymmetry to keep in mind while renaming things: active_follower_count appears zero times in that same wheel. The field is newer than July 2026, so a typed SDK model will not expose it even though the API returns it. The Python models set extra="allow", so the value arrives, just not as an attribute you can autocomplete. Same pattern as the field-group churn covered in X API expansions.
What a lookup actually costs
User reads sit at $0.010 per resource on the pay-per-use card. Only DM Event: Read matches it. Posts are $0.005, and likes, mutes and blocks are $0.001. Profiles are the single most expensive thing you can read ten thousand of.
The word that matters is resource, not request:
| Call | Users returned | Cost |
|---|---|---|
GET /2/users/by/username/jack | 1 | $0.01 |
GET /2/users/by?usernames= with 100 handles | 100 | $1.00 |
GET /2/users/search?query=...&max_results=100 | up to 100 | up to $1.00 |
GET /2/users/search?query=...&max_results=1000 | up to 1,000 | up to $10.00 |
Batching does not save you anything on a per-resource rate card. It saves rate-limit budget, which is a different scarce thing. A thousand single lookups and ten 100-id batches cost the same $10; the batches just use ten requests out of your 300 instead of a thousand.
Two details cut the other way. Resources are deduplicated within a 24-hour UTC day window, so re-reading the same profile inside one day is free, and X calls that a soft guarantee rather than a promise. And the monthly cap X publishes is 3 million Post reads per billing cycle. There is no published user-read cap, and GET /2/usage/tweets, the only programmatic usage endpoint, reports Post consumption. Your profile spend is auditable in the Developer Console and nowhere else. The same blind spot shows up across the rate card in X API pricing.
The Owned Reads gap
Owned Reads drop to $0.001 per resource when the {id} in the path is the authenticated user and that user owns the app. Twelve endpoints qualify, and every one of them is a /2/users/{id}/something route: your posts, mentions, liked posts, bookmarks, followers, following, blocking, muting, and four list routes.
GET /2/users/me is not on that list. Neither is GET /2/users/{id}. So reading your own profile, which is as owned as data gets, bills at the full $0.010. Ten times the owned rate for the one object you certainly have the right to read.
Rate limits: the 75 that is easy to miss
| Route | Per app | Per user |
|---|---|---|
/2/users, /2/users/{id}, /2/users/by, /2/users/by/username/{username} | 300/15min | 900/15min |
/2/users/search | 300/15min | 900/15min |
/2/users/me | no app row | 75/15min |
That 75 is the one that bites. A service that calls /2/users/me on every page load to resolve the signed-in account will hit a 429 at modest traffic, and because the route takes no app-only token there is no app-wide pool to fall back on. Resolve the id once at login, cache it, and use /2/users/{id} after that, where you get 900 per user and 300 per app. More of these asymmetries are mapped in X API rate limits.
Worth noting for anyone counting: the rate-limit page heads this section "Users (14 endpoints)" and the rows under it do add to fourteen. What is absent is GET /2/users/{id}/affiliates and the two /2/users/me/followers and /2/users/me/following routes, which exist in the spec with no rate-limit row anywhere.
Failure modes you should code for
Suspended and deleted accounts behave differently depending on which route you used. A single-user lookup returns 404. A batch lookup returns 200 with the surviving users in data and an errors array naming each missing one by resource_id and resource_type. That is a partial success, so a client that only branches on the status code will treat a half-empty batch as complete.
Protected accounts still return the basics. You get id, name, username and protected: true for an account you do not follow; the pinned post expansion may be withheld.
Handles are not stable identifiers. The username route is convenient and the id route is correct. Store numeric ids, because a handle change silently moves to a different account while the id never does.
When the API is the wrong tool
If you are enriching a CRM, scoring a lead list, or building a dashboard over profiles you already have ids for, these endpoints are exactly right and the per-resource math is predictable once you accept that a batch is not a discount.
If what you want is to watch a feed, reply in your own voice, and post on a schedule from your own account, user lookup is a strange place to start. You end up paying $0.010 a profile to rebuild a view X already renders for free in a browser, and the write side is where the API gets restrictive rather than merely expensive. X-Autopilot takes the browser route instead: it drives your own logged-in session on your own Mac, so your session cookie never leaves the machine and there is no cloud IP pool or delegate account in the path. That is a lower detection surface than a cloud scheduler, not a safe one, and automating engagement through a browser is a gray area that X's automation rules address directly. The API route is the sanctioned one. Pick it when you need sanctioned access, and price it before you build.
Frequently asked
Answers indexed by Google + AI assistants.
How do I look up a user with the X API?+
GET https://api.x.com/2/users/by/username/{username} for a single handle, or GET /2/users/by?usernames=a,b,c for up to 100 at once. If you already hold numeric ids, use GET /2/users/{id} or GET /2/users?ids=. Add user.fields to get anything beyond id, name and username, because the default response is those three keys and nothing else. Four of the six routes accept an app-only Bearer token; /2/users/me and /2/users/search require a user token.
What user fields can the X API return?+
Twenty-seven, as of spec version 2.174: active_follower_count, confirmed_email, connection_status, created_at, description, entities, id, is_identity_verified, location, name, parody, profile_banner_url, profile_image_url, protected, public_metrics, receives_your_chat, receives_your_dm, subscriber_count, subscribes_to_you, subscription, subscription_type, url, username, verified, verified_followers_count, verified_type and withheld. The pinned post and most recent post arrive through expansions, not through user.fields.
Why does expansions=pinned_tweet_id not work?+
Because the expansions enum on every user route holds exactly three values now: affiliation, most_recent_post_id and pinned_post_id. The companion field group is post.fields, not tweet.fields, and the expanded object comes back under includes.posts, not includes.tweets. X's integration guide still shows the old names in its cURL, Python and JavaScript examples and in its sample response.
How much does an X API user lookup cost?+
User: Read is $0.010 per resource returned, and resource means user, not request. A single-handle lookup is a cent. A 100-username batch is $1.00. A user search with max_results=1000 returns up to 1,000 billable users in one call. Resources are deduplicated inside a 24-hour UTC window, which X calls a soft guarantee rather than a promise.
Is GET /2/users/me an Owned Read?+
No. The Owned Reads table lists twelve endpoints that drop to $0.001 per resource when the id matches the authenticated app owner, and all twelve are /2/users/{id}/something routes. Neither /2/users/me nor /2/users/{id} appears on it, so fetching your own profile bills at the standard $0.010 User: Read rate, ten times the owned rate for data that is definitionally yours.
What does connection_status return?+
An array of strings drawn from six values: blocking, follow_request_received, follow_request_sent, followed_by, following and muting. It describes the relationship between the authenticated user and the user being looked up, which makes it the cheapest way to answer do they follow me back without paging a follower list. It needs a user token, and no guide page documents it.
- X live OpenAPI description at api.x.com/2/openapi.json, version 2.174: the six user lookup operations and their security blocks, the 27-value user.fields enum, the 3-value expansions enum, the post.fields group, the Expansions schema keyed on posts, UserPublicMetrics requiring followers_count, following_count, post_count and listed_count, UserConnectionStatus with its 6-value item enum, and the active_follower_count, receives_your_dm and confirmed_email descriptions (accessed 9 November 2026)
- X developer docs, 'User Lookup' hub and 'Integration Guide': the five-row endpoint table that omits GET /2/users/search, the 11-row user.fields accordion, the tweet.fields accordion, and the cURL, Python, JavaScript and response examples using pinned_tweet_id, tweet.fields, includes.tweets and public_metrics.tweet_count (accessed 9 November 2026)
- X developer docs, 'User Search': the single-endpoint table and the only published example request, which authenticates with an app-only Bearer token (accessed 9 November 2026)
- X developer docs, 'Pricing': User: Read at $0.010 per resource, the twelve-endpoint Owned Reads table at $0.001 per resource, the 24-hour UTC deduplication window described as a soft guarantee, and the 3 million Post read cap per billing cycle (accessed 9 November 2026)
- X developer docs, 'X API Rate Limits': the 'Users (14 endpoints)' section, 300/15min per app and 900/15min per user on the four id and username routes, 75/15min per user and no app row on /2/users/me, and 300/900 on /2/users/search (accessed 9 November 2026)
- xdk 0.10.6 for Python, published 25 July 2026, read from the wheel: the users client exposes get_by_id, get_by_ids, get_by_username, get_by_usernames, get_me and search; schemas.py uses pinned_post_id and most_recent_post_id with zero occurrences of pinned_tweet_id; UserPublicMetrics requires post_count and has no tweet_count; active_follower_count appears zero times (accessed 9 November 2026)
Product designer and indie hacker. Runs the agent on his own X account every day and writes up what the data shows, including when it's inconvenient.
Follow on X →