X (Twitter) Followers Scraper - $0.09 per 1,000 avatar

X (Twitter) Followers Scraper - $0.09 per 1,000

Pricing

from $0.09 / 1,000 accounts

Go to Apify Store
X (Twitter) Followers Scraper - $0.09 per 1,000

X (Twitter) Followers Scraper - $0.09 per 1,000

Asking X who follows an account gets whole profiles back, so each row already has the bio, follower and following counts, verification, location, avatar, website and join date. No API key, nothing to paste. $0.09 per 1,000.

Pricing

from $0.09 / 1,000 accounts

Rating

0.0

(0)

Developer

Dami's Studio

Dami's Studio

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

2 days ago

Last modified

Share

X (Twitter) Followers Scraper

Handles in, followers out, one row per account. Set mode to following and you get the accounts they follow instead.

Every row is a full profile: handle, display name, bio, follower and following counts, post count, verification badge and its type, location, avatar, banner, website, and the date the account joined X.

No API key, no cookie to paste, no browser.

  • Followers or following, chosen with a single field. The same columns either way.
  • Profile fields come from X itself at read time, so the counts are live rather than cached.
  • Several handles per run, each paged independently up to your own per-handle limit.
  • A handle that doesn't exist, or is protected, costs nothing. It returns an uncharged row saying so.
  • Empty input returns one labelled sample row, free.

Price

$0.09 per 1,000 accounts. Plus $0.0005 per run as the platform start fee, billed per gigabyte of run memory, so exactly that on the default 1 GB.

No volume tiers, no minimum spend, no subscription.

AccountsTotal
100$0.0095
1,000$0.0905
10,000$0.9005
100,000$9.0005

What gets charged

  • One follower-scraped event per account row written to the dataset. Nothing else is metered per row.
  • Free: the sample row an empty run returns, and every diagnostic row. They all carry "charged": false.
  • Accounts already returned for the same handle in the same run are dropped before they're charged.
  • Handles that don't exist, are suspended or are protected return an uncharged diagnostic row.
  • A run that finds nothing costs the start fee and nothing else.
  • Rows never leave the dataset without a charge and are never charged without a row. The billed event is a named one, so nothing is quietly attached to apify-default-dataset-item.

Input

{
"handles": [
"NASA",
"github"
],
"mode": "followers",
"maxItemsPerHandle": 200
}
FieldWhat it does
handlesOne or more X handles, with or without the leading @. Full profile URLs such as https://x.com/NASA work too. Up to 10 per run.
modefollowers (default) returns the accounts that follow each handle. following returns the accounts each handle follows.
maxItemsPerHandleHow many accounts per handle. Default 50, hard ceiling 2,000 per handle and 5,000 per run. Keep it low while testing, since you pay per row.
enrichProfilesOn by default. Leave it on. It's what fills in follower counts, location, join date and the rest. Turn it off only if all you want is the list of handles.
proxyUrlsLeave empty. Only for callers who want traffic to leave through proxy servers they already pay for, as http://user:pass@host:port.
sessionCookiesOptional, and not needed for a normal run. See When no public source answers.

Run it with empty input and you get one labelled sample row, free.

Output

One row per account. This is a real row from a real run:

{
"ok": true,
"charged": true,
"recordType": "profile",
"sourceHandle": "NASA",
"relation": "follower",
"userName": "NASAHubble",
"name": "Hubble",
"url": "https://x.com/NASAHubble",
"description": "The official X account for the NASA Hubble Space Telescope, managed and operated by NASA's Goddard Space Flight Center.",
"followersCount": 8906746,
"followingCount": 44,
"tweetsCount": 8427,
"isVerified": true,
"verifiedType": "government",
"location": "Goddard Space Flight Center",
"profileImageUrl": "https://pbs.twimg.com/profile_images/3468011581/efb985f24af0a814a722457a768f3cc5_400x400.jpeg",
"createdAt": "2008-03-06T20:15:02.000Z",
"isProtected": false,
"website": "http://www.nasa.gov/mission_pages/hubble/main/index.html",
"profileBannerUrl": "https://pbs.twimg.com/profile_banners/14091091/1669391293",
"listedCount": 15230,
"mediaCount": 3116,
"favouritesCount": 1241,
"userId": "14091091",
"profileEnriched": true,
"scrapedAt": "2026-08-16T15:48:36.774Z"
}

Field notes

  • sourceHandle is the handle you asked about, the account whose list this row came from. Use it to split a multi-handle run.
  • relation is follower when the row is somebody who follows sourceHandle, following when it's somebody sourceHandle follows.
  • userName is the handle exactly as X spells it, without the @.
  • userId is X's numeric account id. It never changes even when the handle does, so it's the better key for anything long-lived.
  • description is the bio as written, line breaks and emoji intact.
  • followersCount is the live count at read time, straight from X.
  • isVerified is true when the account carries a checkmark of any kind. verifiedType says which: blue, business, government or creator. Null when the account isn't verified.
  • location is the free-text location field on the profile. People write anything they like in it, including jokes.
  • website is the link on the profile, expanded from the shortened form X displays.
  • profileImageUrl is the avatar at 400x400 rather than the 48px thumbnail X returns by default.
  • createdAt is the join date, in ISO 8601 UTC.
  • isProtected is true when the account is private.
  • profileEnriched is true when the profile fields were filled in from X. False means only the handle and display name were available for that row. Rare, and visible so you can filter on it.

