Twitter search API: operators and paging.
Send your search as a query parameter and keep asking for the next page until there isn’t one. Watch the syntax: the official X API and the X website don’t share all their operators. The API wants min_likes: and has:media; the website uses min_faves: and filter:media.
Below: a query builder for both, operators tested on our live API, paging scripts, and what a search costs.
Build the query.
Fill in what you need. The builder writes the query in both syntaxes, so you can paste it into the X search box, the official API or cheapxapi.
For date ranges, add since: and until: using the date search guide. On the official API, use the start_time and end_time parameters.
X website and cheapxapi
from:NASA min_faves:100 -filter:repliesOfficial X API v2
from:NASA min_likes:100 -is:replySame search through cheapxapi
curl "https://api.cheapxapi.com/twitter/tweet/advanced_search?query=from%3ANASA%20min_faves%3A100%20-filter%3Areplies&queryType=Latest" \ -H "x-api-key: YOUR_KEY"
Search operators, both syntaxes.
The result column is what we saw on one page of live results from cheapxapi, which takes the website syntax. We checked each field in the response rather than reading the tweets by eye. It covers that one test, and X can change how search behaves.
Swipe the table to see the website syntax and the results.
| You want | Official X API | X website, cheapxapi | Result on cheapxapi |
|---|---|---|---|
| Posts by an account | from:NASA | from:NASA | 20 of 20 by @NASA |
| Replies to an account | to:NASA | to:NASA | 20 of 20 were replies |
| Exact phrase, either of two, grouped | ("solar array" OR "space station") | ("solar array" OR "space station") | Worked, combined with from: |
| Hashtag | #SpaceX | #SpaceX | 20 of 20 had the tag |
| Cashtag | $TSLA | $TSLA | 19 of 19 had the tag |
| Language | lang:en | lang:en | 19 of 19 in English |
| Links to a site | url:github.com | url:github.com | 20 of 20 linked there |
| Photo or video | has:media | filter:media | 20 of 20, either spelling |
| Photo | has:images | filter:images | 20 of 20 had a photo |
| Video | has:video_link | filter:videos | 19 of 19 had a video |
| Any link | has:links | filter:links | 20 of 20 had a link |
| Only replies | is:reply | filter:replies | 20 of 20, either spelling |
| No replies | -is:reply | -filter:replies | 0 replies, either spelling |
| Only retweets | is:retweet | include:nativeretweets filter:nativeretweets | 20 of 20 were retweets |
| No retweets | -is:retweet | -is:retweet | 0 of 20 were retweets |
| Quote tweets | is:quote | filter:quote -is:retweet | 20 of 20. Without -is:retweet, half were retweets |
| Minimum likes | min_likes:100 | min_faves:100 | Exact, either spelling. Lowest at 100,000 was 100,852 |
| Minimum replies | min_replies:10 | min_replies:10 | Exact. Lowest was 10 |
| Minimum reposts | min_reposts:50 | min_retweets:50 | Not exact. Results at 45 and 10 retweets came back |
| Verified authors | is:verified | filter:blue_verified | 20 of 20 had the blue check |
| One conversation | conversation_id:ID | conversation_id:ID | 20 of 20 in that conversation |
Official column: the search operators page in X’s developer documentation. We have not run those queries on X’s API. The X docs also list retweets_of:, list:, place_country:, point_radius:, context: and entity:, which we didn’t test.
Rules on the official API
min_faves:andmin_retweets:are rejected with a 400 error.has:,is:andlang:can’t run alone. Pair them with a keyword, an account or another standalone operator.- A query is limited to 512 characters on recent search and 1,024 on full-archive search, or 4,096 on Enterprise.
- Don’t negate a group. Write
skiing -snow -day, notskiing -(snow OR day). - Quote tweets match on their own text, not the text of the post they quote.
Good to know on any syntax
- A space means AND, and AND binds before OR. X reads
apple OR iphone ipadasapple OR (iphone ipad), so use parentheses. - A narrow query saves money. Filtering in the query means fewer pages to fetch than downloading everything and filtering afterwards.
- If you need a hard floor on reposts, check
retweetCountin your own code as well. - On cheapxapi,
queryTypechanges what you get. In our test,from:NASAwithTopreturned only retweets, against 4 in 20 withLatest. Add-is:retweetfor an account’s own posts.
The request and the response.
On cheapxapi, the advanced search endpoint takes query, queryType (Latest or Top) and an optional cursor. Each tweet arrives with its counts, author and createdAt.
On the official API you need a developer account and a Bearer token (how to get one). You list the extra fields you want in tweet.fields, set the page size with max_results, and read the next-page token from meta.
cheapxapi request
curl "https://api.cheapxapi.com/twitter/tweet/advanced_search?query=from%3ANASA%20filter%3Aimages&queryType=Latest" \
-H "x-api-key: $CHEAPXAPI_KEY"Response, captured from the live API
{
"tweets": [
{
"id": "2117633046002225363",
"url": "https://x.com/NASA/status/2117633046002225363",
"text": "This smoky specter is the Cat’s Head Nebula, captured in intricate detail by @NASAWebb. Its name comes from its shape, which resembles a cosmic kitty sitting with its back toward the viewer. What do you see? https://t.co/w3Tv1m9Fuf",
"likeCount": 5640,
"retweetCount": 752,
"replyCount": 324,
"quoteCount": 87,
"viewCount": 1177181,
"lang": "en",
"isReply": false,
"conversationId": "2117633046002225363",
"author": {
"userName": "NASA",
"name": "NASA",
"id": "11348282",
"isBlueVerified": true,
"followers": 92274720
}
}
],
"has_next_page": true,
"next_cursor": "DAADDAABCgAB..."
}Trimmed to one of 20 tweets and the main fields, with the cursor shortened. A search with no matches returns {"tweets": [], "has_next_page": false, "next_cursor": null}.
Official X API, from X’s docs (not run by us)
curl "https://api.x.com/2/tweets/search/recent?query=cat%20has%3Amedia%20-grumpy&tweet.fields=created_at&max_results=100" \
-H "Authorization: Bearer $BEARER_TOKEN"Page through every result.
Pass next_cursor back as cursor and stop when has_next_page is false. Page size is not a signal. We ran this query from both scripts:
- Python, to the end: pages held 20, 20, 9, 4 and 0 tweets. That is 53 tweets from 5 requests.
- Node.js, capped at two pages: 11 tweets, then 20. The first page was short with more to come, so a loop that stops at a short page would have lost most of the results.
- The last page can be empty, and it is still a billed request.
- Neither run returned a duplicate. The scripts keep a set of IDs in case one does.
Python needs pip install requests. Save the Node.js version as search.mjs; it needs Node 18 or later. Both read your key from CHEAPXAPI_KEY and write one tweet per line to tweets.jsonl. More on pacing: rate limits and 429s. For a spreadsheet: the CSV export script.
import json, os, time, requests
API = "https://api.cheapxapi.com/twitter/tweet/advanced_search"
HEADERS = {"x-api-key": os.environ["CHEAPXAPI_KEY"]}
QUERY = 'from:NASA "solar array" min_faves:1000 -filter:replies'
MAX_PAGES = 50 # every page is one billed request, so this caps the bill
PAUSE = 0.5 # 2 requests a second; 0.1 after your first top-up
cursor, pages, seen = None, 0, set()
with open("tweets.jsonl", "w", encoding="utf-8") as out:
while pages < MAX_PAGES:
params = {"query": QUERY, "queryType": "Latest"}
if cursor:
params["cursor"] = cursor
res = requests.get(API, headers=HEADERS, params=params, timeout=75)
if res.status_code == 429: # not billed; wait and ask for the same page again
time.sleep(int(res.headers.get("retry-after", "1")))
continue
res.raise_for_status()
page = res.json()
pages += 1
for tweet in page["tweets"]:
if tweet["id"] not in seen:
seen.add(tweet["id"])
out.write(json.dumps(tweet, ensure_ascii=False) + "\n")
print(f"page {pages}: {len(page['tweets'])} tweets, {len(seen)} unique so far")
if not page["has_next_page"] or not page["next_cursor"]:
break
cursor = page["next_cursor"]
time.sleep(PAUSE)
print(f"{len(seen)} tweets from {pages} requests")What a search costs.
X bills each post returned. cheapxapi bills each request, so short pages raise the price per tweet: the test run above took 5 requests for 53 tweets, which is $0.003. The same 53 posts at X’s rate cost $0.265.
X is the better deal in one case: you pay nothing when no post comes back, and X says it normally won’t charge twice for the same post in one UTC day. If you poll a quiet search often, compare the two with the polling calculator.
| API | 100,000 tweets |
|---|---|
| Sorsa$199 monthly plan | $2.00 |
| cheapxapi | $3.00 |
| twitterapis.com | $4.00 |
| GetXAPI | $5.00 |
| twitterapi.io | $15.00 |
| SocialData | $20.00 |
| Apify Tweet Scraper V2 | $40.00 |
| Official X API | $500.00 |
List prices from each provider’s own pricing page; sources in our comparison. cheapxapi assumes full pages of 20 tweets at $0.0006 a request. X’s figure is $0.005 per post on pay-per-usage.
Get your API keyQuestions.
Does the X API have a search endpoint?
Yes, two. GET /2/tweets/search/recent covers the last 7 days and returns up to 100 posts per request. GET /2/tweets/search/all covers the archive back to March 2006 and returns up to 500 per request; it needs a pay-per-use or Enterprise account.
Why does min_faves return a 400 error on the X API?
min_faves and min_retweets are the X website’s names. X’s documentation says the API rejects them with a 400 error and takes min_likes and min_reposts instead. On cheapxapi both min_faves and min_likes worked when we tested them.
How many tweets does a search return per request?
On the official API, set it with max_results: up to 100 on recent search, where the default is 10, and up to 500 on full-archive search. On cheapxapi, up to 20 per request, and often fewer: pages of 9, 4 and 0 tweets came back in our test.
How do I get the next page of search results?
On the official API, read meta.next_token and send it back as next_token, until the response has no token. On cheapxapi, send next_cursor back as cursor until has_next_page is false. Don’t stop at a short page: it doesn’t mean the results have ended.
How much does the Twitter search API cost?
The official X API charges $0.005 per post returned, so 100,000 tweets cost $500.00. cheapxapi charges $0.0006 per request, with up to 20 tweets on a page, so the same job costs about $3.00 with full pages.
Can I use X website search operators in the API?
On the official API, not all of them: its operator list has no filter: operators and it rejects min_faves, so use has:, is: and min_likes: there. cheapxapi’s advanced search takes the website syntax, and in our tests has:media, is:reply and min_likes worked there too.