# Steam Reviews Scraper (`scrapyx/steam-reviews-scraper`) Actor

User reviews for any Steam game, with the playtime, purchase source and vote counts Steam attaches to each. Never stops on a short page - Steam serves 99-row pages mid-stream, and a walker that treats one as the end returns 1,499 of 5,236,483 reviews and reports success.

- **URL**: https://apify.com/scrapyx/steam-reviews-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** E-commerce, Games, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.26 / 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

## Steam Reviews Scraper

Every user review for any Steam game, with the playtime, purchase source,
language and vote counts Steam attaches to each one. Straight from
`store.steampowered.com/appreviews` — no login, no API key, no browser.

### The thing that makes this different: it doesn't stop early

Steam serves **short pages in the middle of the stream**. Walking Counter-Strike 2
with 100 reviews per request:

```
page 15 -> 99 reviews, and ALL 99 WERE NEW
page 29 -> 99 reviews, and ALL 99 WERE NEW
... the stream kept going well past page 30
```

The obvious stop condition — *"fewer rows came back than I asked for, so that
was the last page"* — is wrong here, and it fails **silently and
catastrophically**: a scraper using it stops at page 15 with **1,499 reviews of
the 5,236,483 the same response says exist**, and reports success. The first
draft of this actor's own recon walker did exactly that.

This actor never stops on a short page. It counts them (`shortPagesSeen`) and
keeps going, stopping only on an empty page, a cursor that stops advancing, a
page with no new IDs, or your own `maxReviewsPerApp`. Which one ended the run
is reported in `stopReason`.

### Steam's own default hides 46.7% of the reviews

`purchase_type` defaults to `steam` upstream, which excludes keys activated
outside Steam. Measured on CS2 in one minute:

