How do you search for Twitter users by keyword?
Last updated August 24, 2026
The Twitter user search API is one route, GET user/search. It takes a query string, compares it against display names, handles and bios at once, 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. Three fields are compared at the same time: the display name a person chose, the handle they registered, and the bio they wrote. That triple match is why a phrase like machine learning returns accounts whose handle contains neither word, and why a company name finds employees who mention the employer in their profile text. It also means precision is 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.
The request shape, and what it does not accept
Two parameters exist. query is required, cursor is optional and omitted on the first request. There is no count argument, 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 is often the field that explains why a given account matched. 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.
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.
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, usually the reason the account matched |
| 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 company name or a topic phrase all work, because the directory compares your string against the handle, the display name and the bio together. 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?
- It does. Bio text is one of the three matched fields, alongside the handle and the display name, and every result carries that text back in the description field. That makes the route usable for topical lead building: a phrase people use to describe themselves surfaces accounts whose names give no hint of the subject, and you can read the matching bio without a second request.
- 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.
Keep reading
Start with $0.50 in free credits
No credit card. Roughly 12,500 tweets to test every endpoint.