# Twitch Scraper — Channels, Streams, Clips & VODs (`scrapyx/twitch-scraper`) Actor

Scrapes public Twitch data with no login or API key: channel profiles and follower counts, live stream status and viewers, top categories, clips, past broadcasts and search. Anonymous access is capped at 30 rows per query by Twitch, and every run reports it.

- **URL**: https://apify.com/scrapyx/twitch-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Social media, Videos, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Twitch Scraper — Channels, Streams, Clips & VODs

Public Twitch data, with **no login, no API key and no OAuth app to register**.

> ## ⚠️ Requires a residential proxy
>
> Twitch gates this API by **exit-IP reputation**. From a residential
> connection every request succeeds. From a datacenter IP — which is what
> Apify runs on by default — Twitch answers **`failed integrity check`** on
> even a single simple lookup, and a freshly minted `Client-Integrity` token
> does not lift it (verified on real cloud runs, bare and through two
> datacenter proxy groups).
>
> **Set `proxyConfiguration` to Apify Proxy with the `RESIDENTIAL` group
> before running.** Without it every row comes back as an `integrity_refused`
> error naming this exact cause — the actor fails loudly rather than
> returning empty data.

Six things in one actor, chosen with a **mode** setting:

| Mode | You give it | You get back |
|---|---|---|
| **Channel profiles** | channel names or URLs | profile, follower count, live status, social links |
| **Live streams** | a category name (or nothing) | who's live now, viewers, title, tags, category |
| **Top categories** | — | the most-watched categories right now, with viewer counts |
| **Clips** | channel names | top clips, views, duration, who clipped them |
| **Videos (VODs)** | channel names | past broadcasts, highlights, uploads |
| **Search** | search terms | matching channels and categories |

### Why use this actor

- **No credentials at all.** Twitch's official API needs a registered developer
  app and an OAuth token. This needs neither — nothing to set up, nothing to expire.
- **Live status in one field.** `isLive` is a real boolean, plus viewer count,
  title, category and tags when the channel is streaming.
- **Exact numbers.** Follower counts and viewer counts come back as integers
  (`11287668`), not `"11.2M"`.
- **Working image URLs.** Twitch serves preview images as templates containing
  `{width}x{height}`; those are filled in, so the URLs actually load.
- **Honest about limits.** Every run writes a summary row stating the 30-row
  ceiling and why a result is the size it is.

### The one limit you need to know

**Twitch caps anonymous access at 30 rows per query, and refuses pagination.**

Every cursor is rejected with `failed integrity check`. That was tested hard
before this actor was designed around it — cursors inlined and as typed
variables, from the first/middle/last edge, across five different connections,
with a real integrity token, with device and session headers, and via Apollo
persisted queries. It is structural, not a missing header.

So this actor does not promise deep crawls. **Coverage comes from widening the
query, and that genuinely works:**

- More channels or categories in `queries` — each is its own 30.
- **Clips: pick several time periods.** They barely overlap. Measured on one
  channel: last week (25 clips) + last month (30) = **54 unique clips**.
- **Live streams: pick several languages.** The same category in three
  languages is three separate result sets.

Anyone claiming unlimited anonymous Twitch pagination is either using the
official OAuth API or hasn't hit the ceiling yet.

### Input

```json
{
  "mode": "channel",
  "queries": ["shroud", "https://www.twitch.tv/kaicenat"],
  "maxItems": 30
}
```

