Steam Reviews API — Reviews & Game Details avatar

Steam Reviews API — Reviews & Game Details

Pricing

from $0.18 / 1,000 review returneds

Go to Apify Store
Steam Reviews API — Reviews & Game Details

Steam Reviews API — Reviews & Game Details

Scrape Steam user reviews as data. One row per review: text, recommended or not, playtime, helpful and funny votes, author, dates. Plus a game row with price, genres, platforms, Metacritic and the review-score summary. By store URL, app ID or game name. No API key, no login.

Pricing

from $0.18 / 1,000 review returneds

Rating

0.0

(0)

Developer

Insight Solutions

Insight Solutions

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

13 hours ago

Last modified

Categories

Share

Get a Steam game's reviews as data. Give this Actor a list of Steam store links — or app IDs, or just game names — and get back one row per review: the text exactly as posted, whether it recommends the game, how long that person had played when they wrote it, how many people found it helpful or funny, and exact posted and edited timestamps. Each game also comes with a game row: price, developers, release date, genres, platforms, Metacritic, and Steam's own review-score summary.

No API key. No login. No cookies to paste. $0.30 per 1,000 reviews, proxy included, games with no reviews are free, and a run that returns nothing costs nothing at all.

Try it in 30 seconds

{
"apps": [
"https://store.steampowered.com/app/413150/Stardew_Valley/",
"1091500",
"Hollow Knight"
],
"maxReviewsPerApp": 100,
"filter": "recent"
}

A store URL, a bare app ID, and a game name — all three work. Names go through Steam's own store search and the row tells you what they resolved to.

What comes back

One review row per review:

{
"ok": true,
"rowType": "review", // "review" | "game" | "diagnostic"
"appId": "413150",
"appUrl": "https://store.steampowered.com/app/413150/",
"input": "https://store.steampowered.com/app/413150/Stardew_Valley/",
"resolvedFrom": null, // the search term, when the entry was a name
"name": "Stardew Valley",
"reviewId": "208409508",
"authorSteamId": "76561199363389036",
"authorProfileUrl": "https://steamcommunity.com/profiles/76561199363389036/",
"authorGamesOwned": 0, // 0 also means "profile is private"
"authorReviewsCount": 1, // the single best spam signal in the payload
"playtimeForeverMin": 9560, // minutes, total, right now
"playtimeAtReviewMin": 2733, // minutes when they wrote it
"playtimeLastTwoWeeksMin": 0,
"lastPlayedAt": "2026-07-28T16:58:08.000Z",
"language": "english",
"text": "Love this game, but please remember to go to bed before 2 either in the game or in real life.",
"recommended": true, // Steam's whole rating scale: thumb up or down
"votesUp": 477, // helpful votes
"votesFunny": 232, // a separate axis on Steam
"weightedVoteScore": 0.9679602980, // what the "helpful" order sorts by
"commentCount": 6,
"steamPurchase": true,
"receivedForFree": true,
"earlyAccess": false,
"steamDeck": false,
"createdAt": "2025-11-04T17:48:26.000Z", // an exact timestamp, not "3 months ago"
"updatedAt": "2025-11-04T17:48:26.000Z",
"developerResponse": null,
"developerRespondedAt": null,
"position": 1,
"filter": "helpful",
"error": null,
"errorType": null,
"scrapedAt": "2026-09-09T14:00:32.000Z",
"source": "store.steampowered.com",
"sourceUrl": "https://store.steampowered.com/app/413150/"
}

And one game row per app, written before its reviews:

