@pencil-agent/nano-pencil 2.0.0-beta.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -267
- package/dist/build-meta.json +3 -3
- package/dist/core/export-html/AGENT.md +11 -11
- package/dist/core/export-html/template.css +971 -971
- package/dist/core/export-html/template.html +54 -54
- package/dist/core/extensions-host/index.d.ts +1 -1
- package/dist/core/extensions-host/types.d.ts +5 -8
- package/dist/extensions/builtin/AGENT.md +115 -115
- package/dist/extensions/builtin/browser/AGENT.md +17 -17
- package/dist/extensions/builtin/browser/agent-workspace/agent_helpers.py +12 -12
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/amazon/product-search.md +198 -198
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/archive-org/scraping.md +341 -341
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv/scraping.md +311 -311
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/arxiv-bulk/scraping.md +333 -333
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/atlas/overview.md +70 -70
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/booking-com/scraping.md +578 -578
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/capterra/scraping.md +440 -440
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/centilebrain/generate-estimates.md +110 -110
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coingecko/scraping.md +325 -325
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coinmarketcap/scraping.md +463 -463
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/coursera/scraping.md +360 -360
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/craigslist/scraping.md +390 -390
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/crossref/scraping.md +568 -568
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/dev-to/scraping.md +323 -323
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/duckduckgo/scraping.md +349 -349
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/ebay/scraping.md +435 -435
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/etsy/scraping.md +506 -506
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/eventbrite/scraping.md +363 -363
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/expedia/automation.md +168 -168
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/groups.md +236 -236
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/facebook/pages.md +295 -295
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/framer/editor.md +108 -108
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/fred/scraping.md +493 -493
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/g2/scraping.md +580 -580
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/genius/scraping.md +511 -511
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/repo-actions.md +65 -65
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/github/scraping.md +184 -184
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/glassdoor/scraping.md +543 -543
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gmail/compose.md +122 -122
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/goodreads/scraping.md +461 -461
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/gutenberg/scraping.md +383 -383
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/hackernews/scraping.md +243 -243
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/howlongtobeat/scraping.md +473 -473
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/imdb/scraping.md +271 -271
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/itch-io/scraping.md +436 -436
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/job-boards/indeed-glassdoor.md +1021 -1021
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/letterboxd/scraping.md +349 -349
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/linkedin/invitation-manager.md +109 -109
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/loom/folder-enumeration.md +170 -170
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/macrotrends/scraping.md +537 -537
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/article-hydration.md +120 -120
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/medium/scraping.md +414 -414
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/metacritic/scraping.md +477 -477
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/musicbrainz/scraping.md +478 -478
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/nasa/scraping.md +339 -339
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/news-aggregation/multi-source.md +205 -205
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/open-library/scraping.md +472 -472
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openalex/scraping.md +470 -470
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/openstreetmap/scraping.md +490 -490
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/package-registries/npm-pypi.md +478 -478
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/polymarket/scraping.md +234 -234
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/producthunt/scraping.md +307 -307
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/pubmed/scraping.md +421 -421
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/quora/scraping.md +364 -364
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rawg/scraping.md +352 -352
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/reddit/scraping.md +124 -124
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/rest-countries/scraping.md +233 -233
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/sec-edgar/scraping.md +361 -361
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/README.md +36 -36
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/embedded-apps.md +72 -72
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/knowledge-base.md +109 -109
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/shopify-admin/polaris-inputs.md +137 -137
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/soundcloud/scraping.md +362 -362
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/spotify/scraping.md +339 -339
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/stackoverflow/scraping.md +435 -435
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/steam/scraping.md +575 -575
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/substack/scraping.md +338 -338
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/thetechgeeks/pricing.md +52 -52
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tiktok/upload.md +107 -107
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/tradingview/scraping.md +309 -309
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trello/boards-and-lists.md +88 -88
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/trustpilot/scraping.md +375 -375
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/walmart/scraping.md +444 -444
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wayback-machine/scraping.md +306 -306
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/weather/scraping.md +398 -398
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/wellfound/scraping.md +596 -596
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/world-bank/scraping.md +356 -356
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/xiaohongshu/scraping.md +84 -84
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/youtube/scraping.md +418 -418
- package/dist/extensions/builtin/browser/agent-workspace/domain-skills/zillow/scraping.md +433 -433
- package/dist/extensions/builtin/browser/browser.md +73 -73
- package/dist/extensions/builtin/browser/install.md +142 -142
- package/dist/extensions/builtin/browser/interaction-skills/connection.md +48 -48
- package/dist/extensions/builtin/browser/interaction-skills/cookies.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/cross-origin-iframes.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/dialogs.md +64 -64
- package/dist/extensions/builtin/browser/interaction-skills/downloads.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/drag-and-drop.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/dropdowns.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/iframes.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/network-requests.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/print-as-pdf.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/profile-sync.md +90 -90
- package/dist/extensions/builtin/browser/interaction-skills/screenshots.md +17 -17
- package/dist/extensions/builtin/browser/interaction-skills/scrolling.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/shadow-dom.md +3 -3
- package/dist/extensions/builtin/browser/interaction-skills/tabs.md +69 -69
- package/dist/extensions/builtin/browser/interaction-skills/uploads.md +1 -1
- package/dist/extensions/builtin/browser/interaction-skills/viewport.md +3 -3
- package/dist/extensions/builtin/browser/src/browser_harness/AGENT.md +15 -15
- package/dist/extensions/builtin/browser/src/browser_harness/__init__.py +8 -8
- package/dist/extensions/builtin/browser/src/browser_harness/_ipc.py +90 -90
- package/dist/extensions/builtin/browser/src/browser_harness/admin.py +722 -722
- package/dist/extensions/builtin/browser/src/browser_harness/daemon.py +328 -328
- package/dist/extensions/builtin/browser/src/browser_harness/helpers.py +396 -396
- package/dist/extensions/builtin/browser/src/browser_harness/run.py +103 -103
- package/dist/extensions/builtin/discipline/skills/brainstorming/SKILL.md +33 -33
- package/dist/extensions/builtin/discipline/skills/executing-plans/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/finishing-development-branch/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/receiving-code-review/SKILL.md +22 -22
- package/dist/extensions/builtin/discipline/skills/requesting-code-review/SKILL.md +31 -31
- package/dist/extensions/builtin/discipline/skills/systematic-debugging/SKILL.md +28 -28
- package/dist/extensions/builtin/discipline/skills/test-driven-development/SKILL.md +32 -32
- package/dist/extensions/builtin/discipline/skills/using-git-worktrees/SKILL.md +25 -25
- package/dist/extensions/builtin/discipline/skills/verification-before-completion/SKILL.md +27 -27
- package/dist/extensions/builtin/discipline/skills/writing-plans/SKILL.md +26 -26
- package/dist/extensions/builtin/goal/README.md +67 -67
- package/dist/extensions/builtin/goal/goal-controller.js +1 -1
- package/dist/extensions/builtin/goal/goal-prompts.js +4 -4
- package/dist/extensions/builtin/grub/README.md +112 -112
- package/dist/extensions/builtin/link-world/agent-workspace/README.md +16 -16
- package/dist/extensions/builtin/link-world/internet-search/internet-search.md +65 -65
- package/dist/extensions/builtin/link-world/link-world-agent.md +82 -82
- package/dist/extensions/builtin/link-world/linkworld.md +313 -313
- package/dist/extensions/builtin/link-world/network-routing/network-routing.md +67 -67
- package/dist/extensions/builtin/loop/README.md +92 -92
- package/dist/extensions/builtin/mcp/figma-design.md +68 -68
- package/dist/extensions/builtin/mcp/mcp-management.md +85 -85
- package/dist/extensions/builtin/recap/AGENT.md +15 -15
- package/dist/extensions/builtin/sal/README.md +72 -72
- package/dist/extensions/builtin/security-audit/README.md +289 -289
- package/dist/extensions/builtin/team/AGENT.md +112 -112
- package/dist/extensions/builtin/team/TESTING.md +299 -299
- package/dist/extensions/builtin/token-save/README.md +56 -56
- package/dist/extensions/optional/AGENT.md +10 -10
- package/dist/index.d.ts +5 -30
- package/dist/index.js +1 -1
- package/dist/models.d.ts +7 -0
- package/dist/models.js +1 -0
- package/dist/modes/interactive/theme/dark.json +85 -85
- package/dist/modes/interactive/theme/light.json +84 -84
- package/dist/modes/interactive/theme/theme-schema.json +335 -335
- package/dist/modes/interactive/theme/warm.json +81 -81
- package/dist/node_modules/@pencil-agent/ai/dist/cli.js +0 -0
- package/dist/packages/protocol/src/flags.d.ts +20 -0
- package/dist/packages/protocol/src/flags.js +0 -0
- package/dist/packages/protocol/src/hooks.d.ts +17 -0
- package/dist/packages/protocol/src/hooks.js +0 -0
- package/dist/packages/protocol/src/index.d.ts +4 -2
- package/dist/packages/protocol/src/index.js +1 -1
- package/dist/packages/protocol/src/lifecycle.d.ts +11 -21
- package/dist/public-config.d.ts +12 -0
- package/dist/public-config.js +1 -0
- package/dist/runtime.d.ts +9 -0
- package/dist/runtime.js +1 -0
- package/dist/session-compaction.d.ts +7 -0
- package/dist/session-compaction.js +1 -0
- package/dist/session.d.ts +7 -0
- package/dist/session.js +1 -0
- package/dist/skills.d.ts +7 -0
- package/dist/skills.js +1 -0
- package/dist/tools.d.ts +7 -0
- package/dist/tools.js +1 -0
- package/docs/ACP/345/215/217/350/256/256/351/233/206/346/210/220/345/274/200/345/217/221/346/226/207/346/241/243.md +851 -0
- package/docs/SDK-TESTING.md +364 -0
- package/docs/codex-goal-command-impl.md +1055 -1055
- package/docs/codex-goal-vs-grub.md +500 -500
- package/docs/custom-provider.md +27 -27
- package/docs/extensions.md +27 -27
- package/docs/keybindings.md +27 -27
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +250 -250
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +122 -122
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +1222 -1222
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/256/236/347/216/260/346/212/245/345/221/212.md" +158 -158
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/257/271/346/257/224/345/210/206/346/236/220.md" +128 -128
- package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +320 -320
- package/docs/loop-usage-examples.md +214 -214
- package/docs/mem-core/346/212/200/346/234/257/346/226/207/346/241/243.md +593 -0
- package/docs/models.md +27 -27
- package/docs/packages.md +27 -27
- package/docs/pi-design-philosophy.md +457 -457
- package/docs/planmode.md +1987 -1987
- package/docs/prompt-templates.md +27 -27
- package/docs/providers.md +27 -27
- package/docs/sdk.md +27 -27
- package/docs/skills.md +27 -27
- package/docs/startup-performance-optimization.md +301 -0
- package/docs/themes.md +27 -27
- package/docs/tui.md +27 -27
- package/docs//350/256/244/347/237/245/345/234/260/345/233/276.md +47 -0
- package/package.json +190 -162
- package/docs/cc-agent-design.md +0 -1297
- package/docs/cc-tui-design.md +0 -1333
- package/docs/nanoPencil-/345/255/246/344/271/240/350/256/241/345/210/222.md +0 -170
- package/docs/scan-report.md +0 -3820
- package/docs//345/257/271/346/240/207Claude-Code.md +0 -1775
- package/docs//351/230/277/351/207/214/345/267/264/345/267/264/350/264/242/346/212/245/345/210/206/346/236/220/344/271/246.md +0 -261
|
@@ -1,575 +1,575 @@
|
|
|
1
|
-
# Steam — Scraping & Data Extraction
|
|
2
|
-
|
|
3
|
-
Field-tested against store.steampowered.com on 2026-04-18. All code blocks validated with live requests.
|
|
4
|
-
|
|
5
|
-
## Fastest approach: App Details API (no auth, no browser)
|
|
6
|
-
|
|
7
|
-
The `appdetails` endpoint is the primary source for all game data. No API key, no cookies, no auth required. Returns clean JSON for any appid.
|
|
8
|
-
|
|
9
|
-
```python
|
|
10
|
-
import json
|
|
11
|
-
from helpers import http_get
|
|
12
|
-
|
|
13
|
-
def get_app(appid, cc="US"):
|
|
14
|
-
"""
|
|
15
|
-
Fetch full game/DLC/software data by Steam appid.
|
|
16
|
-
cc = ISO-3166 country code for correct regional pricing (default: US).
|
|
17
|
-
Returns None if appid not found or no longer on Steam.
|
|
18
|
-
"""
|
|
19
|
-
resp = http_get(
|
|
20
|
-
f"https://store.steampowered.com/api/appdetails?appids={appid}&cc={cc}"
|
|
21
|
-
)
|
|
22
|
-
data = json.loads(resp)
|
|
23
|
-
entry = data[str(appid)]
|
|
24
|
-
if not entry["success"]:
|
|
25
|
-
return None
|
|
26
|
-
return entry["data"]
|
|
27
|
-
|
|
28
|
-
game = get_app(292030) # The Witcher 3
|
|
29
|
-
# game["name"] -> "The Witcher 3: Wild Hunt"
|
|
30
|
-
# game["steam_appid"] -> 292030
|
|
31
|
-
# game["type"] -> "game" | "dlc" | "demo" | "advertising" | "mod" | "video"
|
|
32
|
-
# game["required_age"] -> 18 (int, 0 if no restriction)
|
|
33
|
-
# game["is_free"] -> False
|
|
34
|
-
# game["short_description"] -> plain-text one-liner
|
|
35
|
-
# game["about_the_game"] -> HTML
|
|
36
|
-
# game["detailed_description"]-> HTML
|
|
37
|
-
# game["website"] -> "https://www.thewitcher.com/witcher3"
|
|
38
|
-
# game["header_image"] -> URL to 460x215px header image
|
|
39
|
-
# game["capsule_image"] -> URL to smaller capsule image
|
|
40
|
-
# game["background"] -> URL to store page background
|
|
41
|
-
# game["supported_languages"]-> HTML string with language list (use html.unescape())
|
|
42
|
-
# game["developers"] -> ["CD PROJEKT RED"]
|
|
43
|
-
# game["publishers"] -> ["CD PROJEKT RED"]
|
|
44
|
-
# game["platforms"] -> {"windows": True, "mac": False, "linux": False}
|
|
45
|
-
# game["metacritic"] -> {"score": 93, "url": "https://www.metacritic.com/..."}
|
|
46
|
-
# game["genres"] -> [{"id": "3", "description": "RPG"}]
|
|
47
|
-
# game["categories"] -> [{"id": 2, "description": "Single-player"}, ...]
|
|
48
|
-
# game["release_date"] -> {"coming_soon": False, "date": "May 18, 2015"}
|
|
49
|
-
# game["dlc"] -> [355880, 378649, ...] (list of DLC appids)
|
|
50
|
-
# game["legal_notice"] -> copyright text
|
|
51
|
-
# game["ratings"] -> per-region rating board data (ESRB, PEGI, USK, ...)
|
|
52
|
-
# game["content_descriptors"]-> {"ids": [1, 5], "notes": "..."}
|
|
53
|
-
# game["recommendations"] -> {"total": 812249}
|
|
54
|
-
# game["achievements"] -> {"total": 78, "highlighted": [...]}
|
|
55
|
-
# game["support_info"] -> {"url": "...", "email": "..."}
|
|
56
|
-
# game["pc_requirements"] -> {"minimum": "<html>...", "recommended": "<html>..."}
|
|
57
|
-
# game["mac_requirements"] -> same structure or []
|
|
58
|
-
# game["linux_requirements"] -> same structure or []
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Price overview
|
|
64
|
-
|
|
65
|
-
Prices are always in **cents** (integer). Use `final_formatted` for display.
|
|
66
|
-
|
|
67
|
-
```python
|
|
68
|
-
game = get_app(292030)
|
|
69
|
-
po = game.get("price_overview")
|
|
70
|
-
# po is None for free-to-play games (is_free=True)
|
|
71
|
-
|
|
72
|
-
if po:
|
|
73
|
-
print(po["currency"]) # "USD"
|
|
74
|
-
print(po["final"]) # 3999 (cents — $39.99)
|
|
75
|
-
print(po["initial"]) # 3999 (original price in cents)
|
|
76
|
-
print(po["discount_percent"]) # 0 (0–100)
|
|
77
|
-
print(po["final_formatted"]) # "$39.99" (always present, ready to display)
|
|
78
|
-
print(po["initial_formatted"]) # "" (EMPTY when not discounted!)
|
|
79
|
-
# "$49.99" (only set when discount_percent > 0)
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
**Critical**: `initial_formatted` is an empty string when `discount_percent == 0`.
|
|
83
|
-
Always use `final_formatted` for displaying current price.
|
|
84
|
-
|
|
85
|
-
```python
|
|
86
|
-
def price_display(game):
|
|
87
|
-
"""Returns (current_price_str, original_price_str_or_None, discount_pct)."""
|
|
88
|
-
if game.get("is_free"):
|
|
89
|
-
return ("Free", None, 0)
|
|
90
|
-
po = game.get("price_overview")
|
|
91
|
-
if not po:
|
|
92
|
-
return ("N/A", None, 0)
|
|
93
|
-
disc = po["discount_percent"]
|
|
94
|
-
orig = po["initial_formatted"] if disc > 0 else None
|
|
95
|
-
return (po["final_formatted"], orig, disc)
|
|
96
|
-
|
|
97
|
-
# Witcher3: ("$39.99", None, 0)
|
|
98
|
-
# Discounted game: ("$24.99", "$49.99", 50)
|
|
99
|
-
# Dota2: ("Free", None, 0)
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Regional pricing
|
|
103
|
-
|
|
104
|
-
Pass `cc=` (ISO-3166 country code) to get local currency:
|
|
105
|
-
|
|
106
|
-
```python
|
|
107
|
-
get_app(292030, cc="GB")["price_overview"]
|
|
108
|
-
# {"currency": "GBP", "initial": 2499, "final": 2499, ..., "final_formatted": "£24.99"}
|
|
109
|
-
|
|
110
|
-
get_app(292030, cc="DE")["price_overview"]
|
|
111
|
-
# {"currency": "EUR", "initial": 2999, "final": 2999, ..., "final_formatted": "29,99€"}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## Bulk / concurrent fetching
|
|
117
|
-
|
|
118
|
-
10 games in 0.54s with 5 workers — no rate-limit errors observed:
|
|
119
|
-
|
|
120
|
-
```python
|
|
121
|
-
import json
|
|
122
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
123
|
-
from helpers import http_get
|
|
124
|
-
|
|
125
|
-
def fetch_game(appid, cc="US"):
|
|
126
|
-
resp = http_get(
|
|
127
|
-
f"https://store.steampowered.com/api/appdetails?appids={appid}&cc={cc}"
|
|
128
|
-
)
|
|
129
|
-
data = json.loads(resp)
|
|
130
|
-
entry = data[str(appid)]
|
|
131
|
-
return entry["data"] if entry["success"] else None
|
|
132
|
-
|
|
133
|
-
appids = [292030, 570, 413150, 427520, 730, 550, 220, 400, 218620, 105600]
|
|
134
|
-
|
|
135
|
-
with ThreadPoolExecutor(max_workers=5) as ex:
|
|
136
|
-
games = list(ex.map(fetch_game, appids))
|
|
137
|
-
# Completed in ~0.54s
|
|
138
|
-
# games[i] is None if appid not found
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
**Confirmed field values for common appids:**
|
|
142
|
-
- `570` (Dota 2): `is_free=True`, `price_overview=None`, `required_age=0`
|
|
143
|
-
- `292030` (Witcher 3): `is_free=False`, `required_age=18`, `metacritic.score=93`
|
|
144
|
-
- `413150` (Stardew Valley): `is_free=False`, `required_age=0`, `metacritic=None`
|
|
145
|
-
- `427520` (Factorio): `is_free=False`, `required_age=0`
|
|
146
|
-
|
|
147
|
-
---
|
|
148
|
-
|
|
149
|
-
## Partial field fetch (filters=)
|
|
150
|
-
|
|
151
|
-
Fetch only specific fields to reduce payload size:
|
|
152
|
-
|
|
153
|
-
```python
|
|
154
|
-
# Price only (tiny response)
|
|
155
|
-
resp = http_get("https://store.steampowered.com/api/appdetails?appids=292030&filters=price_overview")
|
|
156
|
-
data = json.loads(resp)["292030"]["data"]
|
|
157
|
-
# data keys: ["price_overview"]
|
|
158
|
-
|
|
159
|
-
# Basic metadata (no price, no media)
|
|
160
|
-
resp = http_get("https://store.steampowered.com/api/appdetails?appids=292030&filters=basic")
|
|
161
|
-
# data keys: about_the_game, capsule_image, capsule_imagev5, detailed_description, dlc,
|
|
162
|
-
# header_image, is_free, legal_notice, linux_requirements, mac_requirements,
|
|
163
|
-
# name, pc_requirements, required_age, reviews, short_description,
|
|
164
|
-
# steam_appid, supported_languages, type, website
|
|
165
|
-
|
|
166
|
-
# Multiple filters comma-separated
|
|
167
|
-
resp = http_get("https://store.steampowered.com/api/appdetails?appids=292030&filters=screenshots,price_overview")
|
|
168
|
-
# data keys: ["price_overview", "screenshots"]
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
---
|
|
172
|
-
|
|
173
|
-
## Media fields
|
|
174
|
-
|
|
175
|
-
### Screenshots
|
|
176
|
-
|
|
177
|
-
```python
|
|
178
|
-
game = get_app(292030)
|
|
179
|
-
for ss in game["screenshots"]: # 18 screenshots for Witcher 3
|
|
180
|
-
print(ss["id"]) # 0, 1, 2, ...
|
|
181
|
-
print(ss["path_thumbnail"]) # 600x338 JPEG URL
|
|
182
|
-
print(ss["path_full"]) # 1920x1080 JPEG URL
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### Movies / trailers
|
|
186
|
-
|
|
187
|
-
```python
|
|
188
|
-
for m in game["movies"]: # 4 trailers for Witcher 3
|
|
189
|
-
print(m["id"]) # integer
|
|
190
|
-
print(m["name"]) # trailer title
|
|
191
|
-
print(m["thumbnail"]) # thumbnail URL
|
|
192
|
-
print(m["highlight"]) # bool — main trailer flag
|
|
193
|
-
# m["webm"] -> None (old format, mostly absent)
|
|
194
|
-
# m["mp4"] -> None (old format, mostly absent)
|
|
195
|
-
# m["dash_av1"] -> dash_av1 stream URL (present on modern entries)
|
|
196
|
-
# m["dash_h264"] -> dash_h264 stream URL
|
|
197
|
-
# m["hls_h264"] -> HLS stream URL
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
## Ratings and content descriptors
|
|
203
|
-
|
|
204
|
-
The `ratings` dict contains per-region rating board data for mature games:
|
|
205
|
-
|
|
206
|
-
```python
|
|
207
|
-
game = get_app(292030)
|
|
208
|
-
|
|
209
|
-
# ESRB (North America)
|
|
210
|
-
esrb = game["ratings"].get("esrb", {})
|
|
211
|
-
esrb["rating"] # "m" (lowercase) -> M for Mature
|
|
212
|
-
esrb["descriptors"] # "Blood and Gore\r\nIntense Violence\r\nNudity\r\n..."
|
|
213
|
-
esrb["use_age_gate"] # "true" (string, not bool)
|
|
214
|
-
esrb["required_age"] # "17" (string, not int)
|
|
215
|
-
|
|
216
|
-
# PEGI (Europe)
|
|
217
|
-
pegi = game["ratings"].get("pegi", {})
|
|
218
|
-
pegi["rating"] # "18"
|
|
219
|
-
pegi["descriptors"] # "Violence\r\nBad language"
|
|
220
|
-
|
|
221
|
-
# USK (Germany)
|
|
222
|
-
usk = game["ratings"].get("usk", {})
|
|
223
|
-
usk["rating"] # "18"
|
|
224
|
-
|
|
225
|
-
# steam_germany (Germany digital-only classification)
|
|
226
|
-
sg = game["ratings"].get("steam_germany", {})
|
|
227
|
-
sg["rating"] # "16"
|
|
228
|
-
sg["banned"] # "0" (1 = banned in Germany)
|
|
229
|
-
|
|
230
|
-
# igrs (Indonesia)
|
|
231
|
-
igrs = game["ratings"].get("igrs", {})
|
|
232
|
-
igrs["rating"] # "BANNED" if banned there
|
|
233
|
-
igrs["banned"] # "1"
|
|
234
|
-
|
|
235
|
-
# Other keys: oflc, nzoflc, kgrb, dejus, mda, fpb, csrr, crl
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
Content descriptor IDs (from `content_descriptors.ids`):
|
|
239
|
-
- `1` = Some Nudity or Sexual Content
|
|
240
|
-
- `5` = Frequent Violence or Gore
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
## Age-gated store pages
|
|
245
|
-
|
|
246
|
-
**The `appdetails` API completely bypasses age gates.** It returns full data for any game regardless of rating or age restriction — no cookies needed.
|
|
247
|
-
|
|
248
|
-
The **store webpage** (`store.steampowered.com/app/{appid}/`) redirects mature games to an age verification form:
|
|
249
|
-
|
|
250
|
-
```
|
|
251
|
-
GET https://store.steampowered.com/app/292030/
|
|
252
|
-
-> 302 -> https://store.steampowered.com/agecheck/app/292030/
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
To bypass the age gate on the store page, send the `birthtime` cookie:
|
|
256
|
-
|
|
257
|
-
```python
|
|
258
|
-
import urllib.request
|
|
259
|
-
|
|
260
|
-
def get_store_page(appid):
|
|
261
|
-
"""Fetch game store HTML page, bypassing age gate."""
|
|
262
|
-
req = urllib.request.Request(
|
|
263
|
-
f"https://store.steampowered.com/app/{appid}/",
|
|
264
|
-
headers={
|
|
265
|
-
"User-Agent": "Mozilla/5.0",
|
|
266
|
-
"Cookie": "birthtime=631152001; lastagecheckage=1-January-1990"
|
|
267
|
-
}
|
|
268
|
-
)
|
|
269
|
-
with urllib.request.urlopen(req, timeout=15) as r:
|
|
270
|
-
html = r.read().decode("utf-8", errors="replace")
|
|
271
|
-
if "agecheck" in r.url:
|
|
272
|
-
return None # Age gate not bypassed
|
|
273
|
-
return html
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
`birthtime=631152001` = January 1, 1990 in Unix time. Steam accepts any date before the current year minus the required age.
|
|
277
|
-
|
|
278
|
-
---
|
|
279
|
-
|
|
280
|
-
## Search
|
|
281
|
-
|
|
282
|
-
### storesearch API (title search, up to 10 results)
|
|
283
|
-
|
|
284
|
-
```python
|
|
285
|
-
import json, urllib.parse
|
|
286
|
-
from helpers import http_get
|
|
287
|
-
|
|
288
|
-
def search_games(term, cc="US", lang="english"):
|
|
289
|
-
"""
|
|
290
|
-
Returns up to 10 matching apps/DLC/bundles.
|
|
291
|
-
No pagination — always exactly 10 results max.
|
|
292
|
-
"""
|
|
293
|
-
q = urllib.parse.quote(term)
|
|
294
|
-
resp = http_get(
|
|
295
|
-
f"https://store.steampowered.com/api/storesearch/?term={q}&l={lang}&cc={cc}"
|
|
296
|
-
)
|
|
297
|
-
data = json.loads(resp)
|
|
298
|
-
return data["items"]
|
|
299
|
-
|
|
300
|
-
results = search_games("witcher")
|
|
301
|
-
# [
|
|
302
|
-
# {"type": "app", "name": "The Witcher 3: Wild Hunt", "id": 292030,
|
|
303
|
-
# "price": {"currency": "USD", "initial": 3999, "final": 3999},
|
|
304
|
-
# "tiny_image": "https://...", "metascore": "93",
|
|
305
|
-
# "platforms": {"windows": True, "mac": False, "linux": False},
|
|
306
|
-
# "streamingvideo": False},
|
|
307
|
-
# {"type": "sub", ...}, # bundles have type="sub"
|
|
308
|
-
# ...
|
|
309
|
-
# ]
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
**Search result fields:**
|
|
313
|
-
- `id` — appid (or subid for bundles)
|
|
314
|
-
- `type` — `"app"` | `"sub"` (bundle)
|
|
315
|
-
- `name` — game title
|
|
316
|
-
- `price` — `{"currency": "USD", "initial": cents, "final": cents}` — `None` for F2P
|
|
317
|
-
- `metascore` — string e.g. `"93"`, `"0"` if no score
|
|
318
|
-
- `platforms` — `{"windows": bool, "mac": bool, "linux": bool}`
|
|
319
|
-
- `tiny_image` — 231x87px capsule image URL
|
|
320
|
-
- `streamingvideo` — bool
|
|
321
|
-
|
|
322
|
-
**Note:** `price` in search results has only `initial` and `final` — no `discount_percent` or `formatted` strings. Use `appdetails` for full pricing.
|
|
323
|
-
|
|
324
|
-
---
|
|
325
|
-
|
|
326
|
-
## Review scores and user reviews
|
|
327
|
-
|
|
328
|
-
```python
|
|
329
|
-
import json, urllib.parse
|
|
330
|
-
from helpers import http_get
|
|
331
|
-
|
|
332
|
-
def get_reviews(appid, num=10, language="english", filter="recent",
|
|
333
|
-
review_type="all", purchase_type="all", cursor="*"):
|
|
334
|
-
"""
|
|
335
|
-
filter: "recent" | "updated" | "all"
|
|
336
|
-
review_type: "all" | "positive" | "negative"
|
|
337
|
-
purchase_type: "all" | "steam" | "non_steam_purchase"
|
|
338
|
-
language: "english" | "all" | ISO code
|
|
339
|
-
cursor: use returned cursor for next page (URL-encode it)
|
|
340
|
-
"""
|
|
341
|
-
encoded_cursor = urllib.parse.quote(cursor)
|
|
342
|
-
resp = http_get(
|
|
343
|
-
f"https://store.steampowered.com/appreviews/{appid}"
|
|
344
|
-
f"?json=1&num_per_page={num}&language={language}"
|
|
345
|
-
f"&filter={filter}&review_type={review_type}"
|
|
346
|
-
f"&purchase_type={purchase_type}&cursor={encoded_cursor}"
|
|
347
|
-
)
|
|
348
|
-
return json.loads(resp)
|
|
349
|
-
|
|
350
|
-
result = get_reviews(292030, num=5, language="english")
|
|
351
|
-
|
|
352
|
-
# result["success"] -> 1 (int, not bool)
|
|
353
|
-
# result["cursor"] -> "AoJ4rq..." (base64, URL-encode for next page)
|
|
354
|
-
# result["query_summary"]["review_score"] -> 9 (0–9 score)
|
|
355
|
-
# result["query_summary"]["review_score_desc"] -> "Overwhelmingly Positive"
|
|
356
|
-
# result["query_summary"]["total_positive"] -> 226883
|
|
357
|
-
# result["query_summary"]["total_negative"] -> 7499
|
|
358
|
-
# result["query_summary"]["total_reviews"] -> 234382 (steam purchase only)
|
|
359
|
-
# result["reviews"] -> list of review objects
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
**Review score descriptions (review_score int to string):**
|
|
363
|
-
|
|
364
|
-
| Score | Description |
|
|
365
|
-
|-------|-------------|
|
|
366
|
-
| 9 | Overwhelmingly Positive |
|
|
367
|
-
| 8 | Very Positive |
|
|
368
|
-
| 7 | Mostly Positive |
|
|
369
|
-
| 6 | Positive (Mixed) |
|
|
370
|
-
| 5 | Mixed |
|
|
371
|
-
| 4 | Mostly Negative |
|
|
372
|
-
| 3 | Negative |
|
|
373
|
-
| 2 | Mostly Negative |
|
|
374
|
-
| 1 | Overwhelmingly Negative |
|
|
375
|
-
| 0 | No reviews |
|
|
376
|
-
|
|
377
|
-
**Confirmed scores:** Witcher 3 = 9, Counter-Strike 2 = 8, Stardew Valley = 9, Factorio = 9.
|
|
378
|
-
|
|
379
|
-
### Review object fields
|
|
380
|
-
|
|
381
|
-
```python
|
|
382
|
-
review = result["reviews"][0]
|
|
383
|
-
review["recommendationid"] # "221423937" — unique review ID
|
|
384
|
-
review["voted_up"] # True/False — positive/negative
|
|
385
|
-
review["votes_up"] # 213 — helpful votes
|
|
386
|
-
review["votes_funny"] # 66
|
|
387
|
-
review["weighted_vote_score"] # 0.8405... — Steam's helpfulness score
|
|
388
|
-
review["comment_count"] # 20
|
|
389
|
-
review["steam_purchase"] # True
|
|
390
|
-
review["received_for_free"] # False
|
|
391
|
-
review["written_during_early_access"]# False
|
|
392
|
-
review["timestamp_created"] # 1774209092 (Unix timestamp)
|
|
393
|
-
review["timestamp_updated"] # Unix timestamp
|
|
394
|
-
review["language"] # "english"
|
|
395
|
-
review["review"] # review text
|
|
396
|
-
review["app_release_date"] # Unix timestamp of game release
|
|
397
|
-
|
|
398
|
-
review["author"]["steamid"] # "76561198..."
|
|
399
|
-
review["author"]["personaname"] # display name
|
|
400
|
-
review["author"]["num_games_owned"] # 1039
|
|
401
|
-
review["author"]["num_reviews"] # 180
|
|
402
|
-
review["author"]["playtime_forever"] # 1146 (minutes total)
|
|
403
|
-
review["author"]["playtime_last_two_weeks"] # minutes in last 2 weeks
|
|
404
|
-
review["author"]["playtime_at_review"] # minutes at time of review
|
|
405
|
-
review["author"]["last_played"] # Unix timestamp
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
### Cursor-based pagination
|
|
409
|
-
|
|
410
|
-
```python
|
|
411
|
-
import urllib.parse, json
|
|
412
|
-
from helpers import http_get
|
|
413
|
-
|
|
414
|
-
def get_all_reviews(appid, max_pages=5, num_per_page=100, language="all"):
|
|
415
|
-
"""Paginate through reviews using cursor."""
|
|
416
|
-
cursor = "*"
|
|
417
|
-
all_reviews = []
|
|
418
|
-
for _ in range(max_pages):
|
|
419
|
-
resp = http_get(
|
|
420
|
-
f"https://store.steampowered.com/appreviews/{appid}"
|
|
421
|
-
f"?json=1&num_per_page={num_per_page}&language={language}"
|
|
422
|
-
f"&filter=recent&cursor={urllib.parse.quote(cursor)}"
|
|
423
|
-
)
|
|
424
|
-
data = json.loads(resp)
|
|
425
|
-
batch = data.get("reviews", [])
|
|
426
|
-
if not batch:
|
|
427
|
-
break
|
|
428
|
-
all_reviews.extend(batch)
|
|
429
|
-
cursor = data.get("cursor", "")
|
|
430
|
-
if not cursor:
|
|
431
|
-
break
|
|
432
|
-
return all_reviews
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
---
|
|
436
|
-
|
|
437
|
-
## Featured games
|
|
438
|
-
|
|
439
|
-
### Featured items (rotating store front)
|
|
440
|
-
|
|
441
|
-
```python
|
|
442
|
-
import json
|
|
443
|
-
from helpers import http_get
|
|
444
|
-
|
|
445
|
-
data = json.loads(http_get("https://store.steampowered.com/api/featured/"))
|
|
446
|
-
# data["large_capsules"] -> 1-3 hero banner items
|
|
447
|
-
# data["featured_win"] -> 10 featured items for Windows
|
|
448
|
-
# data["featured_mac"] -> macOS featured
|
|
449
|
-
# data["featured_linux"] -> Linux featured
|
|
450
|
-
# data["status"] -> 1
|
|
451
|
-
|
|
452
|
-
item = data["featured_win"][0]
|
|
453
|
-
# item["id"] -> appid
|
|
454
|
-
# item["name"] -> game title
|
|
455
|
-
# item["discounted"] -> bool
|
|
456
|
-
# item["discount_percent"] -> 0-100
|
|
457
|
-
# item["original_price"] -> cents
|
|
458
|
-
# item["final_price"] -> cents
|
|
459
|
-
# item["currency"] -> "USD"
|
|
460
|
-
# item["windows_available"] -> bool
|
|
461
|
-
# item["mac_available"] -> bool
|
|
462
|
-
# item["linux_available"] -> bool
|
|
463
|
-
# item["large_capsule_image"] -> URL
|
|
464
|
-
# item["small_capsule_image"] -> URL
|
|
465
|
-
# item["header_image"] -> URL
|
|
466
|
-
# item["controller_support"] -> "full" | "partial" | ""
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
### Featured categories (top sellers, specials, new releases, coming soon)
|
|
470
|
-
|
|
471
|
-
```python
|
|
472
|
-
data = json.loads(http_get("https://store.steampowered.com/api/featuredcategories/"))
|
|
473
|
-
|
|
474
|
-
# Named sections (most useful):
|
|
475
|
-
specials = data["specials"]["items"] # 10 on-sale games
|
|
476
|
-
top_sellers = data["top_sellers"]["items"] # 10 top sellers
|
|
477
|
-
new_releases= data["new_releases"]["items"] # 30 new releases
|
|
478
|
-
coming_soon = data["coming_soon"]["items"] # 10 upcoming games
|
|
479
|
-
|
|
480
|
-
# Numbered keys "0" through "7" are spotlight banners (deals/events)
|
|
481
|
-
|
|
482
|
-
item = top_sellers[0]
|
|
483
|
-
# item["id"] -> appid
|
|
484
|
-
# item["name"] -> game title
|
|
485
|
-
# item["discounted"] -> bool
|
|
486
|
-
# item["discount_percent"] -> 0-100
|
|
487
|
-
# item["original_price"] -> cents (None for upcoming games)
|
|
488
|
-
# item["final_price"] -> cents (0 for upcoming)
|
|
489
|
-
# item["currency"] -> "USD"
|
|
490
|
-
# item["discount_expiration"] -> Unix timestamp (present for active sales)
|
|
491
|
-
# item["windows_available"] -> bool
|
|
492
|
-
# item["mac_available"] -> bool
|
|
493
|
-
# item["linux_available"] -> bool
|
|
494
|
-
# item["header_image"] -> URL
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
---
|
|
498
|
-
|
|
499
|
-
## App list (all Steam apps)
|
|
500
|
-
|
|
501
|
-
The `ISteamApps/GetAppList` API endpoint (v1, v2, v0001, v0002) currently returns **HTTP 404** from `api.steampowered.com` as of 2026-04-18. The endpoint is effectively retired without a Steamworks API key.
|
|
502
|
-
|
|
503
|
-
**Workaround:** Use the featured categories and search APIs to discover appids, then batch-fetch via `appdetails`.
|
|
504
|
-
|
|
505
|
-
```python
|
|
506
|
-
# Discover appids from top sellers + new releases
|
|
507
|
-
import json
|
|
508
|
-
from helpers import http_get
|
|
509
|
-
|
|
510
|
-
def get_all_store_appids():
|
|
511
|
-
data = json.loads(http_get("https://store.steampowered.com/api/featuredcategories/"))
|
|
512
|
-
appids = set()
|
|
513
|
-
for key in ["specials", "top_sellers", "new_releases", "coming_soon"]:
|
|
514
|
-
for item in data.get(key, {}).get("items", []):
|
|
515
|
-
appids.add(item["id"])
|
|
516
|
-
for key in ["featured_win", "featured_mac", "featured_linux"]:
|
|
517
|
-
for item in data.get(key, []):
|
|
518
|
-
appids.add(item["id"])
|
|
519
|
-
return sorted(appids)
|
|
520
|
-
|
|
521
|
-
# Returns ~50 store-front appids (enough to seed further discovery)
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
---
|
|
525
|
-
|
|
526
|
-
## Rate limits
|
|
527
|
-
|
|
528
|
-
Steam's public APIs are generous. Confirmed during testing:
|
|
529
|
-
|
|
530
|
-
- **10 sequential requests in 1.59s** — all HTTP 200, no throttling
|
|
531
|
-
- **10 concurrent requests (5 workers) in 0.54s** — all succeeded
|
|
532
|
-
- **No `Retry-After` header** observed at any concurrency level
|
|
533
|
-
|
|
534
|
-
Practical limits (undocumented, inferred from community reports):
|
|
535
|
-
- ~200 requests/5 minutes per IP to `appdetails` before soft throttling (returns `success: false`)
|
|
536
|
-
- Review API is more restrictive — keep to ~50 requests/minute
|
|
537
|
-
|
|
538
|
-
---
|
|
539
|
-
|
|
540
|
-
## Gotchas
|
|
541
|
-
|
|
542
|
-
**`success: false` with no data field** — When an appid is invalid, removed, or unreleased, the response is `{"999999": {"success": false}}` with no `data` key. Always check `entry["success"]` before accessing `entry["data"]`.
|
|
543
|
-
|
|
544
|
-
```python
|
|
545
|
-
entry = json.loads(resp)[str(appid)]
|
|
546
|
-
if not entry["success"]:
|
|
547
|
-
return None # game removed or never existed
|
|
548
|
-
game = entry["data"]
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
**Multiple appids in one call — not supported** — `appids=292030,570` returns HTTP 400. The API only accepts a single appid per call. Use `ThreadPoolExecutor` for bulk fetching.
|
|
552
|
-
|
|
553
|
-
**`price_overview` is `None` for free games** — When `is_free=True`, the `price_overview` key is absent or `None`. Never index `game["price_overview"]["final"]` without a None check.
|
|
554
|
-
|
|
555
|
-
**`initial_formatted` is empty string when not on sale** — When `discount_percent == 0`, `initial_formatted` is `""`. Only `final_formatted` is reliably present and non-empty. Use `final_formatted` for display in all cases.
|
|
556
|
-
|
|
557
|
-
**Store page age gate** — `store.steampowered.com/app/{appid}/` redirects mature games to `/agecheck/app/{appid}/`. The `appdetails` API completely bypasses this — no cookies needed. For browser-based scraping of the store page, send `Cookie: birthtime=631152001; lastagecheckage=1-January-1990`.
|
|
558
|
-
|
|
559
|
-
**`storesearch` always returns ≤ 10 results** — No pagination. `total` in the response is always 10, not the true result count. For finding specific games, this is sufficient. For catalog browsing, use `appdetails` with known appids.
|
|
560
|
-
|
|
561
|
-
**`metascore` is string `"0"` in search results, int `93` in appdetails** — Inconsistent types. In `storesearch` results, `metascore` is a string (e.g. `"93"`, `"0"`). In `appdetails`, `metacritic` is a dict `{"score": 93, "url": "..."}` or absent entirely. Always `int()` the storesearch value.
|
|
562
|
-
|
|
563
|
-
**`appdetails` returns `type: "dlc"` for DLC** — Check `game["type"]` before treating every appid as a standalone game. Type values: `"game"`, `"dlc"`, `"demo"`, `"advertising"`, `"mod"`, `"video"`.
|
|
564
|
-
|
|
565
|
-
**`ratings` dict uses string booleans** — `use_age_gate` and `required_age` inside `ratings[board]` are strings (`"true"`, `"17"`), not native types. `banned` is also a string `"0"` or `"1"`.
|
|
566
|
-
|
|
567
|
-
**`ISteamApps/GetAppList` is dead** — HTTP 404 for v1, v2, v0001, v0002 endpoints as of 2026-04-18. Use store front APIs and search to discover appids instead.
|
|
568
|
-
|
|
569
|
-
**`supported_languages` is HTML** — The field contains escaped HTML like `English<strong>*</strong>, French`. Starred languages have full audio. Use `html.unescape()` and strip tags to get a clean list.
|
|
570
|
-
|
|
571
|
-
**`release_date.date` is a locale string, not ISO** — Value is `"May 18, 2015"` not `"2015-05-18"`. Parse with `datetime.strptime(d, "%B %d, %Y")` or use regex.
|
|
572
|
-
|
|
573
|
-
**Review `purchase_type` changes total counts** — `purchase_type=all` includes reviews from non-Steam purchases (physical, Humble, etc.). `purchase_type=steam` is Steam-only. Witcher 3 example: `all`=802,072 reviews, `steam`=234,385.
|
|
574
|
-
|
|
575
|
-
**Currency requires `cc=` param** — Without `cc=`, you get USD by default. Pass `cc=GB` for GBP, `cc=DE` for EUR, etc. Country codes are ISO-3166 (2-letter, uppercase).
|
|
1
|
+
# Steam — Scraping & Data Extraction
|
|
2
|
+
|
|
3
|
+
Field-tested against store.steampowered.com on 2026-04-18. All code blocks validated with live requests.
|
|
4
|
+
|
|
5
|
+
## Fastest approach: App Details API (no auth, no browser)
|
|
6
|
+
|
|
7
|
+
The `appdetails` endpoint is the primary source for all game data. No API key, no cookies, no auth required. Returns clean JSON for any appid.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
import json
|
|
11
|
+
from helpers import http_get
|
|
12
|
+
|
|
13
|
+
def get_app(appid, cc="US"):
|
|
14
|
+
"""
|
|
15
|
+
Fetch full game/DLC/software data by Steam appid.
|
|
16
|
+
cc = ISO-3166 country code for correct regional pricing (default: US).
|
|
17
|
+
Returns None if appid not found or no longer on Steam.
|
|
18
|
+
"""
|
|
19
|
+
resp = http_get(
|
|
20
|
+
f"https://store.steampowered.com/api/appdetails?appids={appid}&cc={cc}"
|
|
21
|
+
)
|
|
22
|
+
data = json.loads(resp)
|
|
23
|
+
entry = data[str(appid)]
|
|
24
|
+
if not entry["success"]:
|
|
25
|
+
return None
|
|
26
|
+
return entry["data"]
|
|
27
|
+
|
|
28
|
+
game = get_app(292030) # The Witcher 3
|
|
29
|
+
# game["name"] -> "The Witcher 3: Wild Hunt"
|
|
30
|
+
# game["steam_appid"] -> 292030
|
|
31
|
+
# game["type"] -> "game" | "dlc" | "demo" | "advertising" | "mod" | "video"
|
|
32
|
+
# game["required_age"] -> 18 (int, 0 if no restriction)
|
|
33
|
+
# game["is_free"] -> False
|
|
34
|
+
# game["short_description"] -> plain-text one-liner
|
|
35
|
+
# game["about_the_game"] -> HTML
|
|
36
|
+
# game["detailed_description"]-> HTML
|
|
37
|
+
# game["website"] -> "https://www.thewitcher.com/witcher3"
|
|
38
|
+
# game["header_image"] -> URL to 460x215px header image
|
|
39
|
+
# game["capsule_image"] -> URL to smaller capsule image
|
|
40
|
+
# game["background"] -> URL to store page background
|
|
41
|
+
# game["supported_languages"]-> HTML string with language list (use html.unescape())
|
|
42
|
+
# game["developers"] -> ["CD PROJEKT RED"]
|
|
43
|
+
# game["publishers"] -> ["CD PROJEKT RED"]
|
|
44
|
+
# game["platforms"] -> {"windows": True, "mac": False, "linux": False}
|
|
45
|
+
# game["metacritic"] -> {"score": 93, "url": "https://www.metacritic.com/..."}
|
|
46
|
+
# game["genres"] -> [{"id": "3", "description": "RPG"}]
|
|
47
|
+
# game["categories"] -> [{"id": 2, "description": "Single-player"}, ...]
|
|
48
|
+
# game["release_date"] -> {"coming_soon": False, "date": "May 18, 2015"}
|
|
49
|
+
# game["dlc"] -> [355880, 378649, ...] (list of DLC appids)
|
|
50
|
+
# game["legal_notice"] -> copyright text
|
|
51
|
+
# game["ratings"] -> per-region rating board data (ESRB, PEGI, USK, ...)
|
|
52
|
+
# game["content_descriptors"]-> {"ids": [1, 5], "notes": "..."}
|
|
53
|
+
# game["recommendations"] -> {"total": 812249}
|
|
54
|
+
# game["achievements"] -> {"total": 78, "highlighted": [...]}
|
|
55
|
+
# game["support_info"] -> {"url": "...", "email": "..."}
|
|
56
|
+
# game["pc_requirements"] -> {"minimum": "<html>...", "recommended": "<html>..."}
|
|
57
|
+
# game["mac_requirements"] -> same structure or []
|
|
58
|
+
# game["linux_requirements"] -> same structure or []
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Price overview
|
|
64
|
+
|
|
65
|
+
Prices are always in **cents** (integer). Use `final_formatted` for display.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
game = get_app(292030)
|
|
69
|
+
po = game.get("price_overview")
|
|
70
|
+
# po is None for free-to-play games (is_free=True)
|
|
71
|
+
|
|
72
|
+
if po:
|
|
73
|
+
print(po["currency"]) # "USD"
|
|
74
|
+
print(po["final"]) # 3999 (cents — $39.99)
|
|
75
|
+
print(po["initial"]) # 3999 (original price in cents)
|
|
76
|
+
print(po["discount_percent"]) # 0 (0–100)
|
|
77
|
+
print(po["final_formatted"]) # "$39.99" (always present, ready to display)
|
|
78
|
+
print(po["initial_formatted"]) # "" (EMPTY when not discounted!)
|
|
79
|
+
# "$49.99" (only set when discount_percent > 0)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Critical**: `initial_formatted` is an empty string when `discount_percent == 0`.
|
|
83
|
+
Always use `final_formatted` for displaying current price.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
def price_display(game):
|
|
87
|
+
"""Returns (current_price_str, original_price_str_or_None, discount_pct)."""
|
|
88
|
+
if game.get("is_free"):
|
|
89
|
+
return ("Free", None, 0)
|
|
90
|
+
po = game.get("price_overview")
|
|
91
|
+
if not po:
|
|
92
|
+
return ("N/A", None, 0)
|
|
93
|
+
disc = po["discount_percent"]
|
|
94
|
+
orig = po["initial_formatted"] if disc > 0 else None
|
|
95
|
+
return (po["final_formatted"], orig, disc)
|
|
96
|
+
|
|
97
|
+
# Witcher3: ("$39.99", None, 0)
|
|
98
|
+
# Discounted game: ("$24.99", "$49.99", 50)
|
|
99
|
+
# Dota2: ("Free", None, 0)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Regional pricing
|
|
103
|
+
|
|
104
|
+
Pass `cc=` (ISO-3166 country code) to get local currency:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
get_app(292030, cc="GB")["price_overview"]
|
|
108
|
+
# {"currency": "GBP", "initial": 2499, "final": 2499, ..., "final_formatted": "£24.99"}
|
|
109
|
+
|
|
110
|
+
get_app(292030, cc="DE")["price_overview"]
|
|
111
|
+
# {"currency": "EUR", "initial": 2999, "final": 2999, ..., "final_formatted": "29,99€"}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Bulk / concurrent fetching
|
|
117
|
+
|
|
118
|
+
10 games in 0.54s with 5 workers — no rate-limit errors observed:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import json
|
|
122
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
123
|
+
from helpers import http_get
|
|
124
|
+
|
|
125
|
+
def fetch_game(appid, cc="US"):
|
|
126
|
+
resp = http_get(
|
|
127
|
+
f"https://store.steampowered.com/api/appdetails?appids={appid}&cc={cc}"
|
|
128
|
+
)
|
|
129
|
+
data = json.loads(resp)
|
|
130
|
+
entry = data[str(appid)]
|
|
131
|
+
return entry["data"] if entry["success"] else None
|
|
132
|
+
|
|
133
|
+
appids = [292030, 570, 413150, 427520, 730, 550, 220, 400, 218620, 105600]
|
|
134
|
+
|
|
135
|
+
with ThreadPoolExecutor(max_workers=5) as ex:
|
|
136
|
+
games = list(ex.map(fetch_game, appids))
|
|
137
|
+
# Completed in ~0.54s
|
|
138
|
+
# games[i] is None if appid not found
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Confirmed field values for common appids:**
|
|
142
|
+
- `570` (Dota 2): `is_free=True`, `price_overview=None`, `required_age=0`
|
|
143
|
+
- `292030` (Witcher 3): `is_free=False`, `required_age=18`, `metacritic.score=93`
|
|
144
|
+
- `413150` (Stardew Valley): `is_free=False`, `required_age=0`, `metacritic=None`
|
|
145
|
+
- `427520` (Factorio): `is_free=False`, `required_age=0`
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Partial field fetch (filters=)
|
|
150
|
+
|
|
151
|
+
Fetch only specific fields to reduce payload size:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# Price only (tiny response)
|
|
155
|
+
resp = http_get("https://store.steampowered.com/api/appdetails?appids=292030&filters=price_overview")
|
|
156
|
+
data = json.loads(resp)["292030"]["data"]
|
|
157
|
+
# data keys: ["price_overview"]
|
|
158
|
+
|
|
159
|
+
# Basic metadata (no price, no media)
|
|
160
|
+
resp = http_get("https://store.steampowered.com/api/appdetails?appids=292030&filters=basic")
|
|
161
|
+
# data keys: about_the_game, capsule_image, capsule_imagev5, detailed_description, dlc,
|
|
162
|
+
# header_image, is_free, legal_notice, linux_requirements, mac_requirements,
|
|
163
|
+
# name, pc_requirements, required_age, reviews, short_description,
|
|
164
|
+
# steam_appid, supported_languages, type, website
|
|
165
|
+
|
|
166
|
+
# Multiple filters comma-separated
|
|
167
|
+
resp = http_get("https://store.steampowered.com/api/appdetails?appids=292030&filters=screenshots,price_overview")
|
|
168
|
+
# data keys: ["price_overview", "screenshots"]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Media fields
|
|
174
|
+
|
|
175
|
+
### Screenshots
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
game = get_app(292030)
|
|
179
|
+
for ss in game["screenshots"]: # 18 screenshots for Witcher 3
|
|
180
|
+
print(ss["id"]) # 0, 1, 2, ...
|
|
181
|
+
print(ss["path_thumbnail"]) # 600x338 JPEG URL
|
|
182
|
+
print(ss["path_full"]) # 1920x1080 JPEG URL
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Movies / trailers
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
for m in game["movies"]: # 4 trailers for Witcher 3
|
|
189
|
+
print(m["id"]) # integer
|
|
190
|
+
print(m["name"]) # trailer title
|
|
191
|
+
print(m["thumbnail"]) # thumbnail URL
|
|
192
|
+
print(m["highlight"]) # bool — main trailer flag
|
|
193
|
+
# m["webm"] -> None (old format, mostly absent)
|
|
194
|
+
# m["mp4"] -> None (old format, mostly absent)
|
|
195
|
+
# m["dash_av1"] -> dash_av1 stream URL (present on modern entries)
|
|
196
|
+
# m["dash_h264"] -> dash_h264 stream URL
|
|
197
|
+
# m["hls_h264"] -> HLS stream URL
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Ratings and content descriptors
|
|
203
|
+
|
|
204
|
+
The `ratings` dict contains per-region rating board data for mature games:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
game = get_app(292030)
|
|
208
|
+
|
|
209
|
+
# ESRB (North America)
|
|
210
|
+
esrb = game["ratings"].get("esrb", {})
|
|
211
|
+
esrb["rating"] # "m" (lowercase) -> M for Mature
|
|
212
|
+
esrb["descriptors"] # "Blood and Gore\r\nIntense Violence\r\nNudity\r\n..."
|
|
213
|
+
esrb["use_age_gate"] # "true" (string, not bool)
|
|
214
|
+
esrb["required_age"] # "17" (string, not int)
|
|
215
|
+
|
|
216
|
+
# PEGI (Europe)
|
|
217
|
+
pegi = game["ratings"].get("pegi", {})
|
|
218
|
+
pegi["rating"] # "18"
|
|
219
|
+
pegi["descriptors"] # "Violence\r\nBad language"
|
|
220
|
+
|
|
221
|
+
# USK (Germany)
|
|
222
|
+
usk = game["ratings"].get("usk", {})
|
|
223
|
+
usk["rating"] # "18"
|
|
224
|
+
|
|
225
|
+
# steam_germany (Germany digital-only classification)
|
|
226
|
+
sg = game["ratings"].get("steam_germany", {})
|
|
227
|
+
sg["rating"] # "16"
|
|
228
|
+
sg["banned"] # "0" (1 = banned in Germany)
|
|
229
|
+
|
|
230
|
+
# igrs (Indonesia)
|
|
231
|
+
igrs = game["ratings"].get("igrs", {})
|
|
232
|
+
igrs["rating"] # "BANNED" if banned there
|
|
233
|
+
igrs["banned"] # "1"
|
|
234
|
+
|
|
235
|
+
# Other keys: oflc, nzoflc, kgrb, dejus, mda, fpb, csrr, crl
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Content descriptor IDs (from `content_descriptors.ids`):
|
|
239
|
+
- `1` = Some Nudity or Sexual Content
|
|
240
|
+
- `5` = Frequent Violence or Gore
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Age-gated store pages
|
|
245
|
+
|
|
246
|
+
**The `appdetails` API completely bypasses age gates.** It returns full data for any game regardless of rating or age restriction — no cookies needed.
|
|
247
|
+
|
|
248
|
+
The **store webpage** (`store.steampowered.com/app/{appid}/`) redirects mature games to an age verification form:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
GET https://store.steampowered.com/app/292030/
|
|
252
|
+
-> 302 -> https://store.steampowered.com/agecheck/app/292030/
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
To bypass the age gate on the store page, send the `birthtime` cookie:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
import urllib.request
|
|
259
|
+
|
|
260
|
+
def get_store_page(appid):
|
|
261
|
+
"""Fetch game store HTML page, bypassing age gate."""
|
|
262
|
+
req = urllib.request.Request(
|
|
263
|
+
f"https://store.steampowered.com/app/{appid}/",
|
|
264
|
+
headers={
|
|
265
|
+
"User-Agent": "Mozilla/5.0",
|
|
266
|
+
"Cookie": "birthtime=631152001; lastagecheckage=1-January-1990"
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
with urllib.request.urlopen(req, timeout=15) as r:
|
|
270
|
+
html = r.read().decode("utf-8", errors="replace")
|
|
271
|
+
if "agecheck" in r.url:
|
|
272
|
+
return None # Age gate not bypassed
|
|
273
|
+
return html
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`birthtime=631152001` = January 1, 1990 in Unix time. Steam accepts any date before the current year minus the required age.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Search
|
|
281
|
+
|
|
282
|
+
### storesearch API (title search, up to 10 results)
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
import json, urllib.parse
|
|
286
|
+
from helpers import http_get
|
|
287
|
+
|
|
288
|
+
def search_games(term, cc="US", lang="english"):
|
|
289
|
+
"""
|
|
290
|
+
Returns up to 10 matching apps/DLC/bundles.
|
|
291
|
+
No pagination — always exactly 10 results max.
|
|
292
|
+
"""
|
|
293
|
+
q = urllib.parse.quote(term)
|
|
294
|
+
resp = http_get(
|
|
295
|
+
f"https://store.steampowered.com/api/storesearch/?term={q}&l={lang}&cc={cc}"
|
|
296
|
+
)
|
|
297
|
+
data = json.loads(resp)
|
|
298
|
+
return data["items"]
|
|
299
|
+
|
|
300
|
+
results = search_games("witcher")
|
|
301
|
+
# [
|
|
302
|
+
# {"type": "app", "name": "The Witcher 3: Wild Hunt", "id": 292030,
|
|
303
|
+
# "price": {"currency": "USD", "initial": 3999, "final": 3999},
|
|
304
|
+
# "tiny_image": "https://...", "metascore": "93",
|
|
305
|
+
# "platforms": {"windows": True, "mac": False, "linux": False},
|
|
306
|
+
# "streamingvideo": False},
|
|
307
|
+
# {"type": "sub", ...}, # bundles have type="sub"
|
|
308
|
+
# ...
|
|
309
|
+
# ]
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Search result fields:**
|
|
313
|
+
- `id` — appid (or subid for bundles)
|
|
314
|
+
- `type` — `"app"` | `"sub"` (bundle)
|
|
315
|
+
- `name` — game title
|
|
316
|
+
- `price` — `{"currency": "USD", "initial": cents, "final": cents}` — `None` for F2P
|
|
317
|
+
- `metascore` — string e.g. `"93"`, `"0"` if no score
|
|
318
|
+
- `platforms` — `{"windows": bool, "mac": bool, "linux": bool}`
|
|
319
|
+
- `tiny_image` — 231x87px capsule image URL
|
|
320
|
+
- `streamingvideo` — bool
|
|
321
|
+
|
|
322
|
+
**Note:** `price` in search results has only `initial` and `final` — no `discount_percent` or `formatted` strings. Use `appdetails` for full pricing.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Review scores and user reviews
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
import json, urllib.parse
|
|
330
|
+
from helpers import http_get
|
|
331
|
+
|
|
332
|
+
def get_reviews(appid, num=10, language="english", filter="recent",
|
|
333
|
+
review_type="all", purchase_type="all", cursor="*"):
|
|
334
|
+
"""
|
|
335
|
+
filter: "recent" | "updated" | "all"
|
|
336
|
+
review_type: "all" | "positive" | "negative"
|
|
337
|
+
purchase_type: "all" | "steam" | "non_steam_purchase"
|
|
338
|
+
language: "english" | "all" | ISO code
|
|
339
|
+
cursor: use returned cursor for next page (URL-encode it)
|
|
340
|
+
"""
|
|
341
|
+
encoded_cursor = urllib.parse.quote(cursor)
|
|
342
|
+
resp = http_get(
|
|
343
|
+
f"https://store.steampowered.com/appreviews/{appid}"
|
|
344
|
+
f"?json=1&num_per_page={num}&language={language}"
|
|
345
|
+
f"&filter={filter}&review_type={review_type}"
|
|
346
|
+
f"&purchase_type={purchase_type}&cursor={encoded_cursor}"
|
|
347
|
+
)
|
|
348
|
+
return json.loads(resp)
|
|
349
|
+
|
|
350
|
+
result = get_reviews(292030, num=5, language="english")
|
|
351
|
+
|
|
352
|
+
# result["success"] -> 1 (int, not bool)
|
|
353
|
+
# result["cursor"] -> "AoJ4rq..." (base64, URL-encode for next page)
|
|
354
|
+
# result["query_summary"]["review_score"] -> 9 (0–9 score)
|
|
355
|
+
# result["query_summary"]["review_score_desc"] -> "Overwhelmingly Positive"
|
|
356
|
+
# result["query_summary"]["total_positive"] -> 226883
|
|
357
|
+
# result["query_summary"]["total_negative"] -> 7499
|
|
358
|
+
# result["query_summary"]["total_reviews"] -> 234382 (steam purchase only)
|
|
359
|
+
# result["reviews"] -> list of review objects
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Review score descriptions (review_score int to string):**
|
|
363
|
+
|
|
364
|
+
| Score | Description |
|
|
365
|
+
|-------|-------------|
|
|
366
|
+
| 9 | Overwhelmingly Positive |
|
|
367
|
+
| 8 | Very Positive |
|
|
368
|
+
| 7 | Mostly Positive |
|
|
369
|
+
| 6 | Positive (Mixed) |
|
|
370
|
+
| 5 | Mixed |
|
|
371
|
+
| 4 | Mostly Negative |
|
|
372
|
+
| 3 | Negative |
|
|
373
|
+
| 2 | Mostly Negative |
|
|
374
|
+
| 1 | Overwhelmingly Negative |
|
|
375
|
+
| 0 | No reviews |
|
|
376
|
+
|
|
377
|
+
**Confirmed scores:** Witcher 3 = 9, Counter-Strike 2 = 8, Stardew Valley = 9, Factorio = 9.
|
|
378
|
+
|
|
379
|
+
### Review object fields
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
review = result["reviews"][0]
|
|
383
|
+
review["recommendationid"] # "221423937" — unique review ID
|
|
384
|
+
review["voted_up"] # True/False — positive/negative
|
|
385
|
+
review["votes_up"] # 213 — helpful votes
|
|
386
|
+
review["votes_funny"] # 66
|
|
387
|
+
review["weighted_vote_score"] # 0.8405... — Steam's helpfulness score
|
|
388
|
+
review["comment_count"] # 20
|
|
389
|
+
review["steam_purchase"] # True
|
|
390
|
+
review["received_for_free"] # False
|
|
391
|
+
review["written_during_early_access"]# False
|
|
392
|
+
review["timestamp_created"] # 1774209092 (Unix timestamp)
|
|
393
|
+
review["timestamp_updated"] # Unix timestamp
|
|
394
|
+
review["language"] # "english"
|
|
395
|
+
review["review"] # review text
|
|
396
|
+
review["app_release_date"] # Unix timestamp of game release
|
|
397
|
+
|
|
398
|
+
review["author"]["steamid"] # "76561198..."
|
|
399
|
+
review["author"]["personaname"] # display name
|
|
400
|
+
review["author"]["num_games_owned"] # 1039
|
|
401
|
+
review["author"]["num_reviews"] # 180
|
|
402
|
+
review["author"]["playtime_forever"] # 1146 (minutes total)
|
|
403
|
+
review["author"]["playtime_last_two_weeks"] # minutes in last 2 weeks
|
|
404
|
+
review["author"]["playtime_at_review"] # minutes at time of review
|
|
405
|
+
review["author"]["last_played"] # Unix timestamp
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Cursor-based pagination
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
import urllib.parse, json
|
|
412
|
+
from helpers import http_get
|
|
413
|
+
|
|
414
|
+
def get_all_reviews(appid, max_pages=5, num_per_page=100, language="all"):
|
|
415
|
+
"""Paginate through reviews using cursor."""
|
|
416
|
+
cursor = "*"
|
|
417
|
+
all_reviews = []
|
|
418
|
+
for _ in range(max_pages):
|
|
419
|
+
resp = http_get(
|
|
420
|
+
f"https://store.steampowered.com/appreviews/{appid}"
|
|
421
|
+
f"?json=1&num_per_page={num_per_page}&language={language}"
|
|
422
|
+
f"&filter=recent&cursor={urllib.parse.quote(cursor)}"
|
|
423
|
+
)
|
|
424
|
+
data = json.loads(resp)
|
|
425
|
+
batch = data.get("reviews", [])
|
|
426
|
+
if not batch:
|
|
427
|
+
break
|
|
428
|
+
all_reviews.extend(batch)
|
|
429
|
+
cursor = data.get("cursor", "")
|
|
430
|
+
if not cursor:
|
|
431
|
+
break
|
|
432
|
+
return all_reviews
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Featured games
|
|
438
|
+
|
|
439
|
+
### Featured items (rotating store front)
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
import json
|
|
443
|
+
from helpers import http_get
|
|
444
|
+
|
|
445
|
+
data = json.loads(http_get("https://store.steampowered.com/api/featured/"))
|
|
446
|
+
# data["large_capsules"] -> 1-3 hero banner items
|
|
447
|
+
# data["featured_win"] -> 10 featured items for Windows
|
|
448
|
+
# data["featured_mac"] -> macOS featured
|
|
449
|
+
# data["featured_linux"] -> Linux featured
|
|
450
|
+
# data["status"] -> 1
|
|
451
|
+
|
|
452
|
+
item = data["featured_win"][0]
|
|
453
|
+
# item["id"] -> appid
|
|
454
|
+
# item["name"] -> game title
|
|
455
|
+
# item["discounted"] -> bool
|
|
456
|
+
# item["discount_percent"] -> 0-100
|
|
457
|
+
# item["original_price"] -> cents
|
|
458
|
+
# item["final_price"] -> cents
|
|
459
|
+
# item["currency"] -> "USD"
|
|
460
|
+
# item["windows_available"] -> bool
|
|
461
|
+
# item["mac_available"] -> bool
|
|
462
|
+
# item["linux_available"] -> bool
|
|
463
|
+
# item["large_capsule_image"] -> URL
|
|
464
|
+
# item["small_capsule_image"] -> URL
|
|
465
|
+
# item["header_image"] -> URL
|
|
466
|
+
# item["controller_support"] -> "full" | "partial" | ""
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Featured categories (top sellers, specials, new releases, coming soon)
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
data = json.loads(http_get("https://store.steampowered.com/api/featuredcategories/"))
|
|
473
|
+
|
|
474
|
+
# Named sections (most useful):
|
|
475
|
+
specials = data["specials"]["items"] # 10 on-sale games
|
|
476
|
+
top_sellers = data["top_sellers"]["items"] # 10 top sellers
|
|
477
|
+
new_releases= data["new_releases"]["items"] # 30 new releases
|
|
478
|
+
coming_soon = data["coming_soon"]["items"] # 10 upcoming games
|
|
479
|
+
|
|
480
|
+
# Numbered keys "0" through "7" are spotlight banners (deals/events)
|
|
481
|
+
|
|
482
|
+
item = top_sellers[0]
|
|
483
|
+
# item["id"] -> appid
|
|
484
|
+
# item["name"] -> game title
|
|
485
|
+
# item["discounted"] -> bool
|
|
486
|
+
# item["discount_percent"] -> 0-100
|
|
487
|
+
# item["original_price"] -> cents (None for upcoming games)
|
|
488
|
+
# item["final_price"] -> cents (0 for upcoming)
|
|
489
|
+
# item["currency"] -> "USD"
|
|
490
|
+
# item["discount_expiration"] -> Unix timestamp (present for active sales)
|
|
491
|
+
# item["windows_available"] -> bool
|
|
492
|
+
# item["mac_available"] -> bool
|
|
493
|
+
# item["linux_available"] -> bool
|
|
494
|
+
# item["header_image"] -> URL
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## App list (all Steam apps)
|
|
500
|
+
|
|
501
|
+
The `ISteamApps/GetAppList` API endpoint (v1, v2, v0001, v0002) currently returns **HTTP 404** from `api.steampowered.com` as of 2026-04-18. The endpoint is effectively retired without a Steamworks API key.
|
|
502
|
+
|
|
503
|
+
**Workaround:** Use the featured categories and search APIs to discover appids, then batch-fetch via `appdetails`.
|
|
504
|
+
|
|
505
|
+
```python
|
|
506
|
+
# Discover appids from top sellers + new releases
|
|
507
|
+
import json
|
|
508
|
+
from helpers import http_get
|
|
509
|
+
|
|
510
|
+
def get_all_store_appids():
|
|
511
|
+
data = json.loads(http_get("https://store.steampowered.com/api/featuredcategories/"))
|
|
512
|
+
appids = set()
|
|
513
|
+
for key in ["specials", "top_sellers", "new_releases", "coming_soon"]:
|
|
514
|
+
for item in data.get(key, {}).get("items", []):
|
|
515
|
+
appids.add(item["id"])
|
|
516
|
+
for key in ["featured_win", "featured_mac", "featured_linux"]:
|
|
517
|
+
for item in data.get(key, []):
|
|
518
|
+
appids.add(item["id"])
|
|
519
|
+
return sorted(appids)
|
|
520
|
+
|
|
521
|
+
# Returns ~50 store-front appids (enough to seed further discovery)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## Rate limits
|
|
527
|
+
|
|
528
|
+
Steam's public APIs are generous. Confirmed during testing:
|
|
529
|
+
|
|
530
|
+
- **10 sequential requests in 1.59s** — all HTTP 200, no throttling
|
|
531
|
+
- **10 concurrent requests (5 workers) in 0.54s** — all succeeded
|
|
532
|
+
- **No `Retry-After` header** observed at any concurrency level
|
|
533
|
+
|
|
534
|
+
Practical limits (undocumented, inferred from community reports):
|
|
535
|
+
- ~200 requests/5 minutes per IP to `appdetails` before soft throttling (returns `success: false`)
|
|
536
|
+
- Review API is more restrictive — keep to ~50 requests/minute
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Gotchas
|
|
541
|
+
|
|
542
|
+
**`success: false` with no data field** — When an appid is invalid, removed, or unreleased, the response is `{"999999": {"success": false}}` with no `data` key. Always check `entry["success"]` before accessing `entry["data"]`.
|
|
543
|
+
|
|
544
|
+
```python
|
|
545
|
+
entry = json.loads(resp)[str(appid)]
|
|
546
|
+
if not entry["success"]:
|
|
547
|
+
return None # game removed or never existed
|
|
548
|
+
game = entry["data"]
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
**Multiple appids in one call — not supported** — `appids=292030,570` returns HTTP 400. The API only accepts a single appid per call. Use `ThreadPoolExecutor` for bulk fetching.
|
|
552
|
+
|
|
553
|
+
**`price_overview` is `None` for free games** — When `is_free=True`, the `price_overview` key is absent or `None`. Never index `game["price_overview"]["final"]` without a None check.
|
|
554
|
+
|
|
555
|
+
**`initial_formatted` is empty string when not on sale** — When `discount_percent == 0`, `initial_formatted` is `""`. Only `final_formatted` is reliably present and non-empty. Use `final_formatted` for display in all cases.
|
|
556
|
+
|
|
557
|
+
**Store page age gate** — `store.steampowered.com/app/{appid}/` redirects mature games to `/agecheck/app/{appid}/`. The `appdetails` API completely bypasses this — no cookies needed. For browser-based scraping of the store page, send `Cookie: birthtime=631152001; lastagecheckage=1-January-1990`.
|
|
558
|
+
|
|
559
|
+
**`storesearch` always returns ≤ 10 results** — No pagination. `total` in the response is always 10, not the true result count. For finding specific games, this is sufficient. For catalog browsing, use `appdetails` with known appids.
|
|
560
|
+
|
|
561
|
+
**`metascore` is string `"0"` in search results, int `93` in appdetails** — Inconsistent types. In `storesearch` results, `metascore` is a string (e.g. `"93"`, `"0"`). In `appdetails`, `metacritic` is a dict `{"score": 93, "url": "..."}` or absent entirely. Always `int()` the storesearch value.
|
|
562
|
+
|
|
563
|
+
**`appdetails` returns `type: "dlc"` for DLC** — Check `game["type"]` before treating every appid as a standalone game. Type values: `"game"`, `"dlc"`, `"demo"`, `"advertising"`, `"mod"`, `"video"`.
|
|
564
|
+
|
|
565
|
+
**`ratings` dict uses string booleans** — `use_age_gate` and `required_age` inside `ratings[board]` are strings (`"true"`, `"17"`), not native types. `banned` is also a string `"0"` or `"1"`.
|
|
566
|
+
|
|
567
|
+
**`ISteamApps/GetAppList` is dead** — HTTP 404 for v1, v2, v0001, v0002 endpoints as of 2026-04-18. Use store front APIs and search to discover appids instead.
|
|
568
|
+
|
|
569
|
+
**`supported_languages` is HTML** — The field contains escaped HTML like `English<strong>*</strong>, French`. Starred languages have full audio. Use `html.unescape()` and strip tags to get a clean list.
|
|
570
|
+
|
|
571
|
+
**`release_date.date` is a locale string, not ISO** — Value is `"May 18, 2015"` not `"2015-05-18"`. Parse with `datetime.strptime(d, "%B %d, %Y")` or use regex.
|
|
572
|
+
|
|
573
|
+
**Review `purchase_type` changes total counts** — `purchase_type=all` includes reviews from non-Steam purchases (physical, Humble, etc.). `purchase_type=steam` is Steam-only. Witcher 3 example: `all`=802,072 reviews, `steam`=234,385.
|
|
574
|
+
|
|
575
|
+
**Currency requires `cc=` param** — Without `cc=`, you get USD by default. Pass `cc=GB` for GBP, `cc=DE` for EUR, etc. Country codes are ISO-3166 (2-letter, uppercase).
|