Real rows carry "charged": true. Sample rows carry "_sample": true, diagnostic rows carry "_diagnostic": true and an errorCode. Neither is billed.

How it works

For each handle it first asks X about the handle itself. That settles in one request whether the account exists, whether it's protected, and how many entries the list even has, so a typo doesn't turn into a long billed run.

It then reads the follower or following list page by page from public, logged-out sources, following the same cursor the pages expose, until it has as many accounts as you asked for.

Every account it finds is then looked up on X directly, and that's where the profile fields come from. The counts, the location, the badge, the avatar and the join date are what X itself reports at read time.

Requests leave through a rotating pool of addresses, so a per-address rate limit is answered by moving rather than waiting.

When no public source answers

Occasionally none of the public sources answers for a handle during a run. Without a cookie of your own, that handle comes back as an uncharged BLOCKED diagnostic row and the run carries on. Re-running usually clears it.

If you pull at volume and would rather that case be covered, paste an auth_token=…; ct0=… pair from your own logged-in x.com into sessionCookies. Both values are needed. X requires the ct0 value to be echoed back in a header, so an auth_token on its own is rejected. The run then reads the list from X directly on your account and your rate limit. It's stored as a secret, and it's pointless unless you're running large jobs. Leaving it empty is the normal case.

The closing log line carries sessionState, so you can see which path a run took.

Ordering, and what "the first 200 followers" means

The list comes back in the order X keeps it, which for followers is newest first. The account that followed most recently is row one. So maxItemsPerHandle: 200 gives you the 200 most recent followers, not a random 200 and not the oldest 200. That's usually what you want for audience tracking, and it's what makes a weekly re-run cheap, because the new arrivals are all at the top.

The following list is ordered the same way, most recently followed first.

There's no way to jump to an arbitrary offset. If you need 2,000 accounts you pay for 2,000 rows and the run pages through them in order.

What people use it for

  • Audience research. Pull the followers of three accounts in your niche and see who overlaps, who's verified, and how big their own audiences are.
  • Lead lists. Filter on followersCount, location or a keyword in description to get a shortlist worth reaching out to.
  • Competitive audience mapping. Run the same handles weekly and diff on userId to see exactly who arrived and who left.
  • Influencer vetting. Read the following list of an account and check what kind of profiles it actually follows before you pay for a placement.
  • Community building. Find the accounts that follow several of your peers but not you yet.

Reading the output

  • Real rows carry "charged": true and "recordType": "profile". One billed event each.
  • The sample row carries "_sample": true and "charged": false. There's exactly one, only when the input had no handles.
  • Diagnostic rows carry "_diagnostic": true, "charged": false and an errorCode: NOT_FOUND when a handle doesn't exist, is suspended or is protected, NO_RESULTS when the list is genuinely empty, BLOCKED when no public source answered for that handle on that run, NETWORK for a transport failure, TIME_BUDGET when time ran out. Each carries a plain-English error and the sourceHandle it belongs to.

Filter on charged == true and you have exactly the rows you paid for.

Limits

  • A protected account keeps its followers and following lists private. The run returns one uncharged row saying so and moves on. There's no public way to read them.
  • Hard ceilings: 2,000 accounts per handle, 5,000 rows per run, 10 handles per run.
  • The list is read newest-first and can't be started from an arbitrary offset, so a deep pull of an account with millions of followers means paging through the rows in between.
  • Follower and following counts are a snapshot at read time. On an active account they'll differ a minute later.
  • location is free text people fill in themselves. Often a joke, a country, or empty. It isn't geocoded.
  • A small number of accounts come back with only the handle and display name, usually because the account was suspended or renamed between the list page and the lookup. Those rows carry profileEnriched: false so you can filter them out.
  • Email addresses and phone numbers aren't returned. X doesn't publish them and nothing here guesses them.
  • Deleted, suspended and deactivated accounts don't appear in the list at all. X removes them from it, so there's nothing to report.

Questions

Do I need an X API key, a developer account or my own cookies?

No. Nothing is asked of you beyond the handles, and the normal path uses no credentials at all.

So what is sessionCookies for?

The rare run where no public source answers for a handle. Without a cookie that handle comes back as an uncharged BLOCKED row. With yours, the run reads the list from X directly on your account and your rate limit. Worth doing if you pull at volume, pointless otherwise.

Can I get the followers of a private account?

No, and neither can anything else. X keeps a protected account's lists private. You get an uncharged row saying the account is protected, and you're not billed for it.

How do I get the accounts a handle follows instead of its followers?

Set mode to following. The columns are identical, and relation changes on every row so mixed datasets stay unambiguous.

Can I run several handles at once?

Yes, up to ten per run. Each handle gets its own maxItemsPerHandle budget, and each row carries sourceHandle so you can split the dataset afterwards.

Will the run fail if something goes wrong?

No. A blocked, empty or missing handle produces an uncharged diagnostic row and the run still finishes as succeeded. A failed run would still bill the start fee, which would mean paying to be told something went wrong.

Can I schedule it?

Yes. Nothing is held between runs. Diff on userId rather than userName. Handles change, ids don't.

How fresh are the numbers in each row?

They're read at the moment the row is built, directly from X. scrapedAt records exactly when.