| Field | Type | Description |
|---|---|---|
| `mode` | string | `channel`, `streams`, `games`, `clips`, `videos`, `search`. |
| `queries` | array | Channel names/URLs, category names, or search terms — depends on mode. Empty is valid for `streams` (global) and `games`. |
| `maxItems` | integer | Rows per query, max 30 (Twitch's ceiling). |
| `clipPeriods` | array | Clips mode: `LAST_DAY`, `LAST_WEEK`, `LAST_MONTH`, `ALL_TIME`. Several = more unique clips. |
| `clipSort` | string | `VIEWS_DESC` (dependable), `TRENDING`, `CREATED_AT_DESC`. |
| `broadcastType` | string | Videos mode: `ARCHIVE`, `HIGHLIGHT`, `UPLOAD`, `PAST_PREMIERE`. |
| `videoSort` | string | `TIME` or `VIEWS`. |
| `streamSort` | string | `VIEWER_COUNT`, `RECENT`, `RELEVANCE`. |
| `languages` | array | Live streams mode: restrict to broadcast languages (`EN`, `ES`, …). |
| `maxConcurrency` | integer | Requests in flight. Default `4`. |
| `minRequestInterval` | number | Seconds between request starts. Default `0.3`. |

### Output

Rows share one envelope, told apart by `recordType`: `CHANNEL`, `STREAM`,
`GAME`, `CLIP`, `VIDEO`, `QUERY_SUMMARY`, `ERROR`.

#### `CHANNEL`

```json
{
  "_input": "shroud",
  "recordType": "CHANNEL",
  "queryMode": "channel",
  "channelId": "37402112",
  "channelLogin": "shroud",
  "channelName": "shroud",
  "url": "https://www.twitch.tv/shroud",
  "description": "I'm back baby",
  "followers": 11287668,
  "isPartner": true,
  "createdAt": "2012-11-03T15:50:32Z",
  "primaryColorHex": "00ADFF",
  "avatarUrl": "https://static-cdn.jtvnw.net/jtv_user_pictures/...",
  "socialLinks": [{ "name": "discord", "title": "Discord", "url": "https://discord.gg/shroud" }],
  "isLive": false,
  "streamGameName": "WARDOGS",
  "lastBroadcastTitle": "stream stops when my bank account hits 0$"
}
```

When the channel is live it also carries `liveTitle`, `liveViewerCount`,
`liveStartedAt`, `liveGameName`, `liveTags` and `livePreviewImageUrl`.

#### `STREAM`

```json
{
  "recordType": "STREAM", "queryMode": "streams", "resultRank": 1,
  "streamId": "316782123862",
  "title": "LIVE: BLAST Premier Open Porto 2026 - MOUZ vs Falcons",
  "viewerCount": 56973,
  "startedAt": "2026-08-31T08:11:01Z",
  "channelLogin": "blastpremier",
  "channelFollowers": 1284410,
  "gameName": "Counter-Strike",
  "tags": ["Esports", "English", "CS2"],
  "previewImageUrl": "https://static-cdn.jtvnw.net/previews-ttv/live_user_blastpremier-1920x1080.jpg"
}
```

#### `CLIP`

```json
{
  "recordType": "CLIP", "resultRank": 1, "clipPeriod": "LAST_WEEK",
  "clipId": "1222162697",
  "title": "XDDD",
  "url": "https://www.twitch.tv/shroud/clip/TentativeTenuousNeanderthalPanicBasket-...",
  "viewCount": 153,
  "durationSeconds": 11,
  "createdAt": "2026-08-25T21:25:52Z",
  "curatorLogin": "bonus_00",
  "gameName": "WARDOGS"
}
```

| Field | Type | Description |
|---|---|---|
| `channelLogin` / `channelName` | string | Login (URL name) and display name. |
| `followers` | integer | Exact follower count. |
| `isLive` | boolean | Whether the channel is streaming right now. |
| `viewerCount` | integer | Live viewers. |
| `tags` | array | Stream tags. |
| `gameName` | string | Category. |
| `viewCount` / `durationSeconds` | integer | Clip and VOD metrics. |
| `previewImageUrl` / `thumbnailUrl` | string | Ready-to-use image URLs. |
| `notes` | array | Honesty flags on the summary row. |

#### `ERROR`

Every input produces at least one row, so a bad channel is visible in the data:

```json
{
  "_input": "zzz_no_such_user_xyz",
  "recordType": "ERROR",
  "_error": "not_found",
  "_errorDetail": "Twitch returned no channel for 'zzz_no_such_user_xyz'. The name may be misspelled, renamed, banned or deleted — this is a normal 200 with a null body, not a fetch failure."
}
```

### Known limits

- **30 rows per query, no pagination.** Explained above. Widen the query instead.
- **`TRENDING` clips are often empty**, and `CREATED_AT_DESC` currently answers a
  server error upstream. `VIEWS_DESC` is the dependable sort; the run says so
  when a sort returns nothing.
- **Empty VOD lists are normal.** Twitch auto-deletes past broadcasts after
  7–60 days depending on channel status.
- **No chat, no subscriber lists, no email addresses, no follower lists.** Those
  are either private or require an authenticated account.
- **Stream ordering is approximate.** Twitch layers its own recommendation
  weighting over the sort you pick.

# Actor input Schema

## `mode` (type: `string`):

Decides what your `queries` mean and which rows you get.

• **Channel** — channel names → profile, follower count, live status, social links
• **Live streams** — top live streams globally, or within a category if you name one
• **Top categories** — the most-watched games/categories right now
• **Clips** — channel names → their top clips
• **Videos (VODs)** — channel names → past broadcasts, highlights or uploads
• **Search** — search terms → matching channels and categories

## `queries` (type: `array`):

One entry per thing to scrape. Meaning depends on the mode:

• **Channel / Clips / Videos** — a channel name (`shroud`) or a `twitch.tv/<channel>` URL
• **Live streams** — a category name exactly as Twitch spells it (`Just Chatting`, `Grand Theft Auto V`), or a `twitch.tv/directory/category/...` URL. **Leave empty for the global top streams.**
• **Top categories** — leave empty; this mode takes no input
• **Search** — any search term

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

Rows per query. **Twitch's hard ceiling for anonymous access is 30** — it refuses every pagination cursor, so no tool can exceed this per single query. To collect more, add more entries to `queries`, or (in Clips mode) select several time periods. Every run reports this in its summary row.

## `clipPeriods` (type: `array`):

Clips mode only. Each period costs one request and returns up to 30 clips — and periods genuinely overlap very little, so selecting several is the way to get past 30. Measured on one channel: last week (25) + last month (30) gave 54 unique clips.

## `clipSort` (type: `string`):

Clips mode only. `Most viewed` is the dependable option. `Trending` is accepted by Twitch but often returns nothing even on channels with plenty of clips, and `Newest first` currently answers a server error upstream — both are offered for completeness and both are reported honestly in the run summary if they come back empty.

## `broadcastType` (type: `string`):

Videos mode only. Note that Twitch auto-deletes past broadcasts after 7–60 days depending on the channel's status, so an empty `Past broadcasts` result is normal rather than an error.

## `videoSort` (type: `string`):

Videos mode only: newest first, or most viewed first.

## `streamSort` (type: `string`):

Live streams mode only. Twitch applies its own recommendation weighting on top of this, so the returned order is close to — but not strictly — the sort you pick.

## `languages` (type: `array`):

Live streams mode only. Restrict to streams broadcast in these languages. Leave empty for all languages. Another useful way to widen coverage past the 30-row ceiling: the same category in three languages is three separate result sets.

## `maxConcurrency` (type: `integer`):

Upper bound on requests in flight at once. Keep it modest — Twitch throttles a hot IP by answering individual queries with a server error rather than a clean rate-limit status.

## `minRequestInterval` (type: `number`):

The honest speed control — it paces request starts without tying up a worker. Raise it if the log shows repeated server-error retries.

## `proxyConfiguration` (type: `object`):

**Required in practice.** Twitch gates this API by exit-IP reputation: a residential IP works, a datacenter IP is refused with `failed integrity check` on every request (verified on real cloud runs — a self-minted integrity token does not help). Use Apify Proxy with the RESIDENTIAL group. Without it, every row will be an `integrity_refused` error explaining this.

## Actor input object example

```json
{
  "mode": "channel",
  "queries": [
    "shroud"
  ],
  "maxItems": 30,
  "clipPeriods": [
    "LAST_WEEK"
  ],
  "clipSort": "VIEWS_DESC",
  "broadcastType": "ARCHIVE",
  "videoSort": "TIME",
  "streamSort": "VIEWER_COUNT",
  "languages": [],
  "maxConcurrency": 4,
  "minRequestInterval": 0.3,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "queries": [
        "shroud"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/twitch-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 = { "queries": ["shroud"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/twitch-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 '{
  "queries": [
    "shroud"
  ]
}' |
apify call scrapyx/twitch-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/twitch-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/7GQdNcz5pbpxJ7esf/builds/PfxlZLmoDiW115aKL/openapi.json