| purchase\_type | total\_reviews |
| --- | ---: |
| *(omitted — Steam's default)* | 5,236,483 |
| `steam` | 5,236,483 |
| `non_steam_purchase` | 4,580,072 |
| **`all`** | **9,816,519** |

So "all the reviews for CS2" is 53.3% of them unless you say otherwise, and
nothing in the response says so. **This actor defaults to `all`**, and every
summary also reports `steamDefaultPurchaseTypeTotal` so the gap is a number in
your data rather than a footnote.

(The parts don't sum to the whole either: 5,236,483 + 4,580,072 = 9,816,555
against `all`'s 9,816,519 — upstream's arithmetic disagrees with itself by 36.)

### Other traps it handles

- **`query_summary` is complete on the first page only.** Later pages return
  `{num_reviews}` and nothing else, so totals read off page 5 are null.
  Captured once, from page one, and kept.
- **An unknown filter value doesn't error — it silently answers a different
  question.** `filter=bogus` returned the *same cursor as `filter=all`*, byte
  for byte; `review_type=bogus` returned `all`'s totals; `purchase_type=bogus`
  fell back to `steam`. Every enum is validated here before the request goes out.
- **Filtering to positive or negative destroys the totals.**
  `review_type=positive|negative` makes upstream stop reporting
  `total_reviews`, `total_positive` and `total_negative` entirely. The rows
  still come; the denominator doesn't. Reported as
  `totalsUnavailableReason` rather than printed as a zero.
- **An omitted language means English, not everything** — 1,498,594 of CS2's
  5,236,483. Defaults to `all` here. An unknown language honestly returns zero.
- **Off-topic ("review bombing") reviews are excluded by default**, and on most
  games you can't tell: Cyberpunk 2077 measured 878,232 with the filter on
  against 884,470 with it off, while CS2, Valheim and Overwatch 2 were
  identical. Turn on `reportOfftopicGap` to get the hidden count as a number.
- **`day_range` is deliberately not offered.** It filters the rows but leaves
  `total_reviews` at the unfiltered figure, so a run reads as "5.2M reviews in
  the last 30 days". The explicit `dateFrom`/`dateTo` window moves both.
- **HTTP 200 carries no information.** Every outcome is a 200; the verdict is
  in the body's `success` field. `success: 2` means Steam couldn't parse the
  request (App ID `0` or non-numeric). And a **nonexistent App ID is
  indistinguishable from a real game with no reviews yet** — both return
  `success: 1` with `total_reviews: 0`.

### Output

One dataset, `recordType` tells the rows apart:

- `SEARCH_SUMMARY` — one per game: upstream's totals and score, pages fetched,
  short pages survived, stop reason, reachable fraction, what Steam's default
  would have said, and the off-topic gap.
- `REVIEW` — one per review, with upstream's object passed through verbatim
  alongside normalised fields. Playtime is kept as **two** figures —
  `playtimeAtReviewMinutes` and `playtimeForeverMinutes` — because "how long
  had they played when they wrote this" and "how long have they played" are
  different questions, and collapsing them loses the only signal that says
  whether a review came after 2 hours or 2,000.
- `ERROR` — one per input that failed, so every input maps to at least one row.

Every row carries the filter context it was produced under (`purchaseTypeApplied`,
`languageApplied`, `filterApplied`, …) — without it a row isn't interpretable,
since the same game yields a different corpus per setting.

### Modes

- **`reviews`** — walk the full stream for each game.
- **`summary`** — one request per game for score and totals only. A cheap way
  to compare hundreds of games' ratings without pulling their reviews.

### Anti-bot and limits

**No WAF.** Seven TLS profiles (chrome124, chrome131, chrome136, safari17\_0,
edge101, firefox133, chrome99\_android) returned 200 cold with byte-identical
17,977-byte bodies. A proxy is optional and **off by default**.

`num_per_page` above 100 is silently clamped by Steam with no error, so it is
capped in code. A 5,000-page backstop per game (~500k reviews) stops an
uncapped run on a 9.8-million-review title from paging forever; it is reported
as `pageCapHit` if reached.

### Policy

`store.steampowered.com/robots.txt` places no restriction on `/appreviews/`
and names no AI crawler. Note that **`api.steampowered.com` is `Disallow: /`**
— the Web API host is closed, which is worth knowing before anyone reaches for
`GetAppList` to enumerate App IDs. This actor does not touch it.

Steam reviews are user-submitted content; check Valve's terms before
redistributing them.

# Actor input Schema

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

reviews = walk the full review stream for each game. summary = one request per game for its score and totals only — a cheap way to compare hundreds of games.

## `appIds` (type: `array`):

A numeric App ID (730) or a store URL (https://store.steampowered.com/app/730/Counter\_Strike\_2/). Must be a positive integer — Steam answers 0 and non-numeric values with an in-body error rather than an HTTP one.

## `purchaseType` (type: `string`):

IMPORTANT: Steam's own default is 'steam', which hides reviews from keys activated outside Steam — measured 5,236,483 of 9,816,519 on CS2, so 46.7% of them. This actor defaults to 'all' instead, and every summary reports what Steam's default would have returned.

## `language` (type: `string`):

A Steam language code (english, french, schinese, russian…) or 'all'. Note Steam treats an omitted or empty language as ENGLISH, not as everything — 1,498,594 of CS2's 5,236,483 reviews. Defaults to 'all' here. An unknown language honestly returns zero results.

## `filter` (type: `string`):

recent = newest first. updated = recently edited first. all = Steam's own relevance order. funny = funniest first. An unknown value is refused here, because Steam silently falls back to 'all' — a different ordering — and still answers 200.

## `reviewType` (type: `string`):

Beware: choosing positive or negative makes Steam stop reporting totals entirely, so the summary's totals become null and the run has no denominator. Leave on 'all' if you need the counts.

## `excludeOfftopicActivity` (type: `boolean`):

Steam's own default. On most games this changes nothing at all; on titles that were actually review-bombed it does — Cyberpunk 2077 measured 878,232 with it on against 884,470 with it off, so 6,238 reviews hidden.

## `reportOfftopicGap` (type: `boolean`):

Spends one extra request per game to fetch the opposite setting's total, so the summary can report offtopicReviewsHidden as a number instead of leaving you to guess. Usually 0.

## `dateFrom` (type: `string`):

Must be given together with dateTo — Steam applies the window only when both bounds are present. Unlike Steam's day\_range parameter (deliberately not offered here), an explicit window moves the reported totals as well as the rows.

## `dateTo` (type: `string`):

End of the window. Required whenever the start is set.

## `dateRangeType` (type: `string`):

include = only reviews inside the window. exclude = everything outside it.

## `maxReviewsPerApp` (type: `integer`):

Set 0 for unlimited. Popular games have millions — CS2 reports 9,816,519 — so leaving this at 0 is a long run. The walker never stops early on a short page, which is what makes an unlimited run actually complete.

## `pageSize` (type: `integer`):

Between 1 and 100. Steam silently clamps anything above 100 with no error.

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

Games in flight at once. Each game's own pages are sequential because the cursor is a chain.

## `minRequestInterval` (type: `integer`):

Politeness pacing shared across all workers. 0 uses the built-in default.

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

Optional and OFF by default. The endpoint is public and keyless and showed no WAF — all seven TLS profiles returned byte-identical responses.

## Actor input object example

```json
{
  "mode": "reviews",
  "appIds": [
    "730",
    "https://store.steampowered.com/app/1091500/Cyberpunk_2077/"
  ],
  "purchaseType": "all",
  "language": "all",
  "filter": "recent",
  "reviewType": "all",
  "excludeOfftopicActivity": true,
  "reportOfftopicGap": false,
  "dateFrom": "2026-01-01",
  "dateTo": "2026-08-01",
  "dateRangeType": "include",
  "maxReviewsPerApp": 500,
  "pageSize": 100,
  "maxConcurrency": 3,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

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

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

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

```

## MCP server setup

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