Key Takeaway: A Twitter engagement API exposes the people and content behind a tweet's like, reply, quote, and retweet counts. The official X API restricts most of this to enterprise OAuth tiers or forces a conversation_id search workaround capped at 7 days. Third-party REST endpoints retrieve replies, quotes, and retweeters directly by tweet URL, with cursor pagination and no time limit.

A like count is a number. A reply is a person, a sentence, an opinion, sometimes a question your support team should be answering. The aggregate counts at the bottom of a tweet are a summary; the engagement underneath them is the actual data.

This guide is about pulling that data out. We will cover how to retrieve a tweet's full metrics snapshot, then drill into the three engagement types that have content or audience behind them: comments (replies), quote tweets, and retweeters. We will use the Sorsa API, a flat-rate REST API for public X data, because the official X API's Engagement product is gated to enterprise customers and the workarounds (filtered search by conversation_id) are rate-limited into uselessness for most use cases. I will note where each approach hits a wall, and what the practical alternatives are.

I have been working with the Twitter/X API since the v1.1 era and have helped over 40 teams migrate off the official API since the 2023 pricing overhaul. The patterns below come from real client work: hedge fund sentiment dashboards, brand monitoring services, campaign verification at scale, and academic dialogue research. Every code example uses plain requests so you can paste it into any Python project.

Disclosure: Sorsa API is our product. I have tried to keep the comparisons fair and recommend you benchmark any provider against your own workload before committing.


Table of Contents

  1. What "tweet engagement" actually includes
  2. Why the official X API makes this hard
  3. Signal density: comments vs quotes vs retweets
  4. Starting point: get a tweet's metrics
  5. Get comments (replies) on a tweet
  6. Get quote tweets
  7. Get retweeters of a tweet
  8. Full engagement report: one tweet, all dimensions
  9. Comparing engagement across multiple tweets
  10. Cost and rate limits at scale
  11. Verifying that a specific user engaged
  12. Exporting engagement data
  13. FAQ
  14. Getting started

What "tweet engagement" actually includes

When most people say "engagement" they mean five things, but the API surface for each is different:

Engagement typeWhat it isWhat you can retrieve
LikesAnonymous heart tapsCount only (the list of likers is no longer publicly exposed)
Replies (comments)Threaded responses with textFull tweet objects: text, author, metrics
Quote tweetsRepost with added commentaryFull tweet objects: text, author, metrics
RetweetsPure amplification, no textUser profiles only (no tweet content, since retweets are just a redistribution)
Views / impressionsHow many times the tweet was renderedCount only, on the original tweet

Bookmark counts also exist and are exposed as a number on the tweet, but the actual bookmarkers are private. Likes used to expose the liker list, but X removed that feed from public view in late 2023. For everything else, the data is reachable, just not always conveniently.

The interesting work happens in the three areas where you can get to the underlying people and text: replies, quotes, and retweeters. The rest of this guide focuses on those.


Why the official X API makes this hard

Three obstacles, in increasing order of pain.

Obstacle 1: The Engagement API is enterprise-only. X's Engagement API is the right product for engagement metrics on paper. It returns favorite, retweet, reply, quote, and impression counts for up to 250 tweets per request. But it sits behind their enterprise sales channel: you need to "set up an account with our enterprise sales team" before you can use it. Pricing is opaque and starts in the thousands per month. For most teams this is a non-starter.

Obstacle 2: Getting actual replies requires the conversation_id workaround. There is no /tweets/:id/replies endpoint in the public X API. To get replies, you query the recent search endpoint with conversation_id:<tweet_id> and filter on referenced_tweets.type = "replied_to". This works, but it has two hard limits: recent search only covers the last 7 days unless you have Academic Research access, and the rate limits on Basic and Pro tiers are punishing. Developers on the X forums regularly report search rate limits dropping to as low as 1 request per 24 hours on certain endpoints. For a tweet older than a week, you cannot get the replies at all without enterprise full-archive search.

