Skip to content
cheapxapi

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:replies

Official X API v2

from:NASA min_likes:100 -is:reply
Open this search on X ↗

Same 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.

Search operators on the official X API and on the X website and cheapxapi, with live test results
You wantOfficial X APIX website, cheapxapiResult on cheapxapi
Posts by an accountfrom:NASAfrom:NASA20 of 20 by @NASA
Replies to an accountto:NASAto:NASA20 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#SpaceX20 of 20 had the tag
Cashtag$TSLA$TSLA19 of 19 had the tag
Languagelang:enlang:en19 of 19 in English
Links to a siteurl:github.comurl:github.com20 of 20 linked there
Photo or videohas:mediafilter:media20 of 20, either spelling
Photohas:imagesfilter:images20 of 20 had a photo
Videohas:video_linkfilter:videos19 of 19 had a video
Any linkhas:linksfilter:links20 of 20 had a link
Only repliesis:replyfilter:replies20 of 20, either spelling
No replies-is:reply-filter:replies0 replies, either spelling
Only retweetsis:retweetinclude:nativeretweets filter:nativeretweets20 of 20 were retweets
No retweets-is:retweet-is:retweet0 of 20 were retweets
Quote tweetsis:quotefilter:quote -is:retweet20 of 20. Without -is:retweet, half were retweets
Minimum likesmin_likes:100min_faves:100Exact, either spelling. Lowest at 100,000 was 100,852
Minimum repliesmin_replies:10min_replies:10Exact. Lowest was 10
Minimum repostsmin_reposts:50min_retweets:50Not exact. Results at 45 and 10 retweets came back
Verified authorsis:verifiedfilter:blue_verified20 of 20 had the blue check
One conversationconversation_id:IDconversation_id:ID20 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: and min_retweets: are rejected with a 400 error.
  • has:, is: and lang: 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, not skiing -(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 ipad as apple 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 retweetCount in your own code as well.
  • On cheapxapi, queryType changes what you get. In our test, from:NASA with Top returned only retweets, against 4 in 20 with Latest. Add -is:retweet for 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.

List price of 100,000 tweets from search on each API
API100,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 key

Questions.

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.