Paste a public profile or post. Web Unlocker returns the X page with no login: a profile's followers, bio and 5 latest posts, each post with exact likes, reposts, replies and views. $0.98 per 1,000 pages.
1,000 free requests · Pay only on success · 195+ countries · Claude ready
X pages usually take 2 to 10 seconds. It is still working, no need to click again.
Ready for your own accounts? Get an API key or book a demo.
Tell us a little about your team and get:
Public pages, with no login, no X account and no X API key. Social listening teams, agencies, brands and AI agents read these two kinds of page, each billed as one page.





Want to try it yourself? Take your API key with 1,000 free requests.
Web Unlocker bills a post page once, with its exact replies, reposts, likes and views, at $0.98 per 1,000 pages. A Twitter scraper on Apify bills $0.05 for each single post link, and Bright Data $1.50 per 1,000 records.
The X API bills $5.00 per 1,000 posts.
Here 1,000 cost $0.98.
A request the service cannot answer costs nothing. In our test 6 of 50 first tries came back with a quick refusal, and asking again brought the page.
What 1,000 X posts cost
1,000 public posts with likes, reposts, replies and views, at published prices
Gologin: $0.98 per 1,000 pages, one page a post. Bright Data bills each record, $1.50 per 1,000. The X API bills $0.005 per post read. Twitter Scraper Lite on Apify bills $0.05 per single post link, plus $0.0004 per post.
Ready to run it on your own posts? Get your API key and free requests.
Our Twitter web scraping test: 20 brand and creator profiles, one fresh post from each, and 10 smaller accounts, each page asked for up to three times, the way the demo above does it. This is what came back, and how fast.
44 of 50 came back on the first try.
The other 6 on the second or third.
The official X API, Apify's Twitter scrapers and Bright Data's X Scraper API: what each bills for the same 1,000 posts and profiles, and what it asks of you before you start.
| What it takes | ||||
|---|---|---|---|---|
| You pay for | A page | Each post or profile | Each query and post | Each record |
| 1,000 posts by link | $0.98 | $5.00 | $50.40 | $1.50 |
| 1,000 profiles | $0.98 | $10.00 | $4.40 | $1.50 |
| Developer account | Not needed | Needed | Not needed | Not needed |
| Repeat checks of an account | Yes | Yes | Limited | Yes |
| Search by keyword | No | Yes | Yes | Yes |
| Exact follower counts | Rounded | Yes | Yes | Yes |
| Free to start | 1,000 requests | No | $5 a month | 5,000 a month |
| Same price on any other site | Yes | No | No | No |
Scraping more than X? Web Unlocker returns any page the same way, at the same price.
A profile page brings an account's 5 latest posts with exact likes, reposts, replies and views. 100 accounts give you 500 fresh posts for $0.098, as often as you check. Apify's main Twitter scraper asks you not to repeat a query.
The X API bills $5 for 1,000 posts.
Here 1,000 come from 200 profiles for $0.20.
X sends a logged-out visitor the data in the page's HTML, with exact counts under every post. Web Unlocker returns that HTML, and a few lines of code read it. The Accounts list tab is a small Twitter crawler: accounts in, latest posts in a CSV out.
Fill in the form above. The key arrives by email within one business day.
A profile for its followers and 5 latest posts, a post for its exact likes, reposts, replies and views.
Check that the page came with data, and ask again when the service sent a quick refusal.
pip install requestsGOLOGIN_SCRAPING_API_KEY=your_key python x_profile.pyhttps://parsing.webunlocker.gologin.com/v1/scrapeimport html
import json
import os
import re
import time
from datetime import datetime, timezone
import requests
username = "NASA"
def scrape(url):
for attempt in range(3):
address = url if attempt == 0 else f"{url}?fresh={int(time.time())}"
response = requests.get(
"https://parsing.webunlocker.gologin.com/v1/scrape",
params={"url": address, "geo": "us", "format": "json"},
headers={"apikey": os.environ["GOLOGIN_SCRAPING_API_KEY"]},
timeout=90,
)
page = response.json().get("data", {}).get("html", "") if response.ok else ""
if 'role="group"' in page:
return page
time.sleep(3)
raise RuntimeError("The page came back without data, try again later")
def meta(page, name):
found = re.search(rf'<meta (?:property|name)="{name}" content="([^"]*)"', page)
return html.unescape(found.group(1)) if found else None
def count(page, word):
found = re.search(rf">([\d.,]+[KMB]?)</span>\s*<span[^>]*>{word}<", page)
return found.group(1) if found else None
def text_of(chunk):
start = chunk.find('aria-label="More"')
if start < 0:
return ""
start = chunk.find("</button>", start) + len("</button>")
ends = [chunk.find(mark, start) for mark in ('inert=""', "data-press-control", "<video", 'role="group"', "<article")]
end = min([chunk.rfind("<", 0, at) for at in ends if at > 0] or [len(chunk)])
words = re.sub(r"<script.*?</script>|<[^>]+>", "", chunk[start:end], flags=re.S)
return html.unescape(words).strip()
page = scrape(f"https://x.com/{username}")
posts = []
start = 0
for match in re.finditer(r'aria-label="([^"]*)"[^>]*role="group"', page):
chunk = page[start : match.start()]
start = match.end()
link = re.search(r'href="/(\w+)/status/(\d+)', chunk)
stamp = re.search(r'"timestamp":(\d+)', chunk)
counts = {word.rstrip("s").replace("replie", "reply"): int(value) for value, word in re.findall(r"(\d+) (\w+)", match.group(1))}
posts.append(
{
"url": f"https://x.com/{link.group(1)}/status/{link.group(2)}",
"time": datetime.fromtimestamp(int(stamp.group(1)) / 1000, timezone.utc).isoformat() if stamp else None,
"pinned": ">Pinned</span>" in chunk[: chunk.find("data-post-avatar")],
"text": text_of(chunk),
"replies": counts.get("reply", 0),
"reposts": counts.get("repost", 0),
"likes": counts.get("like", 0),
"bookmarks": counts.get("bookmark", 0),
"views": counts.get("view"),
}
)
profile = {
"username": meta(page, "profile:username"),
"name": meta(page, "author"),
"bio": meta(page, "description"),
"followers": count(page, "Followers"),
"following": count(page, "Following"),
"posts": meta(page, "twitter:data1"),
"joined": meta(page, "twitter:data2"),
"latest_posts": posts,
}
print(json.dumps(profile, indent=2, ensure_ascii=False))
print(f"@{username}: {profile['followers']} followers, {len(posts)} latest posts with exact counts from one page")GOLOGIN_SCRAPING_API_KEY=your_key node x-posts.mjshttps://parsing.webunlocker.gologin.com/v1/scrapeimport { writeFileSync } from 'node:fs';
const links = [
'https://x.com/NASA/status/2120305504295510080',
'https://x.com/SpaceX/status/2120808224324510133',
'https://x.com/Spotify/status/2120607242374504556',
];
async function scrape(url) {
for (let attempt = 1; attempt <= 3; attempt++) {
const address = attempt === 1 ? url : `${url}?fresh=${Date.now()}`;
const params = new URLSearchParams({ url: address, geo: 'us', format: 'json' });
const response = await fetch(`https://parsing.webunlocker.gologin.com/v1/scrape?${params}`, {
headers: { apikey: process.env.GOLOGIN_SCRAPING_API_KEY },
});
const page = response.ok ? (await response.json()).data?.html : '';
if (page?.includes('role="group"')) return page;
await new Promise((resolve) => setTimeout(resolve, 3000));
}
throw new Error(`${url} came back without data, try again later`);
}
const meta = (page, name) => page.match(new RegExp(`<meta (?:property|name)="${name}" content="([^"]*)"`))?.[1] ?? '';
const count = (label, word) => Number(label.match(new RegExp(`(\\d+) ${word}`))?.[1] ?? 0);
const posts = [];
for (const url of links) {
const page = await scrape(url);
const label = page.match(/aria-label="([^"]*)"[^>]*role="group"/)[1];
posts.push({
url,
author: meta(page, 'og:title').replace(/ on X$/, ''),
date: meta(page, 'article:published_time').slice(0, 10),
replies: count(label, 'repl'),
reposts: count(label, 'repost'),
likes: count(label, 'like'),
bookmarks: count(label, 'bookmark'),
views: count(label, 'view'),
});
}
writeFileSync('posts.csv', ['url,author,date,replies,reposts,likes,bookmarks,views', ...posts.map((post) => Object.values(post).map((value) => JSON.stringify(value)).join(','))].join('\n'));
console.table(posts);
console.log(`${posts.length} posts in ${posts.length} pages: $${(posts.length * 0.00098).toFixed(4)} on the Growth plan`);GOLOGIN_SCRAPING_API_KEY=your_key node x-accounts.mjshttps://parsing.webunlocker.gologin.com/v1/scrapeimport { writeFileSync } from 'node:fs';
const accounts = ['NASA', 'NatGeo', 'SpaceX', 'Spotify', 'YouTube'];
async function scrape(url) {
for (let attempt = 1; attempt <= 3; attempt++) {
const address = attempt === 1 ? url : `${url}?fresh=${Date.now()}`;
const params = new URLSearchParams({ url: address, geo: 'us', format: 'json' });
const response = await fetch(`https://parsing.webunlocker.gologin.com/v1/scrape?${params}`, {
headers: { apikey: process.env.GOLOGIN_SCRAPING_API_KEY },
});
const page = response.ok ? (await response.json()).data?.html : '';
if (page?.includes('role="group"')) return page;
await new Promise((resolve) => setTimeout(resolve, 3000));
}
return '';
}
const count = (label, word) => Number(label.match(new RegExp(`(\\d+) ${word}`))?.[1] ?? 0);
const rows = [];
for (const account of accounts) {
const page = await scrape(`https://x.com/${account}`);
const followers = page.match(/>([\d.,]+[KMB]?)<\/span>\s*<span[^>]*>Followers</)?.[1] ?? '';
let start = 0;
for (const group of page.matchAll(/aria-label="([^"]*)"[^>]*role="group"/g)) {
const chunk = page.slice(start, group.index);
start = group.index + group[0].length;
const [, author, id] = chunk.match(/href="\/(\w+)\/status\/(\d+)/) ?? [];
const time = chunk.match(/"timestamp":(\d+)/)?.[1];
rows.push({
account,
followers,
post: `https://x.com/${author}/status/${id}`,
date: time ? new Date(Number(time)).toISOString().slice(0, 10) : '',
replies: count(group[1], 'repl'),
reposts: count(group[1], 'repost'),
likes: count(group[1], 'like'),
views: count(group[1], 'view'),
});
}
}
writeFileSync('latest-posts.csv', ['account,followers,post,date,replies,reposts,likes,views', ...rows.map((row) => Object.values(row).map((value) => JSON.stringify(value)).join(','))].join('\n'));
console.table(rows);
console.log(`${rows.length} posts from ${accounts.length} accounts in ${accounts.length} pages: $${(accounts.length * 0.00098).toFixed(4)} on the Growth plan`);Ready to run it on your own accounts? Get your API key and free requests.
One account, one proxy network, one ecosystem. Pick the product the job needs.
Scraping more sites? See the Instagram, Reddit and YouTube scrapers, or every web scraping tool.
A Twitter scraper, now also called an X scraper, is a tool that reads public X (formerly Twitter) pages and turns them into data: a profile's name, bio, followers and latest posts, a post's text, time, replies, reposts, likes and views. Gologin Web Unlocker is a Twitter scraper API: send it a page address and it returns the page with that data, at $0.98 per 1,000 pages.
Yes, with no login. X shows public profiles and posts to logged-out visitors, with the data in the page itself. Web Unlocker opens them as a logged-out visitor from a US address, so you need no X account, no cookies and no X API key: in our test 50 of 50 pages came back with data.
Send the profile or post address to Web Unlocker with the requests library and read the counts from the page. The Python tab above is a complete Python Twitter scraper, tested on the live API: it prints a profile's followers and its 5 latest posts with exact likes, reposts, replies and views.
Yes. The official X API is one way to read posts, the public page is another. Web Unlocker reads the page, so you skip the developer account, the X API bill and its monthly cap of 3 million post reads.
Not for reading posts at scale. The X API now bills per use: $0.005 a post read and $0.010 a profile, up to 3 million post reads a month. Web Unlocker comes with 1,000 free requests, enough for 1,000 posts or profiles.
Twitter API pricing is now X API pricing, billed per use: $5.00 for 1,000 posts and $10.00 for 1,000 profiles. Web Unlocker bills $0.98 per 1,000 pages either way, and a profile page already carries the account's 5 latest posts: 1,000 posts from 200 profiles cost $0.20.
$980 if you read each post from its own link, at $0.98 per 1,000 pages. Read them from profile pages and 1 million posts take 200,000 pages of 5 posts each: $196. The X API bills $5,000 for the same million posts.
Yes. Each request loads the live page, so the counts are the ones X shows at that moment: in our test the same NASA post had 4,337 likes, and 4,341 twenty minutes later.
Not with Web Unlocker. X opens search and hashtag pages only after a login, so a logged-out request lands on the sign-up page. Start from the accounts and posts you follow: their pages are public, and the Accounts list sample above checks a whole list of them.
In part. A post page brings its top 3 or 4 replies, each with its author, text and exact likes, reposts and views. The full list of replies opens only after a login.
Not as a list: who follows an account is shown after a login only. The follower count is public and comes with every profile, rounded the way X shows it to logged-out visitors (92.2M, 28M): the page carries no exact figure. Every post's counts come exact.
The 5 latest, as X shows them to a logged-out visitor, each with exact replies, reposts, likes, bookmarks and views. A pinned post comes first and is marked as pinned. For older posts, use their links: each post page is one request.
It depends on the job. Apify's scrapers search by keyword and bill each post. The X API reads everything, at $5 per 1,000 posts. Web Unlocker returns the public profile or post page with exact counts at one price per page, on X and on any other site.
Yes. Apify's main Twitter scraper takes searches only, at least 50 posts a query, and asks you not to repeat the same query; single posts go through its Lite version at $0.05 a link. Web Unlocker takes any profile or post link at $0.98 per 1,000 pages, as often as you need.
Profiles and posts that load without a login are public pages. In X Corp. v. Bright Data, in May 2024, a US federal judge dismissed X's lawsuit over the scraping of public data. What you collect and how you use it is your call, so check it against the rules that apply to your project.
Yes. An agent sends the profile or post address to the same API and reads the counts from the page. With MCP & AI Skills, Claude builds that tool from a description of the job.
Tell us a little about your team and get: