How do you search for Twitter users by keyword?
Last updated September 28, 2026
The Twitter user search API is one route, GET user/search. It takes a query string, matches it against display names and handles (not bios), and returns user objects carrying id, username, name, description, followers_count and verified. Walk the matches with cursor. A page of results costs $0.0008, and hydrating one handle afterwards is another call at that rate.
Every rate here is the pricing TwitterAPIs publishes. The billed rate is $0.0008 per call; $0.04 per 1,000 tweets is derived from it at a full 20-tweet page, which is the default page size rather than a guaranteed yield (source: twitterapis.com/pricing).
What the query parameter is matched against
GET user/search takes query, a keyword or a partial handle, and runs it against the user directory. Two fields are compared: the display name a person chose and the handle they registered. The bio they wrote is not searched, so a company name finds the company's own accounts, not the employees who mention it in their profile text; to reach those, page the company's followers with user/followers or search tweets that mention it with tweet/advanced_search, then filter the description field. Precision is also your problem rather than the directory's: a two-word phrase is a much narrower filter than a single common noun, and results are ranked by X rather than by any relevance score you can tune from the request.
What makes Twitter API keyword search peculiar
Most people arrive expecting a search engine and find a directory lookup. The peculiarity is that GET user/search compares one string against two fields at once, ranks the result set by X's own ordering, and gives you no lever over any of it. There is no relevance score in the response, no operator syntax, no exact-match mode and no way to say match the handle only. A query that works well is one narrow enough that the name and handle match cannot drift, which in practice means a two-word phrase or a distinctive partial handle rather than a single common noun. The second peculiarity is that matches are full user objects rather than snippets, so the bio, description, arrives in the same response and you can filter on it locally, which is the only way to act on bio text here.
The request shape, and what it does not accept
query is required, cursor is optional and omitted on the first request, and count sets the page size (default 20, clamped to 1 to 100). There is no verified-only switch, no minimum follower threshold and no location filter, so any narrowing beyond the keyword itself happens in your own code after the page arrives. Auth is a bearer token: send Authorization as Bearer plus your key, or the identical value under an x-api-key header if your HTTP client makes that simpler. Because the whole request is one query parameter, a search is trivially cacheable on your side, and caching matters more than it looks, since re-running the same discovery sweep is otherwise billed again from page one.
Every field on a result object
Results are full user objects, not search snippets. id is the numeric account identifier delivered as a string so a JavaScript client cannot round it. username is the handle without the leading @. name is the display label. description carries the bio text, which the search does not match on but which you can filter locally. followers_count and following_count are integers. verified is a boolean for the badge. profile_image_url points at the avatar. The documented example returns one match with id 44196397, username elonmusk, followers_count 211000000 and verified true, alongside count and a next_cursor token. Around that array sit count, the size of this page, and next_cursor, the token for the following one, which is the same two-field envelope every paginated route in this API wraps its collection in.
Handle, display name and numeric id
Three things get loosely called a name and only one of them is stable. username is the handle typed after the @, unique at any moment but freely changed by its owner. name is the display label, neither unique nor stable, so two accounts can carry the same one. id is assigned once and never reissued. A lead table keyed on username will quietly rot the first time somebody rebrands, and the row will look fine until a later lookup fails. Key on id, keep username as a display column, and re-resolve it on a schedule rather than trusting the copy you captured months ago. The identifier arrives as a string rather than a number, which is how the API stops a JavaScript client from rounding a long value while parsing it.
Paging a directory result set
Each response carries users, count and next_cursor. Omit cursor to get the opening page, then copy next_cursor into the parameter on every following request. No state is pinned on the server between calls, so a sweep can pause overnight and resume from a stored token in a different process. On the stop condition, be conservative: the shared cursor contract documents null-terminating behaviour for List and affiliate routes and non-null cursors past the end for follower graph routes, and directory search belongs to neither group, so break when the users array comes back empty. That condition is correct under either regime and costs one extra request at most. Record the token you used beside each page you write out, so a sweep that stopped halfway is auditable rather than merely resumable.
Hydrating a match into a full profile
Search results already carry description and followers_count, which is usually enough to rank candidates before you spend anything else. When you want the canonical record for one account, user/info takes username with the @ stripped and returns the complete user object under a user key. The documented example resolves naval into id 745273 with followers_count 2100000, following_count 421 and an avatar URL. That is the route to call once per shortlisted account rather than per match, because a discovery sweep that hydrates every result multiplies its own bill by the page size for no ranking benefit. That response wraps everything under a single user key rather than an array, so the parse differs slightly from the search shape and deserves its own function.
Re-resolving a saved account after a rename
When a stored handle stops working, the usual cause is a rename rather than a deletion. user/info_by_id exists for that case: it takes user_id, the numeric identifier you captured at discovery time, and returns the same full user object including whatever the current username is. Identifiers survive renames, so this lookup keeps working indefinitely while a handle-keyed one does not. The practical pattern is to capture id on the first search, treat the handle as cache, and run an id-keyed refresh whenever a downstream call starts failing on a name you thought was correct. Its response shape matches the handle-keyed lookup exactly, one user key holding the full object, so both resolvers can share a parser. The documented example turns 745273 back into the current profile for naval.
Telling a suspended account from a deleted one
A plain profile lookup collapses suspended, deleted and never-existed into a single 404, which is useless when you are auditing an old list. user/status exists for exactly that gap. It takes the handle and returns a status of alive, suspended, not_found or unavailable, and every outcome is an HTTP 200, so you branch on the field rather than on the transport. It also returns id when the account is alive and reason when X supplies one. Running it across a stale prospect list separates the accounts worth re-resolving from the ones to drop. The handle parameter is spelled userName on that route, with username accepted as an alias, so an existing helper does not have to be renamed. Its documented example returns alive for openai together with the numeric id 1634058197493178368.
Which search type fits which job
Three routes answer questions that sound alike and are not. GET user/search is a directory lookup: give it a keyword and it returns accounts whose display name or handle match (bio text is not searched), which is the right call when you know a name, a brand or part of a handle. GET user/info takes a handle you already hold and returns that one profile, so it is a resolver rather than a search and it is what you use after a match, not instead of one. GET tweet/advanced_search searches tweet text, so it finds people by what they posted rather than by how they describe themselves, which surfaces people by topic, something a name and handle lookup cannot do. Discovery work usually needs two of the three: user/search or advanced_search to build a candidate list, then user/info to hydrate the handles worth keeping.
What a discovery pass costs
Pricing here is multiplication rather than tiers, so the estimate is written before the job runs. Twenty pages of candidates plus fifty shortlisted profile lookups is seventy requests, close to six cents. Add a status sweep over 500 saved handles and that is another 500 requests, forty cents. The $0.50 credit granted at signup covers 625 requests with no card involved, which is enough to build and validate a first list end to end. Throughput rather than price sets the wall clock: a key may hold 20 requests open at once and issue 600 in a minute. Those ceilings are per key and apply with 20 requests open at once, so what limits a sweep is how fast you can issue calls rather than how many candidates you wanted. $0.0008 a call, per our published pricing.
Enriching a shortlist 100 accounts at a time
Once a search has produced candidates, the expensive habit is hydrating them one call at a time. user/user_about/batch takes up to 100 handles, or up to 100 numeric ids, in one request and returns the full About object for each, in the order you sent them, with an error code on any item that could not be read rather than a failure for the whole request. Each About object carries more than a search hit does: bio, location, linked website, avatar, verification and identity-verification flags, the professional account type and category, follower and following counts, the date the account was created, and X's About this account panel with the account's country, how it was created and its username-change history. Billing is $0.0008 per item, so a shortlist of 300 is three requests and $0.24. Handles may be sent with or without the @ and are compared case-insensitively, so duplicates in your list collapse before they cost anything, and ids with leading zeros are treated as the same account. Respect one rule: send usernames or user_ids, never both, and validate the list first, because a single malformed handle returns a 400 for the entire request.
Turning a company into its people
Keyword search finds an organisation's own accounts by name, but not the people who work there, because bios are not searched. user/affiliates closes part of that gap. It returns the accounts linked under an organisation's Verified Organizations profile, the employees and brand accounts the company has chosen to show, cursor paginated at 1 to 100 per page. Where the organisation groups its affiliates into teams, the team parameter narrows the list to one of them. The input is the organisation's handle or numeric id, so the usual pattern is user/search to find the company account, user/affiliates on the match, then a batch About read for the affiliates worth ranking. Each page is a standard $0.0008 read. Two limits deserve plain statement. Only organisations with a Verified Organizations profile expose affiliates at all, and the list is what the company chose to publish, so it is a roster of declared affiliations rather than a census of staff. For companies outside that programme, the fallback remains the follower route, or tweet search on the company's name with the authors' description field filtered in your own code.
Narrowing a large follower base to verified accounts
A popular account's followers are often the best pool of people interested in a topic, and also the noisiest. user/verified_followers returns only the verified accounts that follow a user, cursor paginated with the same 1 to 100 page size the other graph routes use, so a list of hundreds of thousands narrows to the subset most likely to be active and reachable before you pay to page anything else. It takes a handle or a numeric id. Each page is a standard $0.0008 read and returns full user objects, so the description, follower count and verification flags arrive with each row and ranking can start immediately. Verification is a filter rather than a verdict: it tells you the account holds a badge, not that it fits your list. Pair it with a description match in your own code, a job title or a technology name for example, and you have a targeted list built from people who already chose to follow the account you started from. That is usually a better seed than a cold keyword match, because a follow is a decision the person made about a topic.
Checking who already follows you, in bulk
Before contacting anyone on a discovered list, it helps to know which of them already know you. user/check_follow_relationship/batch answers that for up to 100 accounts in one request. Fix one source, your own account, and send up to 100 targets, or fix one target and send up to 100 sources. Each item comes back in request order with the relationship read from the source's side: following, whether the source follows the target, and followed_by, whether the target follows the source, plus block, mute and direct message eligibility flags. Billing is $0.0008 per item, so checking 500 candidates against your account is five requests and $0.40. A source or target handle that does not exist returns a 404 and nothing is billed. The result splits a list three ways that matter for outreach: people who already follow you, people you follow who do not follow back, and strangers. The direct message eligibility flag is worth its own column as well, because a candidate you cannot message changes what the sensible next step is, and it is far cheaper to learn that from one batch call than from a refused send.
Where a candidate's audience is based
For partnership or influencer work, who follows a candidate can matter more than the candidate's own profile. user/audience_summary samples between 10 and 100 of an account's followers, 50 by default, reads each sampled account's About object, and returns a country histogram of where those followers are based, with each country's share of the accounts whose country is known. It also counts how many sampled accounts show each of seven documented profile signals, and how many show three or more, reported as likely_bot_count, which is a heuristic over profile fields rather than a verdict on any single account. Billing is $0.0008 per billed item, and the balance needed up front is the sample size plus three items, so a 50 account sample needs about four cents available. The same route can sample a tweet's retweeters instead of an account's followers, which shows who actually amplified a post. Treat the output as a sample: fifty followers out of a million is a quick read on direction, not a census. Running it on your top ten candidates before outreach is an inexpensive way to drop accounts whose audience sits in the wrong market.
Finding people by topic rather than by name
A name search answers who is called this. A topic search answers who talks about this, and it runs through tweet search rather than the directory. Send an operator query to tweet/advanced_search, for example a technology name with a quoted phrase, an engagement floor such as min_faves:20 and -filter:replies to keep original posts, and every returned post carries its author nested inside it: numeric id, handle, display name and verification flags. Collect the author ids, count how many matching posts each one wrote, and you have a ranked list of people active on the topic, built from what they posted rather than from how they describe themselves. Each search page is one $0.0008 call returning about 20 posts, so 25 pages is 500 posts and two cents, with no extra charge for the authors that ride along. Deduplicate on the id rather than the handle, since handles change. Then feed the top of the list into the batch About route for bios and locations, and into the relationship check before outreach. The directory search and the topic search find different people, so serious discovery uses both and merges them on the id.
Finding people inside a community
X Communities are self-selected groups around a topic, which makes their rosters a strong discovery source. Find the community with community/search, then page community/members with its id at up to 100 rows a page, the standard $0.0008 a call. Each row pairs a member with a role, Admin, Moderator or Member, and a reduced profile: id, username, name, avatar, the verification flags and whether the account is protected. There is no bio and no follower count on these rows, so the usual next step is the batch About route for the members you want to rank. Moderators deserve their own call through community/moderators, because they are scattered through the roster rather than grouped at the top, and community/about returns them as full profiles in a single request. For a large community the arithmetic is short: a 10,000 member roster at full pages is 100 calls, eight cents. Membership is a stronger signal of interest than a keyword match, since every member chose to join, and a member list crossed with a topic search on the same subject usually surfaces the people worth contacting first.
What each field on a user search result is good for
| Field | Type | What you use it for |
|---|---|---|
| id | string | The only permanent key. Store this, join on this |
| username | string | The @handle, unique today but the owner can change it |
| name | string | Display label, neither unique nor stable |
| description | string | Bio text, the field to filter on when you need a bio match |
| followers_count | integer | Ranking a candidate list without a second call |
| verified | boolean | Filtering to badged accounts in your own code |
The User lookup endpoints let you retrieve profile information for one or more users.
Questions and answers
- Can I search X accounts without knowing the exact handle?
- Yes, that is what the query parameter is for. A partial handle, a person's name or a brand name all work, because the directory compares your string against the handle and the display name. Matches arrive as full user objects rather than links, so a vague guess resolves directly into records you can pass to other endpoints without any scraping in between.
- Does keyword search look inside bios?
- No. X's People search matches the handle and the display name, not the bio, so a phrase that appears only in a bio returns nothing. Every result still carries its bio in the description field. To find people by how they describe themselves, pull the followers of a relevant account with user/followers, or search tweets mentioning it with tweet/advanced_search, and filter the description field in your own code.
- What parameters does user/search accept?
- Exactly two. query is required and carries the keyword. cursor is optional and is omitted on the first call. There is no page size argument, no verified-only flag and no follower threshold, so narrowing beyond the keyword happens in your own code after the response arrives. That also makes the request trivially cacheable, which is worth doing before you repeat a sweep.
- What is the difference between name and username in the response?
- username is the handle, the string after the @ that appears in URLs. name is a display label its owner edits freely, so it is neither unique nor stable and two accounts can share one. Neither is a safe primary key. The id field is assigned once and never reissued, which is why a stored record should be keyed on it and show the handle only as a label.
- How do I get more than one page of matches?
- Read next_cursor from the response and send it as the cursor parameter on the next request, leaving it empty for the first. The users array and the count field describe the current page only. Because nothing is held server side between calls, a stored cursor can be resumed hours later from a different machine, which makes a long sweep safely interruptible.
- When should I stop paging a user search?
- Break when the users array comes back empty. The cursor contract across this API is not uniform: List and affiliate routes null their cursor on the final page, while follower graph routes keep sending one that still looks usable. Directory search sits in neither group, so the empty-collection check is the condition that behaves correctly regardless, at a cost of one extra request.
- Do I need a second call to get follower counts?
- Usually not. Every match already carries followers_count and verified, which covers most ranking work before you spend anything more. A profile lookup pays off only for a shortlisted account where you want the canonical record straight from the profile route rather than out of the directory index. Hydrating every result instead of the shortlist multiplies a sweep's bill by the page size and adds nothing to the ranking.
- What happens when a handle I saved stops resolving?
- Most often the account renamed itself rather than disappearing. Handles are mutable and numeric identifiers are not, so a lookup keyed on user_id keeps working through a rename and returns the current handle in the same object. Keep the id captured during discovery, and when a stored handle starts failing, resolve it by id rather than deciding the account is gone.
- How do I tell a suspended account from a deleted one?
- Call user/status, which reports alive, suspended, not_found or unavailable and returns HTTP 200 for all four so you branch on the field instead of the status code. A plain profile lookup cannot make that distinction: suspended, deleted and never-existed all collapse into one indistinguishable 404. The route also returns the numeric id for a live account and X's own reason string when there is one.
- What does a thousand-result discovery pass cost?
- Cost tracks request count, so the sum is written in advance rather than estimated afterwards. Fifty pages of directory results is fifty requests, four cents. Hydrating a hundred shortlisted accounts adds a hundred more, eight cents. Auditing 500 stored handles for liveness is another forty. The free $0.50 signup credit covers 625 requests, which is a whole first campaign, and the ceiling that actually bites is throughput at 600 requests a minute.
- Can I enrich many accounts in one request?
- Yes. user/user_about/batch reads up to 100 handles or ids per request and returns each About object in the order sent, billed at $0.0008 per item. A malformed handle fails the whole request with a 400, so validate the list before sending it.
- How do I find a company's employees on X?
- If the company has a Verified Organizations profile, user/affiliates lists the accounts it links there, optionally narrowed by team. Otherwise search tweets that mention the company and filter the authors, or page the company's followers and match job titles in the description field.
Keep reading
Start with $0.50 in free credits
No credit card. Roughly 12,500 tweets to test every endpoint.