{
"ok": true,
"rowType": "game",
"appId": "413150",
"name": "Stardew Valley",
"type": "game",
"isFree": false,
"price": 14.99, // decimal, after any discount
"currency": "USD", // follows the `country` input
"discountPercent": 0,
"developers": ["ConcernedApe"],
"publishers": ["ConcernedApe"],
"releaseDate": "2016-02-26", // ISO when Steam publishes a full date
"releaseDateText": "Feb 26, 2016", // the raw string, kept for "Q1 2026" cases
"comingSoon": false,
"genres": ["Indie", "RPG", "Simulation"],
"categories": ["Single-player", "Multi-player", "Co-op", "…"],
"metacriticScore": 89,
"recommendationsTotal": 893013,
"reviewScore": 9, // Steam's 0–9 band…
"reviewScoreDesc": "Overwhelmingly Positive",
"totalReviews": 466038, // …under this run's filters
"totalPositive": 460653,
"totalNegative": 5385,
"positivePercent": 98.8,
"shortDescription": "You've inherited your grandfather's old farm plot in Stardew Valley…",
"description": "Stardew Valley is an open-ended country-life RPG!…", // HTML converted to text
"headerImageUrl": "https://shared.akamai.steamstatic.com/…/413150/header.jpg",
"website": "http://www.stardewvalley.net",
"platforms": { "windows": true, "mac": true, "linux": true },
"requiredAge": 0,
"achievementsTotal": 49,
"dlcCount": 1
}

Every row carries every column, with null wherever it does not apply — so a CSV export, a SQL insert or a dataframe gets one stable shape.

Use cases

  • Player sentiment, with the receipts. One flat array of review text ready to hand to a model, weighted by votesUp and sliced by createdAt — a real timeline, not a rendered "3 months ago".
  • Did the patch land? createdAt plus recommended gives you a before-and-after around any date. earlyAccess separates people reviewing the game you shipped from people reviewing the one you were building.
  • Refund and churn signals. playtimeAtReviewMin against playtimeForeverMin says whether a negative review came from someone who bounced in an hour or someone with three hundred hours who finally had enough.
  • Competitor and market research. Feed a list of names, get price, genres, platforms, Metacritic and the score summary for all of them in one run, plus the reviews behind the score.
  • Store price tracking. includeGameDetails: true, maxReviewsPerApp: 1, a list of app IDs, and the country you sell in.
  • Spam and review-bomb detection. authorReviewsCount: 1, authorGamesOwned: 0, a burst of recent reviews in one language, and steamPurchase: false are all in the row already.

How it works, and why it keeps working

Steam has no public review API with a key. What it has are the three JSON endpoints the store front-end itself calls, and this Actor reads exactly those:

StepRequestWhat it gets
0api/storesearchAn app ID for a game name — only when the entry was a name
1api/appdetailsThe store record: price, developers, genres, platforms, Metacritic, release date
2appreviews/<id>?cursor=*Page one: up to 100 reviews and the review-score summary
3…appreviews/<id>?cursor=<next>100 more each time, until your cap, your budget, or the end of the feed

Two details in there are the ones cheap scrapers get wrong:

The summary only exists on page one. Page two answers with query_summary: { num_reviews: 100 } and nothing else. A walker that reads the totals off whatever page it happens to be holding reports zeros. This one fetches page one, keeps its summary, and attaches it to the game row — which is why the game row is written after the first review page, not before it.

The cursor is base64 and must be percent-encoded. Steam's cursors routinely contain /, + and =. Pasted into a URL raw, page two silently returns page one again, and the walk loops forever on the same hundred reviews. The end of a feed is a page with no reviews in it, or a cursor Steam has already handed out — both are checked.

Underneath: Apify datacenter proxy, one pinned session per parallel worker. Steam answers a burst from one address with HTTP 429 rather than a challenge, and when that happens the session is retired and the same page is asked for once more from a different exit. Retrying on an address that was just refused only deepens it, so it is never done. If the second exit is refused too, the walk stops, keeps every review it already delivered, and files one free blocked row saying where it stopped.

Pages of the same game are spaced 250–600 ms apart. Nothing forces that; it is the difference between reading a review feed and hammering one.

