Updated: July 2026. Added the 100 free requests starting offer to the getting-started and pricing guidance, and refreshed the official X API cost comparison for its current pay-per-use pricing.
Key Takeaway: Twitter (X) lets you verify five user actions through an API: follows, retweets, quote tweets, comments, and community membership. Each check returns a single true/false result. Likes became private in June 2024 and can no longer be verified by any tool. Account ownership is proven by having the user post a unique code.
Running a giveaway, ambassador program, or quest campaign that rewards actions on X breaks the moment fake handles, half-finished tasks, and bots start filling your form. By participant three hundred, manual review is hopeless and honor-system checkboxes are worthless. What you actually need is an API that answers, for every participant, "did this person really do what they claimed?" Sorsa API, an alternative Twitter/X API provider, exposes each verification as one yes/no endpoint: pass a handle and an action, get a boolean back. There is no OAuth handshake and no developer-account approval, just a single ApiKey header; the rate limit is a flat 20 requests per second on every plan; every account starts with 100 free requests (one-time, no card required, valid across all 40 endpoints and never expiring), enough to verify a full test campaign before you commit; and at flat per-request pricing (from $0.0049 on the entry plan, dropping to roughly $0.002 on Pro) it costs a fraction of reconstructing the same checks on the official X API, which bills per resource across full lists. Sorsa is read-only, so it verifies and reads public data but does not post, follow, or DM; for write actions you would still use the official API.
We build and operate these endpoints, and across dozens of campaigns we have run and audited this pattern, mostly for creator-marketing agencies and giveaway tools. The endpoints are simple. The pitfalls (private accounts, spoofed handles, bot farms, the lost ability to check likes) are not. This guide walks through every check with working Python, then stitches them into a full campaign pipeline: one participant first, then bulk verification for tens of thousands. If you would rather run the whole flow without writing code, our giveaway verification solution wraps these same checks behind a UI.
Table of Contents
- What You Can and Cannot Verify on X
- How Verification Works on the Official X API vs Sorsa
- Check 1: Did the User Follow an Account?
- Check 2: Did the User Retweet a Tweet?
- Check 3: Did the User Quote a Tweet?
- Check 4: Did the User Comment on a Tweet?
- Check 5: Is the User a Community Member?
- Building a Full Campaign Verification Pipeline
- Verifying Participants in Bulk
- Verifying Account Ownership
- Anti-Fraud: Account Quality Checks
- Influence-Weighted Reward Scoring
- In Practice: 47,000 Participants in 14 Days
- Cost per Participant
- Getting Started
- FAQ
What You Can and Cannot Verify on X
Five user actions on X are verifiable through an API today: a follow, a retweet, a quote tweet, a comment, and joining a Community. Each maps to one endpoint and returns a boolean. Likes are no longer verifiable by any tool, public or third-party, because X made them private in June 2024. Views are not verifiable either.
| Action | Endpoint | Method | Returns | Verifiable? |
|---|---|---|---|---|
| User follows an account | /check-follow | POST | {follow: true/false} | Yes |
| User retweeted a tweet | /check-retweet | POST | {retweet: true/false} | Yes |
| User quoted a tweet | /check-quoted | POST | {status: "quoted" / "retweet" / "not_found"} | Yes |
| User commented on a tweet | /check-comment | GET | {commented: true/false, tweet: {...}} | Yes |
| User joined an X Community | /check-community-member | POST | {is_member: true/false} | Yes |
| User liked a tweet | (none) | (none) | (none) | No, private since June 2024 |
| User viewed/impressed a tweet | (none) | (none) | (none) | No |
The likes restriction is the most common surprise. In June 2024, X made likes private for everyone: only the author of a post can see who liked it, and no API (including the official X API) can answer "did user A like tweet B" anymore. If you have old campaign templates that include "Like this post," replace that task with a retweet or comment. Both remain fully verifiable and produce stronger engagement signals anyway.
Everything else in the table above is a single API call. Authentication is a single ApiKey header (no OAuth flow, no app approval), and the response is plain JSON. The rest of this guide is the practical how-to.
How Verification Works on the Official X API vs Sorsa
The official X API has no endpoint that directly answers whether a specific user followed, retweeted, quoted, or commented. You reconstruct each answer by fetching full lists of followers, retweeters, or repliers and searching them, billed per resource fetched. A purpose-built verification API instead returns one boolean per check in a single request.
This is the single biggest reason teams reach for a dedicated verification layer, and it is worth seeing side by side. The numbers below are the official X API's current pay-per-use rates and Sorsa's flat per-request pricing.
| Task | Official X API (pay-per-use) | Sorsa API |
|---|---|---|
| Did a user follow an account | No direct endpoint. Paginate the account's full follower or following list and search it; every profile returned is billed as a user read ($0.010 each). | One /check-follow request returns follow: true/false. |
| Did a user retweet a tweet | No direct endpoint. Fetch the full retweeter list and search it for the handle. | One /check-retweet request returns retweet: true/false, with pagination for very large lists. |
| Did a user quote a tweet | No direct endpoint. Fetch quote tweets and match the author. | One /check-quoted request returns quoted, retweet, or not_found. |
| Did a user comment | No direct endpoint. Fetch the reply list and search for the handle. | One /check-comment request returns commented: true/false plus the reply. |
| Is a user a community member | No equivalent public endpoint. | One /check-community-member request returns is_member: true/false. |
| Authentication | OAuth 2.0 with a Bearer token and an approved developer app. | A single ApiKey header. No approval queue. |
| Billing | Per resource fetched: $0.005 per post, $0.010 per user profile, with a 2,000,000 post-read monthly cap. | Flat per request: 1 call = 1 request, from $0.0049 (Starter) down to roughly $0.002 (Pro), every endpoint included. |
| Rate limit | Varies by endpoint, in fixed windows. | Flat 20 requests per second on every plan. |
There is a second, quieter advantage to checking against the full audience rather than a sample. The public X interface and most free giveaway pickers only surface the most recent slice of an audience, often the last 100 or so retweeters or repliers, so a winner drawn from them silently excludes everyone who entered earlier. Verification against the complete list avoids that: Sorsa's /check-retweet paginates 100 entries at a time through the entire list, and the direct per-user checks answer for a specific participant no matter where they sit in the audience.
If your goal is the underlying engagement data rather than a yes/no answer (the full list of repliers, quoters, or retweeters and their metrics), that is a different job, covered in our Twitter engagement API guide.
Check 1: Did the User Follow an Account?
The most common campaign task ("Follow @YourBrand to enter"). The /check-follow endpoint answers this directly. The endpoint's logic is "does user_2 follow user_1?" so user_1 is the brand and user_2 is the participant.
Endpoint: POST https://api.sorsa.io/v3/check-follow
Parameters
Provide one identifier for the brand (the followed account) and one for the participant.
| Parameter | Type | Required | Description |
|---|---|---|---|
username_1 | string | One of | The brand's handle. |
user_link_1 | string | these | Or the brand's profile URL. |
user_id_1 | string | Or the brand's numeric user ID. | |
username_2 | string | One of | Participant's handle. |
user_link_2 | string | these | Or participant's profile URL. |
user_id_2 | string | Or participant's numeric user ID. |
Python
import requests
API_KEY = "YOUR_API_KEY"
BASE = "https://api.sorsa.io/v3"
HEADERS = {"ApiKey": API_KEY, "Content-Type": "application/json"}
def check_follow(brand_handle: str, participant_handle: str) -> dict:
resp = requests.post(
f"{BASE}/check-follow",
headers=HEADERS,
json={"username_1": brand_handle, "username_2": participant_handle},
timeout=15,
)
resp.raise_for_status()
return resp.json()
result = check_follow("YourBrand", "participant123")
if result["follow"]:
print("Follow verified.")
elif result.get("user_protected"):
print("Account is private; follow cannot be confirmed.")
else:
print("Not following.")
Response
{
"follow": true,
"user_protected": false
}
Edge cases to know
If user_protected is true, the participant's account is private and their follow graph is not visible to any third party. You have three options: reject the entry, ask the participant to make their account public for verification, or use account-ownership verification (covered below) to confirm they own the handle and then accept the entry on a manual override. In our experience, fewer than 1% of giveaway participants have private accounts, so a hard reject with a clear message is usually fine.
This check answers one direction of a follow relationship for a campaign. To check a single follow or mutual follows outside a campaign context, see our guide on how to check if one account follows another, or run a one-off check in the browser with the no-code follow checker tool.
Check 2: Did the User Retweet a Tweet?
"Retweet this post to enter." Standard mechanic for boosting reach. The /check-retweet endpoint returns a boolean and paginates for large lists.
Endpoint: POST https://api.sorsa.io/v3/check-retweet
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
tweet_link | string | Yes | URL of the tweet to verify. |
username | string | One of | Participant handle. |
user_link | string | these | Or profile URL. |
user_id | string | Or numeric user ID. | |
next_cursor | string | No | Pagination for tweets with > 100 retweets. |
Python
def check_retweet(tweet_link: str, participant_handle: str) -> bool:
cursor = None
for _ in range(5): # check up to 500 retweets total
body = {"tweet_link": tweet_link, "username": participant_handle}
if cursor:
body["next_cursor"] = cursor
resp = requests.post(f"{BASE}/check-retweet", headers=HEADERS, json=body, timeout=15)
resp.raise_for_status()
data = resp.json()
if data["retweet"]:
return True
cursor = data.get("next_cursor")
if not cursor:
return False
return False
How pagination works
Each call scans the most recent 100 retweets. If your tweet has thousands of retweets and the user retweeted early, their action may be deeper in the list and require pagination. The example above caps at 5 pages (the most recent 500 retweets) to keep verification fast. For most campaigns this is more than enough because participants tend to retweet within hours of seeing the prompt, so their action sits at the top of the list.
This is the practical difference from drawing a winner off the native X interface or a free picker, which typically only see the most recent batch. Because /check-retweet walks the full list 100 at a time, an early retweeter is found just as reliably as a late one. The official X API offers no equivalent direct check: you would fetch the entire retweeter list and search it yourself, with OAuth setup, windowed rate-limit accounting, and your own pagination logic on top.
Check 3: Did the User Quote a Tweet?
"Quote this with your thoughts." This is more valuable than a plain retweet because the quote tweet adds the participant's own commentary and amplifies the campaign with personalized text.
Endpoint: POST https://api.sorsa.io/v3/check-quoted
The /check-quoted endpoint is smart about distinguishing a quote from a plain retweet, returning one of three statuses.
Python
def check_quoted(tweet_link: str, participant_handle: str) -> dict:
resp = requests.post(
f"{BASE}/check-quoted",
headers=HEADERS,
json={"tweet_link": tweet_link, "username": participant_handle},
timeout=15,
)
resp.raise_for_status()
return resp.json()
data = check_quoted("https://x.com/YourBrand/status/1234567890", "participant123")
if data["status"] == "quoted":
print(f"Quote verified on {data['date']}: {data['text']}")
elif data["status"] == "retweet":
print("Retweeted without commentary; does not satisfy quote requirement.")
else:
print("No quote or retweet found.")
Why the quote text matters
The response includes the full quote text and date, which you can pipe into a quality check before approving the entry. A campaign that requires "quote with your thoughts on the new product" deserves more than a one-word quote like "nice". Most teams running these campaigns apply a minimum-character rule (typically 30 to 50 characters), a profanity check, and a required hashtag if the campaign uses one.
def quote_is_acceptable(quote_text: str, min_length: int = 30, required_hashtag: str = None) -> bool:
if len(quote_text.strip()) < min_length:
return False
if required_hashtag and required_hashtag.lower() not in quote_text.lower():
return False
return True
Check 4: Did the User Comment on a Tweet?
"Drop a comment under this post." The only verification endpoint that uses GET instead of POST.
Endpoint: GET https://api.sorsa.io/v3/check-comment
Parameters (query string)
| Parameter | Type | Required | Description |
|---|---|---|---|
tweet_link | string | Yes | URL of the tweet. |
username | string | One of | Participant handle. |
user_link | string | these | Or profile URL. |
user_id | string | Or numeric user ID. |
Python
def check_comment(tweet_link: str, participant_handle: str) -> dict:
resp = requests.get(
f"{BASE}/check-comment",
headers={"ApiKey": API_KEY},
params={"tweet_link": tweet_link, "username": participant_handle},
timeout=15,
)
resp.raise_for_status()
return resp.json()
data = check_comment("https://x.com/YourBrand/status/1234567890", "participant123")
if data["commented"]:
text = data["tweet"]["full_text"]
print(f"Comment verified: {text[:120]}")
else:
print("No comment found.")
Response and comment quality
When commented is true, the response from /check-comment includes the full tweet object of the comment itself: text, engagement metrics, language detection, timestamp. Use this to enforce minimum length, required keywords, or reject emoji-only spam replies. In a campaign where the comment is the entire engagement task, the quality bar should be higher than a single emoji.
def comment_is_acceptable(comment: dict, min_length: int = 20, required_keyword: str = None) -> bool:
text = comment.get("full_text", "").strip()
if len(text) < min_length:
return False
if required_keyword and required_keyword.lower() not in text.lower():
return False
# Reject emoji-only or single-word comments
if len(text.split()) < 3:
return False
return True
Check 5: Is the User a Community Member?
"Join our X Community to participate." Useful when you want the participant to be a sustained part of the community rather than a one-shot retweeter.
Endpoint: POST https://api.sorsa.io/v3/check-community-member
def check_community_member(community_id: str, participant_handle: str) -> bool:
resp = requests.post(
f"{BASE}/check-community-member",
headers=HEADERS,
json={"community_id": community_id, "username": participant_handle},
timeout=15,
)
resp.raise_for_status()
return resp.json().get("is_member", False)
is_member = check_community_member("1966045657589813686", "participant123")
print("Member" if is_member else "Not a member")
The community ID is the long numeric string in the community URL (x.com/i/communities/<id>). The /check-community-member endpoint returns a clean boolean. Communities are often a more durable signal than a one-time retweet because joining a community signals intent to stay engaged.
Building a Full Campaign Verification Pipeline
In a real campaign, participants complete several tasks. Here is a pattern that runs all five checks for one participant, returns a structured result, and applies quality rules to the comment and quote.
from dataclasses import dataclass, field
@dataclass
class CampaignConfig:
brand_handle: str
tweet_to_retweet: str
tweet_to_quote: str
tweet_to_comment: str
community_id: str
required_hashtag: str = ""
min_quote_length: int = 30
min_comment_length: int = 20
@dataclass
class ParticipantResult:
username: str
follow: bool = False
retweet: bool = False
quote: bool = False
quote_text: str = ""
comment: bool = False
comment_text: str = ""
community: bool = False
completed: int = field(init=False, default=0)
def total(self) -> int:
return sum([self.follow, self.retweet, self.quote, self.comment, self.community])
def verify_participant(username: str, cfg: CampaignConfig) -> ParticipantResult:
r = ParticipantResult(username=username)
# Follow
r.follow = check_follow(cfg.brand_handle, username)["follow"]
# Retweet
r.retweet = bool(check_retweet(cfg.tweet_to_retweet, username))
# Quote tweet (with quality check)
quote_data = check_quoted(cfg.tweet_to_quote, username)
if quote_data["status"] == "quoted":
r.quote_text = quote_data.get("text", "")
r.quote = quote_is_acceptable(r.quote_text, cfg.min_quote_length, cfg.required_hashtag)
# Comment (with quality check)
comment_data = check_comment(cfg.tweet_to_comment, username)
if comment_data.get("commented"):
r.comment_text = comment_data["tweet"].get("full_text", "")
r.comment = comment_is_acceptable(comment_data["tweet"], cfg.min_comment_length)
# Community
r.community = check_community_member(cfg.community_id, username)
r.completed = r.total()
return r
cfg = CampaignConfig(
brand_handle="YourBrand",
tweet_to_retweet="https://x.com/YourBrand/status/111111111",
tweet_to_quote="https://x.com/YourBrand/status/222222222",
tweet_to_comment="https://x.com/YourBrand/status/333333333",
community_id="1966045657589813686",
required_hashtag="#YourLaunch",
)
result = verify_participant("participant123", cfg)
print(f"@{result.username}: {result.completed}/5 tasks done")
A single participant costs 5 API requests (one per task). At Sorsa's universal rate limit of 20 requests per second, one worker thread can verify roughly 4 participants per second sequentially. For most campaigns running verification once per submission, that is more than enough headroom.
Verifying Participants in Bulk
When a campaign has thousands of participants and you want to verify them in a batch (for example, before announcing winners), the pattern looks like this. Note the rate-limit handling, the CSV output, and the resumable design (writes a row immediately after each participant so a crash does not lose progress).
import csv
import time
from pathlib import Path
def verify_campaign_batch(usernames: list[str], cfg: CampaignConfig, output_file: str) -> None:
fields = ["username", "follow", "retweet", "quote", "comment", "community",
"completed", "quote_text", "comment_text"]
already_done = set()
out_path = Path(output_file)
if out_path.exists():
with out_path.open() as f:
already_done = {row["username"] for row in csv.DictReader(f)}
mode = "a" if out_path.exists() else "w"
with out_path.open(mode, newline="") as f:
writer = csv.DictWriter(f, fieldnames=fields)
if mode == "w":
writer.writeheader()
for i, username in enumerate(usernames):
if username in already_done:
continue
try:
r = verify_participant(username, cfg)
writer.writerow({
"username": r.username,
"follow": r.follow,
"retweet": r.retweet,
"quote": r.quote,
"comment": r.comment,
"community": r.community,
"completed": r.completed,
"quote_text": r.quote_text,
"comment_text": r.comment_text,
})
f.flush()
print(f"[{i+1}/{len(usernames)}] @{username}: {r.completed}/5")
except requests.HTTPError as e:
if e.response.status_code == 429:
print("Rate limit hit, sleeping 5s and retrying...")
time.sleep(5)
continue
print(f"[{i+1}] @{username}: ERROR {e}")
time.sleep(0.25) # stay safely under 20 req/s with 5 reqs per participant
participants = open("entries.txt").read().splitlines()
verify_campaign_batch(participants, cfg, "campaign_results.csv")
This pattern verifies roughly 14,000 participants per hour single-threaded. If you parallelize across two or three workers (still respecting the global 20 req/s ceiling), you can hit 30,000 per hour. For most campaigns under 100,000 entries, single-threaded sequential verification finishes overnight.
Verifying Account Ownership
Before a participant can win anything, you may want to prove they actually own the X handle they submitted. Without this step, anyone can paste a famous handle into your form and claim the reward. The standard pattern: generate a unique code, ask the participant to post a tweet containing it, then check their recent timeline for the code.
import secrets
def generate_verification_code(prefix: str = "VERIFY") -> str:
return f"{prefix}-{secrets.token_hex(4)}"
def verify_account_ownership(username: str, expected_code: str) -> bool:
"""Check if the user posted a tweet containing the verification code."""
resp = requests.post(
f"{BASE}/user-tweets",
headers=HEADERS,
json={"username": username},
timeout=15,
)
resp.raise_for_status()
tweets = resp.json().get("tweets", [])
for tweet in tweets:
if expected_code in tweet.get("full_text", ""):
return True
return False
# Workflow
code = generate_verification_code()
print(f"Ask the user to post a tweet containing: {code}")
# ... user posts the tweet ...
if verify_account_ownership("participant123", code):
print("Account ownership confirmed.")
else:
print("Code not found in recent tweets.")
This is the same mechanism most serious giveaway and ambassador platforms use. The participant can delete the tweet after verification if they want, since you only need to confirm the post once.
Anti-Fraud: Account Quality Checks
Automated campaigns attract bots, and platforms running large-scale quest mechanics invest heavily in sybil prevention as a result. A few API-level checks rule out the obvious offenders without needing a full sybil-detection system. Each one is one additional call to /info.
from datetime import datetime, timezone
def is_legitimate_account(
username: str,
min_age_days: int = 30,
min_tweets: int = 10,
min_followers: int = 5,
) -> tuple[bool, dict]:
resp = requests.get(
f"{BASE}/info",
headers={"ApiKey": API_KEY},
params={"username": username},
timeout=15,
)
resp.raise_for_status()
profile = resp.json()
created = datetime.fromisoformat(profile["created_at"].replace("Z", "+00:00"))
age_days = (datetime.now(timezone.utc) - created).days
checks = {
"account_age_ok": age_days >= min_age_days,
"has_tweets": profile.get("tweets_count", 0) >= min_tweets,
"has_followers": profile.get("followers_count", 0) >= min_followers,
"not_protected": not profile.get("protected", False),
}
return all(checks.values()), checks
Three observations from running this in production:
- Minimum age of 30 days catches most fresh bot accounts. Bot farms typically register accounts in batches and use them within days. A 30-day floor knocks out the majority. Push to 90 days if your campaign is high-value.
- Zero-tweet accounts are almost always fake. A minimum of 5 to 10 existing tweets is a strong signal of real human use.
- Follower-to-following ratio matters less than you think. Real people with 50 followers and 800 following are common (passive consumers). Do not use ratio as a primary filter.
These thresholds clear the obvious bots cheaply. For high-value campaigns where you want to go further and score how much of a participant's own follower base looks fake or inactive, our guide on auditing fake followers walks through that deeper pass.
Apply this check before running any of the five verification checks. If is_legitimate_account returns False, you save 5 verification requests on a participant you would have rejected anyway.
Influence-Weighted Reward Scoring
Not all participants have the same reach. A retweet from a creator with 50,000 followers is worth more to a brand campaign than one from an account with 50. The straightforward fix is to weight each task's point value by a logarithmic function of the participant's follower count.
import math
BASE_POINTS = {"follow": 10, "retweet": 15, "quote": 25, "comment": 20, "community": 10}
def get_follower_count(username: str) -> int:
resp = requests.get(
f"{BASE}/info",
headers={"ApiKey": API_KEY},
params={"username": username},
timeout=15,
)
resp.raise_for_status()
return resp.json().get("followers_count", 0)
def calculate_weighted_points(result: ParticipantResult) -> dict:
followers = get_follower_count(result.username)
# log scaling: 100 followers -> 2x, 10K -> 4x, 1M -> 6x
multiplier = max(1.0, math.log10(followers + 1))
total = 0
breakdown = {}
for task, base in BASE_POINTS.items():
if getattr(result, task):
points = round(base * multiplier)
breakdown[task] = points
total += points
return {"followers": followers, "multiplier": round(multiplier, 2),
"breakdown": breakdown, "total": total}
The result: an account with 50 followers completing all five tasks earns about 80 points. An account with 50,000 followers completing the same tasks earns about 380 points. The campaign rewards reach proportionally without paying micro-celebrities the same as zero-reach accounts.
For crypto-flavored campaigns, you can replace the follower-count multiplier with the Sorsa Score, which measures an account's recognition among crypto KOLs, projects, and VCs. Two accounts can have similar follower counts but very different Sorsa Scores if one is a crypto-native voice and the other is a general-interest account.
In Practice: 47,000 Participants in 14 Days
A creator-marketing agency we worked with launched a 14-day giveaway for a direct-to-consumer home goods brand. The campaign mechanic was standard: follow the brand, retweet the launch tweet, quote it with a branded hashtag, comment on a second tweet, and join their X Community. Three winners would each receive a furnished room makeover valued at roughly $4,500.
Submissions came in via a campaign landing page. By day 14 they had 47,000 entries.
The agency's previous campaigns, run on honor-system checkboxes, typically saw 50 to 60 percent fake or partial completions, requiring days of manual review before announcing winners. This time they used the API verification pipeline above. Numbers from the run:
- 47,000 total submissions
- 235,000 verification requests (5 per participant)
- 47,000 additional
/infocalls for the anti-fraud and influence-weight steps - Total API usage: ~282,000 requests over the campaign window
- Plan used: Enterprise (500K requests/month at $899)
- 31,200 participants passed all 5 tasks
- 8,400 participants passed 3 to 4 tasks (eligible for partial-prize tier)
- 7,400 participants rejected outright (failed account-quality check or completed 0 to 2 tasks)
- Manual moderation time saved: ~120 hours (their estimate, based on past campaigns at similar scale)
The cost of running verification at this scale (one month of the Enterprise plan) was less than a single day of moderator time. The agency now uses the same pipeline as a template for every brand campaign they run.
Disclosure: Sorsa API is our product, and the figures above describe an anonymized, composite client deployment rather than one named engagement. The technical claims are accurate and the cost advantage is a real property of flat per-request pricing; for your own campaign, run a small pilot before committing to a workflow.
Cost per Participant
A full five-task verification, with anti-fraud and influence scoring layered on, takes 7 API requests per participant:
- 1 request for
/info(anti-fraud plus follower count for scoring) - 5 requests for the five verification checks
- 1 request optionally for account-ownership verification (if implemented)
Sorsa uses flat-rate pricing: 1 API call = 1 request from the monthly quota, regardless of endpoint. On the Pro plan ($199/mo, 100,000 requests), that is roughly 14,000 fully-verified participants per month. On Enterprise ($899/mo, 500,000 requests), about 71,000. Custom plans are available above that ceiling.
| Campaign size | Requests needed | Recommended plan | Plan price |
|---|---|---|---|
| Up to 1,400 participants | ~10,000 | Starter | $49/mo |
| Up to 14,000 participants | ~100,000 | Pro | $199/mo |
| Up to 71,000 participants | ~500,000 | Enterprise | $899/mo |
| 71,000+ participants | Custom | Contact sales | Custom |
At Pro-plan rates, a full 7-request verification costs roughly $0.014 per participant. For a 10,000-participant campaign, that is approximately $140 in API requests.
The contrast with the official X API comes from the billing model, not a single headline price. X bills per resource fetched, $0.005 per post and $0.010 per user profile, and has no direct check endpoint, so verifying actions means pulling whole retweeter and follower lists and paying for every item in them, under a 2 million post-read monthly cap and OAuth 2.0. Sorsa charges one flat request per /check-* call regardless of how large the underlying audience is. The full per-plan and per-endpoint breakdown for both providers lives in our Twitter API pricing guide, and if you are still weighing options, our rundown of Twitter API alternatives compares the wider field on cost and capabilities.
Getting Started
You can have a working verification check running in a few minutes. There is no developer-account approval to wait on and no OAuth flow to wire up: create a key, put it in the ApiKey header, and call /check-follow. Every account starts with 100 free requests, a one-time allowance that needs no card, never expires, and covers all 40 endpoints, so you can verify a small test batch before paying anything. After that the entry plan is $49 for 10,000 requests, every endpoint is included on every tier, and the rate limit is a flat 20 requests per second.
- Try the endpoints with no code in the API playground.
- Read the quickstart to make your first authenticated request.
- Follow the campaign verification walkthrough for the end-to-end pipeline in the docs.
- For campaigns above 500,000 requests a month or a higher rate limit, talk to sales.
For the full menu of verification endpoints and response shapes, see the verification endpoint reference.
FAQ
Can I verify Twitter likes via API?
No. X made likes private in June 2024, and as of 2026 no public or third-party API can answer whether one user liked another's tweet. This is a platform-level change, not a Sorsa limitation. If your campaign template still asks for likes, replace that task with a retweet or comment, both of which remain fully verifiable and carry stronger engagement signals.
Do I need OAuth or developer-account approval to verify Twitter actions?
Not with Sorsa API. Authentication is a single ApiKey header, with no OAuth handshake, no callback URLs, and no app review or approval queue. The official X API does require OAuth 2.0 and an approved developer app, and it bills per resource fetched, which makes verification at scale slower to set up and more expensive than a flat per-request model.
Can I check if someone retweeted a tweet that has thousands of retweets?
Yes. The /check-retweet endpoint scans 100 retweets per call and returns a next_cursor to paginate. The example code in this guide caps at 5 pages (500 retweets), which is usually enough because participants typically retweet within hours of being prompted. For tweets where you need to scan deeper, raise the page cap and the check keeps walking the full list.
How do I verify someone actually owns the Twitter handle they entered?
Generate a unique short code, ask the participant to post a tweet containing it, then use the user-tweets endpoint to scan their recent timeline for that code. A working example is in the account-ownership section of this guide. This is the standard pattern serious giveaway and ambassador platforms use to prevent people from submitting a handle they do not control.
What happens if a participant has a private (protected) account?
The check-follow and check-quoted responses include a user_protected flag. When it is true, the account's follow graph is not exposed and you cannot programmatically confirm a follow or quote. Your options are to ask the participant to make their profile public for verification, reject the entry with a clear message, or run account-ownership verification and accept on a manual override. In a typical campaign, under 1% of participants have private accounts.
How do I prevent bots from gaming my giveaway?
Use three API-level layers of defense: an account-quality check via the /info endpoint for minimum age, tweet count, and followers; comment and quote quality checks for minimum length and a required hashtag; and account-ownership verification before any reward goes out. None of these is a full sybil-detection system, but together they remove the easy-win cases that drain most campaigns, and each rejected account also saves you the verification requests you would have spent on it.
Is verifying engagement cheaper than using the official X API?
For verification specifically, yes. The official X API has no direct check endpoint, so you reconstruct each answer by pulling full retweeter or follower lists and paying per resource, $0.005 per post and $0.010 per profile, under a 2 million post-read cap. Sorsa charges one flat request per check regardless of audience size, from $0.0049 down to about $0.002 per request, so a full campaign verification runs at a small fraction of the cost.
Can I use these checks for non-marketing use cases?
Yes. Common non-marketing uses include access-gating a private Discord channel by verifying a user follows the brand before granting a role, tracking employee or partner social-amplification compliance, and validating user-submitted attribution claims in affiliate programs. Each is the same single boolean check, just applied outside a giveaway context.
Reviewed by Keksich, founder of Sorsa, marketer and X API researcher.
How this guide was put together: it draws on our hands-on work building and operating Sorsa's verification endpoints, on live calls against the API itself, and on the Sorsa API documentation for endpoint and response details. The cost comparison uses the official X API's current pay-per-use pricing and policy pages as the reference for its rates, the 2 million post-read cap, and its OAuth requirement. Verified June 2026.