GuidesSTEAM
Steam API from JavaScript
Prefix https://proxy.cors.dev/ to a Steam store or Web API URL: free, no signup, 600 requests per minute, JSON up to 1 MiB. Steam sends no Access-Control-Allow-Origin header, so fetch() from your page fails with a CORS error even though the URL opens fine in a browser tab. Game details, prices, search, reviews, current player counts, news and global achievement rates need no Steam API key. Methods that do need a key, such as GetPlayerSummaries and GetOwnedGames, belong on a server, because a key in browser code is public.
Why Steam blocks your page
Opening https://store.steampowered.com/api/appdetails?appids=620 in a tab works because a page load is not subject to CORS. A fetch() of the same URL from your site is. Responses from store.steampowered.com, api.steampowered.com and steamcommunity.com carry no Access-Control-Allow-Origin header, so the browser hides them from your script and logs blocked by CORS policy. Steam has no setting for this; the request has to come from a server, or through a proxy that adds the header.
Game details and price from the store API
appdetails returns everything on a store page. Add filters=basic,price_overview to cut the response from about 29 KB to 7 KB, cc for the currency and l for the language. An unknown app id still returns status 200, with {"1":{"success":false}}, so check success.
// Markup: <div id="game"></div>
const proxy = 'https://proxy.cors.dev/';
async function getGame(appId, country = 'us') {
const url = `https://store.steampowered.com/api/appdetails?appids=${appId}&cc=${country}&l=english&filters=basic,price_overview`;
const response = await fetch(proxy + url, { credentials: 'omit' });
if (!response.ok) {
throw new Error(`Steam request failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
}
const entry = (await response.json())[appId];
if (!entry?.success) throw new Error(`No store data for app ${appId}`);
const game = entry.data;
return {
name: game.name,
summary: new DOMParser().parseFromString(game.short_description, 'text/html').body.textContent,
image: game.header_image,
price: game.is_free ? 'Free' : (game.price_overview?.final_formatted ?? 'Not for sale'),
};
}
const card = document.querySelector('#game');
getGame(620)
.then((game) => {
const image = document.createElement('img');
image.src = game.image;
image.alt = game.name;
const title = document.createElement('h2');
title.textContent = `${game.name}, ${game.price}`;
const summary = document.createElement('p');
summary.textContent = game.summary;
card.replaceChildren(image, title, summary);
})
.catch((error) => {
card.textContent = error.message;
});Free games have no price_overview; read is_free instead. To price several games at once, pass a comma-separated appids list together with filters=price_overview; without that filter Steam answers 400 for more than one id. Images such as header_image load in an <img> tag directly, without the proxy.
Player counts and news without a key
A few api.steampowered.com methods are public. GetNumberOfCurrentPlayers returns a single number in 48 bytes, which makes a cheap live counter. GetNewsForApp returns news posts, including ones from outside news sites; add feeds=steam_community_announcements to keep only official announcements and patch notes. GetGlobalAchievementPercentagesForApp returns the unlock rate of every achievement in a game.
// Runs in a module script or the DevTools console
const proxy = 'https://proxy.cors.dev/';
async function steamWebApi(path) {
const response = await fetch(proxy + 'https://api.steampowered.com' + path, { credentials: 'omit' });
if (!response.ok) {
throw new Error(`Steam request failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
}
return response.json();
}
const appId = 730;
const [players, news] = await Promise.all([
steamWebApi(`/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid=${appId}`),
steamWebApi(`/ISteamNews/GetNewsForApp/v2/?appid=${appId}&count=3&maxlength=300&feeds=steam_community_announcements`),
]);
console.log(`${players.response.player_count.toLocaleString()} players in game right now`);
for (const item of news.appnews.newsitems) {
console.log(new Date(item.date * 1000).toDateString(), item.title, item.url);
}Steam endpoints that work from the browser
| Endpoint | Returns | Size | Key |
|---|---|---|---|
| store.steampowered.com/api/appdetails?appids=620 | Description, price, screenshots, release date, Metacritic score | 29 KB | Not needed |
| store.steampowered.com/api/appdetails?appids=620&cc=us&filters=basic,price_overview | Name, short description, header image, price | 7 KB | Not needed |
| store.steampowered.com/api/appdetails?appids=620,400&filters=price_overview | Prices for several games in one call | 0.3 KB | Not needed |
| store.steampowered.com/api/storesearch/?term=portal&cc=us | Search results with prices and Metacritic score | 3.5 KB | Not needed |
| store.steampowered.com/api/featuredcategories | Specials, top sellers, new releases | 51 KB | Not needed |
| store.steampowered.com/appreviews/620?json=1&num_per_page=5 | Review score, positive and negative totals, recent reviews | 10 KB | Not needed |
| api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid=730 | Players in game right now | 48 bytes | Not needed |
| api.steampowered.com/ISteamNews/GetNewsForApp/v2/?appid=440&count=3 | Latest news posts and patch notes | 1.9 KB | Not needed |
| api.steampowered.com/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2/?gameid=440 | Unlock rate of every achievement | 2.3 KB | Not needed |
| steamcommunity.com/market/priceoverview/?appid=730¤cy=1&market_hash_name=... | Lowest and median market price, volume | 78 bytes | Not needed |
| steamcommunity.com/profiles/<steamid64>?xml=1 | Public profile as XML | 0.8 KB | Not needed |
| api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/ | Names, avatars and online status | n/a | Required, call it from a server |
| api.steampowered.com/IPlayerService/GetOwnedGames/v1/ | Owned games and playtime | n/a | Required, call it from a server |
API keys and limits
Player data methods such as GetPlayerSummaries and GetOwnedGames need a key from steamcommunity.com/dev/apikey. Keep those calls on a server or a serverless function. The free tier refuses any target URL with a key parameter with a 400 error, which stops a key from leaking by accident. Pro accepts keys in URLs for APIs meant to be called that way, but anything in browser code is visible to every visitor, so a Steam key still does not belong there.
- cors.dev free tier: GET and HEAD, 600 requests per minute per client IP, no daily or monthly quota, responses up to 1 MiB and 10 seconds for Steam to answer. Cookies are never forwarded.
- Steam throttling: the store API is undocumented and so are its limits. Cache app details for hours; names and prices rarely change within a day.
- Community data: market prices and
?xml=1profiles work for public data only.
Good questions.
Do I need a Steam API key?
Not for the store API (appdetails, storesearch, featuredcategories, appreviews) or for GetNumberOfCurrentPlayers, GetNewsForApp and GetGlobalAchievementPercentagesForApp. You need one for player data such as profiles, friend lists and owned games, and those calls belong on a server.
Can I send my Steam API key through cors.dev?
The free tier refuses URLs that contain a key parameter with a 400 error. Pro accepts keys in URLs, but a key used in browser code can be copied by anyone who opens developer tools, so call keyed Steam methods from your own server.
Why does appdetails return null for several app ids?
Steam only accepts a list of app ids when filters=price_overview is set; otherwise it answers 400 with a body of null. For full details, request one app id per call.
How do I show prices in euros or pounds?
Set cc to a country code: cc=de returned 9,69€ and cc=gb returned £8.19 for Portal 2. Add l for the language of the text fields. final_formatted is ready to display, and final holds the price as an integer in cents.
How often can I call the Steam store API?
cors.dev allows 600 requests per minute per client IP on the free tier, with no daily quota. Steam throttles on its side without publishing numbers, so cache responses and do not refetch the same app on every page view.
Does this work in a Unity WebGL or browser game?
Yes. Any code running in a browser, including Unity WebGL builds, goes through the same CORS check, so the same prefixed URLs work there.