Obstacle 3: The retweeted-by endpoint is restricted and time-limited. GET /2/tweets/:id/retweeted_by exists, but it only returns up to the first 100 retweeters and is rate-limited at 75 requests per 15 minutes. For viral tweets with thousands of retweets, you get a sample and that is it. The same applies to GET /2/tweets/:id/quote_tweets, which is rate-limited and tier-gated.

When I helped a media client migrate off the official API in early 2024, the breaking point was a reply-extraction workflow. They needed all replies under their brand's tweets going back 6 months. With the official API this would have required Enterprise full-archive search plus the v2 recent-search workaround, well over $5,000 per month, and a custom OAuth setup. With a third-party REST endpoint it became three function calls and ran on the Starter plan.

For a deeper look at the cost structure, see Twitter API Pricing 2026 and why the official X API is so expensive.


Signal density: comments vs quotes vs retweets

Before drilling into endpoints, a useful frame: not all engagement is equally informative.

Retweets are the lowest signal density. A retweet is one click. The user did not add commentary, did not explain why they were sharing it, did not push back. You learn one thing: this person decided their audience should see this. Useful for measuring reach, weak for understanding why.

Comments are medium signal. Replies contain text, which means sentiment, questions, objections, and corrections. But replies are also where the noise lives: low-quality "first" replies, spam bots, drive-by criticism. Volume is high, average quality is lower.

Quote tweets are the highest signal density. A quote requires effort. The user took your tweet, added their own framing, and broadcast that combination to their entire audience. The text of the quote is usually substantive: an endorsement, a critique, a "this aged poorly," a counterargument. For PR, competitive intelligence, and content analysis, quote tweets are where the actual conversation happens. They are also where a tweet can go viral in unexpected directions, since each quote is a new top-level post in the quoter's feed.

When I build engagement dashboards for clients, I weight quote tweets roughly 5x comments and 20x retweets for qualitative analysis. The exact ratio does not matter; the point is that volume and importance are inversely correlated across these three types.


Starting point: get a tweet's metrics

Before pulling the underlying engagement, get the aggregate counts. The /tweet-info endpoint returns the full tweet object including likes, retweets, replies, quotes, views, and bookmarks.

python
import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY, "Content-Type": "application/json"}


def get_tweet(tweet_link: str) -> dict:
    resp = requests.post(
        f"{BASE}/tweet-info",
        headers=HEADERS,
        json={"tweet_link": tweet_link},
    )
    resp.raise_for_status()
    return resp.json()


tweet = get_tweet("https://x.com/elonmusk/status/1234567890")

print(f"Author:    @{tweet['user']['username']}")
print(f"Text:      {tweet['full_text'][:100]}")
print(f"Likes:     {tweet.get('likes_count', 0):,}")
print(f"Retweets:  {tweet.get('retweet_count', 0):,}")
print(f"Quotes:    {tweet.get('quote_count', 0):,}")
print(f"Replies:   {tweet.get('reply_count', 0):,}")
print(f"Views:     {tweet.get('view_count', 0):,}")
print(f"Bookmarks: {tweet.get('bookmark_count', 0):,}")

For pulling metrics on many tweets at once, use /tweet-info-bulk, which accepts up to 100 tweet IDs per request and counts as a single API call. On the Pro plan that brings the per-tweet cost to roughly $0.00002, which matters when you are analyzing thousands of posts. See Optimizing API Usage for the batch patterns.


Get comments (replies) on a tweet

Endpoint: POST /v3/comments. Returns up to 20 replies per page as full tweet objects, with cursor-based pagination.

python
import time