How it compares

  • No API key, no quota, no login. A store URL is the whole input.
  • Exact timestamps. Steam publishes real Unix timestamps for when a review was posted and last edited. Most review sources publish "3 months ago" and nothing behind it. createdAt and updatedAt here are the real thing, to the second.
  • Playtime is the column nobody else has. playtimeAtReviewMin is how long that person had actually played when they formed the opinion. It is the single most useful weight on a Steam review and it is in every row.
  • The game row is a first-class row, not a header. Price, genres, platforms, Metacritic and the score summary come back as data you can join, in the same dataset, with the same columns.
  • Names work. "Hollow Knight" resolves through Steam's own store search, and resolvedFrom tells you what it matched, so a wrong match is visible rather than silent.
  • Failures are free and legible. An unknown app ID, a game with no reviews in your language, a name the store cannot place or a rate-limited page produce a diagnostic row with an errorType you can branch on — and no charge. A run that returns nothing at all finishes FAILED with the reason in its status message, never a green run containing an apology.
  • A partial walk is kept, not thrown away. Hit maxRunSecs or your charge ceiling on page 40 and you keep pages 1–39.

Input reference

FieldTypeDefaultWhat it does
apps (required)array of stringsprefilled with one gameSteam store URLs, bare app IDs, or game names. Bundle and package URLs are rejected. Duplicates are read, and billed, once
maxReviewsPerAppinteger100Reviews per game. Steam serves at most 100 per page, so the walk stops on the page that reaches your number. 0 = every review the game has
filterrecent | helpful | updatedrecentSteam's own three orders. helpful is the ranking the store shows first, and the only one that honours dayRange
languagestringallSteam's own language name — english, russian, schinese, tchinese, german, japanese, koreana, brazilian … — or all
reviewTypeall | positive | negativeallThumbs-up only, thumbs-down only, or both
purchaseTypeall | steam | non_steam_purchaseallBought on Steam, activated from elsewhere, or both
dayRangeinteger0Only reviews from the last N days. Steam honours this on helpful only; 0 turns it off
includeGameDetailsbooleantrueReturn the game row. One extra request per game, billed as its own event
countrystringusTwo-letter store country. Decides price and currency, nothing else
maxConcurrencyinteger3Games in parallel. Each worker keeps its own proxy session. Pages within one game cannot be parallelised
maxRunSecsinteger240Whole-run wall-clock budget. When it runs out the Actor keeps what it has and files a free diagnostic row for each game it never reached
proxyConfigurationobjectApify datacenterLeave it alone unless you are pulling hundreds of thousands of reviews, in which case residential is the upgrade

Output reference

Every row carries the same keys. ok: true is a review or a game; ok: false is a free diagnostic row.

FieldWhat it is
rowTypereview, game or diagnostic
appId, appUrl, input, resolvedFrom, nameThe game, its store page, the entry you supplied, the search term that found it, and its title
reviewId, position, filterSteam's review ID, where it fell in the order this run walked, and which order that was
text, recommended, languageThe review exactly as posted, thumb up or down, and the language Steam filed it under
votesUp, votesFunny, weightedVoteScore, commentCountHelpful votes, funny votes, Steam's own helpfulness weight, and comments under the review
playtimeForeverMin, playtimeAtReviewMin, playtimeLastTwoWeeksMin, lastPlayedAtHow much they have played, how much they had played when they wrote it, recent activity, last launch
authorSteamId, authorProfileUrl, authorGamesOwned, authorReviewsCountWho wrote it, and the two numbers that place them
steamPurchase, receivedForFree, earlyAccess, steamDeckHow they got the game, whether they disclosed a free copy, whether the game was in Early Access, and whether they played on a Deck
createdAt, updatedAtExact ISO timestamps for posted and last edited
developerResponse, developerRespondedAtThe developer's public reply, when there is one
price, currency, discountPercent, isFree, typeGame rows: what it costs today in your country, and what kind of app it is
developers, publishers, releaseDate, releaseDateText, comingSoonGame rows: who made it and when it came out
genres, categories, platforms, requiredAge, achievementsTotal, dlcCountGame rows: how the store classifies and gates it
metacriticScore, recommendationsTotalGame rows: the two headline numbers from the store page
reviewScore, reviewScoreDesc, totalReviews, totalPositive, totalNegative, positivePercentGame rows: Steam's review-score summary, under this run's filters
shortDescription, description, headerImageUrl, websiteGame rows: the blurb, the long description as plain text, the capsule image and the official site
ok, error, errorTypeWhether this row is data, and if not, why not
scrapedAt, source, sourceUrlWhen, and from where

