If you're pulling game data from Steam to populate a directory, the public Steam Web API will surprise you in ways that aren't in the developer documentation. I ran into four of them building Find Games Like, the indie game recommender in my three-site programmatic experiment. None caused permanent damage, but each cost debugging time I wouldn't have spent with a more complete write-up somewhere.
Every response wraps in a success envelope — and HTTP 200 doesn't mean success
The standard game metadata endpoint:
https://store.steampowered.com/api/appdetails?appids=578080
Returns HTTP 200 regardless of whether the appid is valid, the game exists, or the title has been removed. The actual result is nested under an appid key:
{
"578080": {
"success": true,
"data": { "name": "PLAYERUNKNOWN'S BATTLEGROUNDS", ... }
}
}
When the appid is invalid or the game has been delisted:
{
"578080": {
"success": false
}
}
The correct check is entry?.success ? entry.data : null. If you check entry?.data directly, you get undefined on failure, which looks like a missing field in your ETL rather than a failed lookup. My TypeScript client:
const data = (await res.json()) as Record<string, { success: boolean; data: SteamAppDetail }>;
const entry = data[String(appid)];
return entry?.success ? entry.data : null;
HTTP error handling alone is not enough here. You need the explicit success check because 200 + success: false is a valid failure state.
The cc=us&l=en parameters control pricing and text consistency
Without explicit country and language parameters:
/api/appdetails?appids=578080
Steam returns pricing in the currency of your server's IP geolocation. GitHub Actions runners are in US regions, so production works fine. But local development from Japan returns JPY pricing. Development from Germany returns EUR. The price_overview.final_formatted string you're storing in Turso ends up different depending on where the job ran.
Adding cc=us&l=en:
/api/appdetails?appids=578080&cc=us&l=en
Fixes this: you consistently get USD prices formatted as "$11.99" and English strings in all description fields. Without it, a game refreshed in one country and later refreshed in another produces a spurious "price changed" update in the Turso upsert, even though the price hasn't actually changed.
l=en also matters for short_description and about_the_game — games with non-English primary locales return their native language by default. For a directory serving an English-speaking audience, you want English descriptions even when the game is French or Japanese.
The review summary endpoint is at a completely different path
The appdetails endpoint returns metadata but no review data — no ratings, no review counts, no "Very Positive" or "Mixed" label. For that, you need a separate call:
https://store.steampowered.com/appreviews/{appid}?json=1&filter=summary&purchase_type=all&num_per_page=0&language=all
This is at store.steampowered.com/appreviews/, not store.steampowered.com/api/ and not api.steampowered.com. The path structure is inconsistent with the rest of the Store API, so if you're reading URL patterns to figure out where other endpoints might live, this one breaks the pattern.
Parameters that matter:
-
json=1— returns JSON instead of an HTML page -
filter=summary— returns aggregate stats, not individual review text -
num_per_page=0— returns no individual reviews (summary only) -
purchase_type=all— includes all buyers, not just verified-purchase reviewers -
language=all— aggregates across all review languages
The response has a different success field too. appdetails uses success: boolean. This endpoint uses success: 1 — an integer, not a boolean:
const data = (await res.json()) as {
success: 1 | number;
query_summary?: { total_reviews?: number; review_score_desc?: string; ... };
};
if (data.success !== 1 || !data.query_summary) return null;
I treat review fetch failures as non-fatal in the ETL. The game row is written from appdetails first; review stats are added separately. If the review call fails with a 429 or 5xx, the ETL logs it and continues. The game still has a page; it shows no review data until the next run.
DLC and soundtrack appids pollute the full app list
Steam's GetAppList endpoint returns every entry in the catalog:
https://api.steampowered.com/ISteamApps/GetAppList/v2
That's roughly 200,000+ entries — games, DLC packs, official soundtracks, game demos, tools, dedicated server packages. All of them share the same numeric appid namespace.
If you send appdetails requests for these indiscriminately, you get back valid-looking responses. A DLC appid returns a success: true object with a data block, but data.type will be "dlc" and the name will be something like "Fallout 4 - Wasteland Workshop." A soundtrack appid returns type: "music". A dedicated server returns type: "tool".
The Find Games Like ETL avoids this by using a curated seed-appids.json — a hand-maintained list of game appids — rather than pulling from GetAppList. If you're building from the full app list instead, filter on data.type === "game" before inserting. Without that filter, your directory ends up with DLC entries that have no "similar games" context, no meaningful genre data, and no cover art in the expected dimensions.
The TypeScript interface design for these clients — specifically how optional fields are modeled and where null checks live — comes directly from encountering all four of these in production ETL runs.
Part of an ongoing 6-month experiment running three AI-curated directory sites. The technical claims here are real; this article was AI-assisted.
Top comments (0)