# X Profile Scraper - No Login (`renzomacar/x-profile-scraper`) Actor

Bulk-resolve X (Twitter) handles into full public profiles: followers, following, bio, links, location, join date, verification and pinned post. No login. Same price on every Apify plan.

- **URL**: https://apify.com/renzomacar/x-profile-scraper.md
- **Developed by:** [Renzo Madueno](https://apify.com/renzomacar) (community)
- **Categories:** Social media, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.25 / 1,000 profile returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## X Profile Scraper - No Login

Feed it X (Twitter) handles. Get back full public profiles: followers, following, post count, bio, bio links, website, location, join date, verification status, avatar, banner, pinned post, and professional category.

No login. No cookies. No browser. No Twitter account.

### What makes this one worth using

**Same price on every Apify plan.** $0.25 per 1,000 profiles whether you are on the free plan or on Business. No free-tier multiplier, no reduced result cap for free users, no invoice that arrives looking nothing like the price on the listing page.

**Dead handles cost nothing.** Hand it a list of 10,000 handles scraped from somewhere and a good chunk of them will be suspended, renamed or deleted. Those produce a `FAILURES` entry naming the handle and the reason. They do not produce a dataset row and they do not produce a charge. On a real list that is often 5-15% of your input, and it is the difference between paying for data and paying for absence.

**Renamed accounts are flagged, not silently swapped.** X resolves an old handle to whatever the account is called now. That is useful, but you need to know it happened. Every row carries `queriedHandle` (what you asked for) and `handleChanged` (a boolean). Nothing gets quietly substituted underneath you.

### Input

```json
{
  "handles": [
    "nasa",
    "@apify",
    "https://x.com/naval",
    "esa"
  ],
  "minFollowers": 1000
}
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `handles` | array | — | Handles or profile URLs. Aliases: `handle`, `usernames`, `username`, `urls`, `url`, `profileUrls`, `startUrls`. |
| `maxItems` | integer | none | Ceiling on delivered rows. Aliases: `maxResults`, `limit`, `maxProfiles`. |
| `minFollowers` | integer | none | Drop accounts below this follower count. Filtered accounts are not charged. |
| `joinedAfter` | string | — | Only accounts created on or after this date. |
| `joinedBefore` | string | — | Only accounts created on or before this date. |

`nasa`, `@nasa`, `NASA` and `https://x.com/nasa` all work, as do profile URLs with `/with_replies`, `/media` or query strings attached. Duplicates are collapsed case-insensitively before anything is fetched, so the same account listed three times costs once.

#### On date filters

A profile has exactly one timestamp: the day the account was created. So this actor supports `joinedAfter` and `joinedBefore`, and **rejects post-date filters with an explicit error**:

> X Profile Scraper cannot filter by date on this data (`postedAfter` was supplied). X does not expose a usable timestamp for this surface, so honouring the filter is impossible and ignoring it would hand you rows outside your range. Supported date fields here: `joinedAfter`, `joinedBefore`.

The alternative — accepting the field and ignoring it — is how you end up with a dataset that quietly does not match what you asked for. A loud error costs you thirty seconds. A silent one costs you an analysis.

`joinedAfter` and `joinedBefore` accept `YYYY-MM-DD`, a full ISO timestamp, or a relative window like `2 years`, `18 months`, `90 days`. They are genuinely useful: filtering out accounts created in the last six months is one of the cheapest bot filters available.

### Output

One row per profile. Fill rates below are measured across a verification sample of six real accounts on 2026-08-22.

| Field | Fill | Notes |
|---|---|---|
| `id` | 100% | Numeric X user ID. Stable across handle changes. |
| `handle`, `name`, `url` | 100% | `handle` is the account's *current* handle. |
| `description` | 100% | The bio, as plain text. |
| `createdAt` | 100% | Account creation date, ISO 8601 UTC. |
| `followersCount`, `followingCount` | 100% | |
| `tweetsCount`, `mediaCount`, `likesCount`, `listedCount` | 100% | |
| `isBlueVerified`, `isVerified`, `isProtected` | 100% | `isBlueVerified` is the paid checkmark; `isVerified` is the retired legacy one. |
| `isDefaultProfileImage` | 100% | A strong bot signal. |
| `profileImageUrl`, `profileImageUrlLarge` | 100% | The large variant is the 400x400 original. |
| `profileBannerUrl` | 100% | |
| `websiteUrl` | ~83% | The link in the profile header, un-shortened from `t.co`. |
| `pinnedTweetId` | ~67% | |
| `location` | ~50% | Free text as typed by the user. Not geocoded, and not always a real place. |
| `professionalType` | ~50% | e.g. `Business`, `Creator`. |
| `descriptionLinks` | ~33% | Links inside the bio text, un-shortened. |
| `affiliateLabel` | ~33% | The parent organisation on affiliated accounts. |
| `professionalCategory` | ~17% | The self-declared business category. |
| `queriedHandle`, `handleChanged`, `scrapedAt` | 100% | |

#### Data integrity, not just data presence

The fill rates above say a field arrived. These rules make sure what arrived is usable:

- Follower and post counts are normalised to integers, including abbreviated display values like `"1.5M"`.
- **Negative counts become `null`.** On very large accounts a 32-bit overflow can surface as something like `-576135159`. That is not a follower count, and silently passing it through would wreck any ranking built on the column.
- `createdAt` is parsed from X's own format (`Wed Oct 10 20:19:24 +0000 2018`) into ISO 8601. A value that cannot be read becomes `null`, never a 1970 epoch date that would quietly pass a `joinedAfter` filter it should have failed.
- `websiteUrl` and `descriptionLinks` are resolved past `t.co`, so you get the real destination rather than a shortlink you would have to expand yourself.

#### What is deliberately absent

**Email addresses and phone numbers.** X does not publish them. No scraper of X can produce them honestly, and any that claims to is either guessing or enriching from somewhere else without telling you.

**Follower and following lists.** Those need a logged-in session. This actor does not use one.

### How it works

A single GraphQL call per handle — `UserByScreenName`, the same request x.com makes when a logged-out visitor loads a profile page. Authentication is a guest token from `POST /1.1/guest/activate.json`: free, tied to no account, and refreshed automatically inside the run whenever X rotates or rejects it. Retired GraphQL query IDs are rotated automatically as well.

No browser, no proxy required, 512 MB of memory. Platform compute cost is negligible next to the per-result price.

Roughly 150 ms of spacing between lookups keeps runs well inside what the public endpoint tolerates. A 10,000-handle run takes about half an hour and costs $2.50.

### Errors go where you can act on them

Nothing that failed appears in the dataset. It all goes into the `FAILURES` key-value record:

```json
{
  "actor": "X Profile Scraper",
  "targetsRequested": 4,
  "resultsDelivered": 3,
  "resultsCharged": 3,
  "emptyRowsSuppressed": 0,
  "failureCount": 1,
  "failures": [
    {
      "target": "someaccountthatisgone",
      "error": "Account does not exist, is suspended, or was renamed - X returns nothing for it"
    }
  ],
  "notes": [
    "1 handle(s) no longer resolve to a public account. They are listed in this record and were not charged."
  ]
}
```

That record doubles as a cleaning report. Diff it against your input list and you have your list hygiene for free.

**A run that delivers nothing is marked FAILED.** If every handle in your list is dead, the run does not finish green over an empty dataset. It fails, and names the reason.

### Pricing

**$0.25 per 1,000 profiles. Pay per result. No subscription. Same price on every Apify plan.**

| Profiles | Cost |
|---|---|
| 100 | $0.025 |
| 1,000 | $0.25 |
| 10,000 | $2.50 |
| 100,000 | $25.00 |

Suspended handles, deleted accounts, unparseable inputs and filtered-out profiles all cost $0.00.

### Typical uses

- **Lead qualification.** You have a list of handles from a conference, a newsletter, a GitHub org or a set of replies. Resolve them into follower counts, bios and websites and decide who is worth contacting.
- **List hygiene.** Run your CRM's stored handles through it quarterly. The `FAILURES` record tells you which contacts have gone dark and `handleChanged` tells you who renamed.
- **Influencer shortlisting.** Followers, post count, listed count and account age in one table, then filter with `minFollowers`.
- **Bot and authenticity screening.** The `audit` dataset view puts account age, default-avatar status, follower/following ratio and post count side by side. Accounts created recently, with a default avatar and a lopsided ratio, are the classic pattern.
- **Enrichment for another dataset.** Chain it after any actor that emits X handles — the replies scraper in this fleet is a natural upstream.
- **Competitive tracking.** Re-run weekly against a fixed list and diff follower counts to get real growth curves rather than screenshots.

### Limits worth knowing before you start

- **Protected accounts return almost nothing.** X publishes only minimal metadata about them to logged-out clients. They are reported honestly rather than padded out.
- **`location` is free text.** Users type whatever they want in it, including jokes and fictional places. It is not geocoded and should not be treated as a reliable geographic field.
- **`isVerified` is the retired legacy checkmark**, not the current paid one. `isBlueVerified` is the one that means something today. Both are returned so you can tell them apart.
- **No follower or following lists.** Those require a logged-in session, which this actor deliberately does not use.

### Verified

Every endpoint, field name and fill rate in this README was tested live against X on **2026-08-22** from a datacenter IP with no cookies and no account. The fill rates come from actual measurement across a sample of real accounts, not from documentation.

### Related actors

- **Tweet Scraper by URL or ID** — individual tweets, $0.20/1k.
- **X Profile Posts Scraper - No Cookies** — an account's timeline, with a real per-account cap, $0.30/1k.
- **Tweet Replies Scraper - No Login** — publicly visible replies, $0.30/1k.
- **X Trends Scraper by Country** — 62 countries and 400+ cities, $0.40/1k.

Same rules across all of them: one price on every plan, and nothing charged for nothing.

# Actor input Schema

## `handles` (type: `array`):

Handles or profile URLs, one per line. "nasa", "@nasa" and "https://x.com/nasa" all work. Duplicates are collapsed before anything is charged.

## `maxItems` (type: `integer`):

Hard stop on delivered rows. Leave empty for no cap.

## `minFollowers` (type: `integer`):

Drop accounts below this follower count. Filtered accounts are never charged.

## `joinedAfter` (type: `string`):

YYYY-MM-DD, a full ISO timestamp, or a relative window such as "2 years". A profile has only one timestamp - the day the account was created - so this is the one date filter that means anything here. Post-date filters are rejected with an explicit error rather than silently ignored.

## `joinedBefore` (type: `string`):

YYYY-MM-DD, a full ISO timestamp, or a relative window such as "1 year". Useful for filtering out freshly minted accounts.

## Actor input object example

```json
{
  "handles": [
    "nasa"
  ],
  "joinedAfter": "2020-01-01",
  "joinedBefore": "2024-01-01"
}
```

# Actor output Schema

## `results` (type: `string`):

One row per X profile: followers, following, bio, location, website and join date.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "handles": [
        "nasa",
        "@apify",
        "https://x.com/naval"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("renzomacar/x-profile-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "handles": [
        "nasa",
        "@apify",
        "https://x.com/naval",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("renzomacar/x-profile-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "handles": [
    "nasa",
    "@apify",
    "https://x.com/naval"
  ]
}' |
apify call renzomacar/x-profile-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,renzomacar/x-profile-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/T7fd4VahV2xUAnhOp/builds/RCCr5G9WhtPO09LXY/openapi.json