errorType on a diagnostic row is one of:

ValueMeaningCharged?
not-foundNo Steam game with that app ID, or a name the store search could not placeNo
no-reviewsThe game exists but has no reviews matching your filtersNo
blockedSteam refused our requests from two different exit IPs — usually a rate limit. Reviews already returned for that game are keptNo
invalid-inputThe entry was not a Steam game — a bundle URL, a non-Steam link, a bad IDNo
timeoutThe run's maxRunSecs budget ran out before this game was reachedNo
unavailableSteam answered with something unusableNo

Pricing

$0.30 per 1,000 reviews. Pay-per-event, with the proxy already inside that number — there is no separate proxy line on your bill for this Actor.

EventWhat triggers itFREEStarterScaleBusiness
Review returned (primary)One review row written to your dataset$0.0003$0.0003$0.00024$0.00018
Game details returnedOne game row — price, genres, platforms, score summary$0.001$0.001$0.001$0.001
Run startedOnce per run, after the first paid row$0.001$0.001$0.001$0.001

Worked example. 50 games at 100 reviews each with game details on, of which 2 app IDs do not exist:

  • 48 games × 100 reviews × $0.0003 = $1.44
  • 48 game rows × $0.001 = $0.048
  • 1 run start = $0.001
  • 2 unknown app IDs = $0.00
  • Total: $1.489

What you are never charged for: a game with no reviews, an unknown app ID, a name the store search could not place, an entry that was not a Steam game, a game the run never reached before maxRunSecs, or a page Steam rate-limited. If a whole run comes back empty it finishes FAILED and bills nothing at all, start fee included.

Set ACTOR_MAX_TOTAL_CHARGE_USD on a run and the Actor stops walking once the ceiling is in sight, rather than handing you rows it cannot bill or billing you for rows it cannot hand over. It finishes SUCCEEDED with the ceiling named in its status message, and everything already delivered is yours.

Limits, and the ones that might bite

The score summary follows your filters. Ask for language: "english" and totalReviews, totalPositive, reviewScore and positivePercent on the game row are all English-only. That is Steam's own behaviour on the endpoint, not a quirk of this Actor, and it is usually what you want — but it is why the number can differ from the one on the store page. recommendationsTotal comes from the store record instead and is unfiltered.

maxReviewsPerApp: 0 on a big game is a big job. A major title's feed runs into millions of reviews, which is tens of thousands of sequential page requests with a pause between each — days of wall clock and a four-figure bill. maxRunSecs and ACTOR_MAX_TOTAL_CHARGE_USD are the two brakes. Set them.

Steam's review order is Steam's. helpful is a weighted ranking that changes as people vote, so the same request on two days returns overlapping but not identical sets. recent is stable in the sense that matters: new reviews arrive at the front.

Reviews you can only see signed in. Reviews Steam has hidden as off-topic review-bombing are excluded from the default feed, as they are on the store page. This Actor does not log in, does not accept cookies and does not take a session token, and it never will — that is a deliberate line, not a missing feature.

Review text keeps Steam's markup. People write [h1], [spoiler] and [b] in reviews and Steam stores it verbatim. So does this Actor. Stripping it would be guessing at what the author meant; the raw text is what a model or a reader wants.

A private profile reports zero. authorGamesOwned: 0 overwhelmingly means "this profile is private", not "this person owns no games". Steam publishes no flag for the difference.

Names resolve to the first store hit. "Stardew Valley" finds the game; a vague or misspelled name can find a soundtrack, a DLC or something unrelated. resolvedFrom and name on every row make that visible, and an app ID or store URL removes the ambiguity entirely.

Steam may change the format. These are the store front-end's own JSON endpoints, undocumented and changeable without notice — that is true of every tool that reads Steam reviews, including the ones that do not say so. When a shape changes, rows stop arriving and you get free blocked or unavailable diagnostic rows rather than quietly wrong data, and a run that returns nothing bills nothing.