def get_all_comments(tweet_link: str, max_pages: int = 20) -> list[dict]:
    """Fetch all replies under a tweet, paginating until cursor is exhausted."""
    comments, cursor = [], None

    for _ in range(max_pages):
        body = {"tweet_link": tweet_link}
        if cursor:
            body["next_cursor"] = cursor

        resp = requests.post(f"{BASE}/comments", headers=HEADERS, json=body)
        resp.raise_for_status()
        data = resp.json()

        comments.extend(data.get("tweets", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)

    return comments


comments = get_all_comments("https://x.com/elonmusk/status/1234567890")
print(f"Collected {len(comments)} comments")

What you can do with reply data

Each comment is a full tweet object: text, engagement metrics, and author profile. That unlocks several analysis patterns I see clients use regularly.

Sentiment classification. Feed full_text of each reply into a sentiment model (HuggingFace cardiffnlp/twitter-roberta-base-sentiment-latest is the de facto standard, or any LLM with a sentiment prompt). A tweet with 500 replies that come back 80% negative tells a fundamentally different story than 500 positive replies. We cover this end-to-end in Twitter Sentiment Analysis.

Top-voice extraction. Sort replies by likes_count to find the responses that shaped the conversation. The most-liked replies are usually the ones the rest of the audience saw as authoritative or amusing.

python
top = sorted(comments, key=lambda c: c.get("likes_count", 0), reverse=True)
for c in top[:5]:
    u = c["user"]
    print(f"@{u['username']} ({c['likes_count']} likes, "
          f"{u['followers_count']:,} followers): {c['full_text'][:80]}")

Question extraction. Filter replies containing ? to surface what your audience is asking. Useful for FAQ population, content ideation, and support-ticket detection. A line that took me an embarrassingly long time to write the first time:

python
questions = [c for c in comments if "?" in c["full_text"]]

Spam and bot filtering. Filter on author signals: minimum follower count, minimum account age, verified status. A simple heuristic that removes most low-quality replies:

python
def looks_legit(comment):
    u = comment.get("user", {})
    return (
        u.get("followers_count", 0) >= 10
        and not u.get("possibly_sensitive")
        and len(comment["full_text"]) > 5
    )

quality_comments = [c for c in comments if looks_legit(c)]

Get quote tweets

Endpoint: POST /v3/quotes. Same shape as /comments: 20 quote tweets per page, full tweet objects, cursor pagination.

python
def get_all_quotes(tweet_link: str, max_pages: int = 20) -> list[dict]:
    quotes, cursor = [], None

    for _ in range(max_pages):
        body = {"tweet_link": tweet_link}
        if cursor:
            body["next_cursor"] = cursor

        resp = requests.post(f"{BASE}/quotes", headers=HEADERS, json=body)
        resp.raise_for_status()
        data = resp.json()

        quotes.extend(data.get("tweets", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)

    return quotes

Analyzing quote tweets for reach and tone

Because quotes carry both author profile and text, you can rank them by reach (the quoter's follower count) and by content (the quote text itself).

python
quotes = get_all_quotes("https://x.com/brand/status/1234567890")

# Quotes that hit the biggest audiences
quotes.sort(key=lambda q: q["user"].get("followers_count", 0), reverse=True)

for q in quotes[:5]:
    u = q["user"]
    print(f"@{u['username']} ({u['followers_count']:,} followers)")
    print(f"  \"{q['full_text'][:120]}\"\n")

For brand monitoring, this is where I would start. Comments are noisy, retweets are anonymous votes, but a quote from a 200K-follower journalist or a competing exec is exactly the kind of signal that should trigger a Slack alert. A common pattern is to set a threshold (e.g., quoter follower count > 50K, or quoter is on a curated industry list) and route those quotes into a review channel.


Get retweeters of a tweet

Endpoint: POST /v3/retweeters. This is the one shape difference in the engagement family. The endpoint returns a UsersResponse (a list of user profiles), not a TweetsResponse. That is because a retweet has no independent text or metrics; it is just a redistribution of the original. The thing worth retrieving is who did the redistribution.

EndpointReturnsResponse keyContents
/commentsTweetstweetsFull tweet objects (text + author)
/quotesTweetstweetsFull tweet objects (text + author)
/retweetersUsersusersUser profile objects
python
def get_all_retweeters(tweet_link: str, max_pages: int = 20) -> list[dict]:
    retweeters, cursor = [], None

    for _ in range(max_pages):
        body = {"tweet_link": tweet_link}
        if cursor:
            body["next_cursor"] = cursor

        resp = requests.post(f"{BASE}/retweeters", headers=HEADERS, json=body)
        resp.raise_for_status()
        data = resp.json()

        retweeters.extend(data.get("users", []))
        cursor = data.get("next_cursor")
        if not cursor:
            break
        time.sleep(0.1)

    return retweeters

Audience analysis from retweeters

With full user profiles in hand, you can quantify reach and audience composition.

python
retweeters = get_all_retweeters("https://x.com/brand/status/1234567890")

total_reach = sum(u.get("followers_count", 0) for u in retweeters)
verified = sum(1 for u in retweeters if u.get("verified"))
avg_followers = total_reach / len(retweeters) if retweeters else 0

print(f"Retweeters:          {len(retweeters)}")
print(f"Combined reach:      {total_reach:,} followers")
print(f"Verified accounts:   {verified}")
print(f"Average follower #:  {avg_followers:,.0f}")

Combined reach is a back-of-the-envelope number (it double-counts overlapping audiences and ignores algorithmic delivery), but for relative comparison across tweets in the same campaign it is more useful than the raw retweet count. A tweet with 200 retweets from accounts averaging 50K followers had a fundamentally different ride than one with 500 retweets from accounts averaging 200 followers.


Full engagement report: one tweet, all dimensions

The functions above compose. Here is a complete engagement audit for a single tweet:

python
def full_engagement_report(tweet_link: str) -> dict:
    tweet = get_tweet(tweet_link)
    comments = get_all_comments(tweet_link, max_pages=10)
    quotes = get_all_quotes(tweet_link, max_pages=10)
    retweeters = get_all_retweeters(tweet_link, max_pages=10)

    print(f"Tweet by @{tweet['user']['username']}:")
    print(f"  \"{tweet['full_text'][:100]}\"")
    print(f"  Likes: {tweet.get('likes_count', 0):,} | "
          f"Views: {tweet.get('view_count', 0):,}\n")

    print(f"Comments collected: {len(comments)}")
    if comments:
        top = max(comments, key=lambda c: c.get("likes_count", 0))
        print(f"  Top reply: @{top['user']['username']} "
              f"({top['likes_count']} likes)")
        print(f"  \"{top['full_text'][:80]}\"\n")

    print(f"Quotes collected: {len(quotes)}")
    if quotes:
        biggest = max(quotes, key=lambda q: q["user"].get("followers_count", 0))
        u = biggest["user"]
        print(f"  Highest reach: @{u['username']} ({u['followers_count']:,} followers)")
        print(f"  \"{biggest['full_text'][:80]}\"\n")

    rt_reach = sum(u.get("followers_count", 0) for u in retweeters)
    print(f"Retweeters collected: {len(retweeters)}")
    print(f"  Combined reach: {rt_reach:,}")

    return {
        "tweet": tweet,
        "comments": comments,
        "quotes": quotes,
        "retweeters": retweeters,
    }

Sample output for a Series B announcement we audited last quarter:

Tweet by @brand:
  "We're excited to announce our Series B funding round of $50M..."
  Likes: 2,847 | Views: 892,000

Comments collected: 156
  Top reply: @tech_journalist (89 likes)
  "Congrats! What's the plan for international expansion?..."

Quotes collected: 43
  Highest reach: @vc_partner (284,000 followers)
  "This team has been on our radar for two years. Well deserved..."

Retweeters collected: 312
  Combined reach: 4,218,000

This costs 4 API calls plus pagination (typically 8 to 15 total calls depending on engagement volume), so a single audit runs under $0.03 on the Pro plan. That economics is what makes the next pattern, multi-tweet comparison, actually viable.


Comparing engagement across multiple tweets

Most engagement work is comparative. Which post in a campaign performed best? Which executive's tweets get the highest-quality engagement? Pull the tweet list first via /user-tweets or /search-tweets, then drill into each one.

python
def compare_tweet_engagement(tweet_links: list[str], pages_each: int = 3):
    results = []

    for link in tweet_links:
        tweet = get_tweet(link)
        comments = get_all_comments(link, max_pages=pages_each)
        quotes = get_all_quotes(link, max_pages=pages_each)
        retweeters = get_all_retweeters(link, max_pages=pages_each)

        rt_reach = sum(u.get("followers_count", 0) for u in retweeters)

        results.append({
            "snippet": tweet["full_text"][:50],
            "likes": tweet.get("likes_count", 0),
            "comments": len(comments),
            "quotes": len(quotes),
            "retweets": len(retweeters),
            "rt_reach": rt_reach,
        })
        time.sleep(0.5)

    return results

When I run this for clients, the interesting finding is usually not which tweet had the most engagement, but which tweet had a different shape of engagement. A tweet that gets disproportionate quote tweets relative to its likes is often controversial (good for visibility, sometimes bad for brand). A tweet with high comment-to-like ratio is conversation-starting. A tweet with high retweet-to-comment ratio is broadcast content: agreeable, share-worthy, but not discussion-starting.

Tip: If you only need aggregate counts for many tweets and not the underlying replies/quotes/retweeters, skip the per-tweet drill and use /tweet-info-bulk to pull 100 tweet metrics in a single call.


Cost and rate limits at scale

Engagement extraction can balloon quickly. A single viral tweet might have 50,000 comments. If you are auditing a brand's full timeline, you can easily generate tens of thousands of API calls. Two factors keep this manageable on Sorsa.

Flat-rate pricing. Every endpoint counts as 1 request from your quota, regardless of what it returns. On the Pro plan at $199/month you get 100,000 requests, which is enough for roughly 2 million tweets when you use batch endpoints. Compared to the official X API Pro tier at $5,000/month for 1M tweets, Sorsa Pro lands at roughly 4% of the cost for comparable volume.

A universal 20 req/sec rate limit. All Sorsa endpoints share the same limit: 20 requests per second per API key. No per-endpoint windows, no 15-minute resets, no surprise drops. If you hit it, you get a 429 and retry after one second. For deep engagement audits (paginating through 50K replies, for example) you can sustain this by spacing requests at 50ms intervals or using a simple semaphore. Custom higher limits are available on request.

For background on how X's rate-limit changes since 2023 have affected engagement workflows specifically, see Twitter API Rate Limits in 2026.


Verifying that a specific user engaged

A common adjacent use case: not "list everyone who engaged" but "did this specific person engage?" This shows up in giveaway verification, campaign compliance, and ambassador programs. Paginating through every retweeter to find one username is wasteful. Sorsa provides three dedicated verification endpoints that return a yes/no:

  • /check-comment: did user X reply to tweet Y?
  • /check-quoted: did user X quote tweet Y?
  • /check-retweet: did user X retweet tweet Y?

Each costs one API call regardless of how many comments/quotes/retweets exist. For a campaign with 2,000 entrants and three required actions, this is 6,000 calls, well within the Starter plan. Full patterns are covered in Marketing Campaign Verification and the Twitter Follow Checker guide.


Exporting engagement data

The endpoints return JSON, but most analysis happens in spreadsheets, dataframes, or databases. A minimal CSV exporter:

python
import csv


def export_comments_csv(comments: list[dict], path: str = "comments.csv"):
    fields = [
        "comment_id", "created_at", "full_text",
        "likes", "retweets", "reply_count",
        "author_username", "author_followers", "author_verified",
    ]
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fields)
        writer.writeheader()
        for c in comments:
            u = c.get("user", {})
            writer.writerow({
                "comment_id": c["id"],
                "created_at": c["created_at"],
                "full_text": c["full_text"],
                "likes": c.get("likes_count", 0),
                "retweets": c.get("retweet_count", 0),
                "reply_count": c.get("reply_count", 0),
                "author_username": u.get("username", ""),
                "author_followers": u.get("followers_count", 0),
                "author_verified": u.get("verified", False),
            })

The same pattern works for quotes (also tweet objects). For retweeters, swap the fields to user attributes: username, display_name, followers_count, verified, created_at.

For larger jobs, write directly to a database. Postgres with a jsonb column for the raw payload plus a few indexed columns (tweet_id, author_id, created_at, likes_count) is enough for tens of millions of rows. If you are joining engagement data with other social signals long-term, see Historical Data for archival patterns.


FAQ

Can you get all replies to a tweet using the Twitter API?

Not directly with the official X API. There is no /tweets/:id/replies endpoint. The official workaround is to query the recent search endpoint with conversation_id:<tweet_id>, which is limited to the last 7 days unless you have Academic Research or Enterprise full-archive access. Third-party APIs like Sorsa expose a direct /comments endpoint that returns replies for any public tweet with cursor pagination, regardless of age.

What is the difference between a retweet and a quote tweet?

A retweet redistributes the original tweet as-is, with no added text. A quote tweet is a new tweet that embeds the original and adds the quoter's own commentary. From a data perspective: retweets carry no independent content, so APIs return retweeter profiles only. Quotes are full tweet objects with their own text, engagement counts, and threading. For analysis, quotes are far more informative than retweets.

How do I see who retweeted a tweet?

The official X API's GET /2/tweets/:id/retweeted_by endpoint returns retweeters but is rate-limited to 75 requests per 15 minutes and returns a maximum of 100 retweeters per call. For viral tweets you only get a sample. Sorsa's /retweeters endpoint paginates without that cap and returns full user profiles, not just IDs.

Does the X API show comments on a tweet?

The X API does not have a comments-of-a-tweet endpoint. Replies are accessible only via the search endpoint using conversation_id, which has tier restrictions and time limits. The behavior surprises most developers coming from other social APIs where comments_of(post) is a first-class operation.

How many replies can the API return per request?

Sorsa's /comments, /quotes, and /retweeters endpoints all return up to 20 results per page (200 for some endpoints on the user-graph side). Pagination via next_cursor is unbounded, so you can fetch every reply on a tweet of any age in a loop. The X API's recent search returns up to 100 results per page but is gated by tier-specific request limits and the 7-day window.

Can you get engagement data for old tweets?

With Sorsa, yes. The /comments, /quotes, and /retweeters endpoints work on any public tweet regardless of age. With the official X API, replies are only retrievable for tweets posted in the last 7 days unless you have Academic Research or Enterprise full-archive search (both require approval and significant cost). See Historical Data for more on retrieving older content.

Is there a free way to get tweet engagement data?

The free tier of the X API is intentionally minimal: you cannot retrieve engagement data on it at all. The Basic tier ($200/month) gives limited search access but hits rate limits quickly. For exploratory work, Sorsa's API playground lets you test endpoints from the browser without a paid plan; you only pay once you start running them at volume.

How do I calculate engagement rate from API data?

Engagement rate is typically (likes + replies + retweets + quotes) / impressions, or / follower_count if impressions are unavailable. The view_count field on the tweet object gives you impressions for tweets posted after X enabled view counts (mid-2022). For older tweets you can substitute follower count of the author. Sorsa's free engagement calculator computes this across recent tweets for any public account.


Getting started

If you want to try this yourself:

  1. Sign up at api.sorsa.io and get your API key.
  2. Test endpoints with no code at the playground, or use the API reference for full specs.
  3. Drop the code in this guide into a Python script, swap the tweet URL, and run.

For migrating an existing pipeline off the official X API, the migration guide walks through the request mapping. For questions, the team is on Discord and at contacts@sorsa.io.


Written by Daniel Kolbassen, data engineer and API infrastructure consultant. Daniel has worked with the Twitter/X API since the v1.1 era and has helped over 40 companies restructure their social-data pipelines.

Last updated: May 18, 2026.