Rate and reliability. Requests go out through Apify datacenter proxy with per-worker sessions, one rotation per refusal, and a 250–600 ms pause between pages of the same game. Three games in parallel is the default because it is where throughput and Steam's rate limiting balance; raising maxConcurrency speeds a long list up and makes 429s more likely.

Use it from an AI agent, or from code

One JSON object in, one flat array out — the shape agent runtimes want. The Actor runs with limited permissions, uses pay-per-event pricing and never enters Standby, so it works over the Apify MCP server and with x402 agentic payments. The Integrations tab pushes results to Slack, a webhook, Zapier, Make, Google Sheets, Snowflake or BigQuery.

curl -X POST "https://api.apify.com/v2/acts/insight.solutions~steam-reviews-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"apps":["413150"],"maxReviewsPerApp":50,"filter":"helpful","language":"english"}'
# pip install apify-client
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/steam-reviews-api").call(run_input={
"apps": ["https://store.steampowered.com/app/413150/Stardew_Valley/"],
"maxReviewsPerApp": 200,
"filter": "helpful",
"language": "english",
"includeGameDetails": True,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if not row.get("ok"):
print("skipped:", row["input"], row["errorType"])
elif row["rowType"] == "game":
print(f'{row["name"]}{row["reviewScoreDesc"]} ({row["positivePercent"]}% of {row["totalReviews"]})')
else:
thumb = "+" if row["recommended"] else "-"
print(f'{thumb} {row["playtimeAtReviewMin"] // 60}h: {row["text"][:80]}')

FAQ

How do I get the reviews the store page shows first? "filter": "helpful". That is Steam's own helpfulness ranking — the endpoint spells it all, which is why passing that word straight through would quietly give you the recent feed instead. Add "dayRange": 30 to rank only the last month.

How many reviews can I get from one game? As many as it has. Set maxReviewsPerApp: 0 and raise maxRunSecs. A game with 100,000 reviews is 1,000 sequential page requests with a pause between each — plan for tens of minutes, and bound it with ACTOR_MAX_TOTAL_CHARGE_USD if you are not sure what you are asking for.

Why does the game row's totalReviews differ from the store page? Because it follows your language, reviewType and purchaseType filters. Run with the defaults (all everywhere) and it matches the store's own "all reviews" count.

Are game rows charged? Yes, at $0.001 each — one extra request and the largest parse in the run. Set includeGameDetails: false and they are neither fetched nor billed.

Can I pass a game name instead of an ID? Yes. It costs one store-search request, and resolvedFrom on every row records the term that found the game so a wrong match is visible.

Do I need my own proxy or an API key? Neither. Apify datacenter proxy is configured by default and its cost is inside the per-review price. Steam has no public review API key to get.

Will it read a bundle, a package or a whole publisher's catalogue? Not yet — this Actor takes games. Feed it a list of app IDs or names from wherever your list comes from.

What happens if one game fails? The others still run. The failed one produces a free diagnostic row and the run finishes SUCCEEDED. If every game fails, the run finishes FAILED and you are billed nothing at all.

Is the data fresh? Live. Every run reads Steam at that moment; nothing is cached.

  • Public store pages only. Every source is a public Steam store endpoint. The Actor never logs in, never accepts cookies or session tokens, never takes an API key belonging to anyone else, and never touches private profiles, friends lists, libraries or purchase history.
  • Reviews are personal data in most jurisdictions. A review carries a public SteamID, a profile link and playtime, and under the GDPR and similar laws that is personal data about an identifiable person. You are the controller of whatever you collect: have a lawful basis, keep only what you need, honour deletion requests, and remember that a review deleted on Steam stays in your dataset until you remove it.
  • Reviews are their authors' words. Republishing them, or training on them, is your call and your responsibility, subject to Valve's terms and to the law where you operate. Aggregation, sentiment analysis and quotation are the ordinary uses and are what this is built for.
  • Not affiliated with Valve Corporation, Steam, or with any developer, publisher or reviewer whose content you retrieve. All product names and trademarks belong to their respective owners and are used only to describe which public endpoints this Actor reads.

Our other Actors

Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.

Video, audio & social

News, documents & the web

Business, finance & jobs

Apps & games