GuidesGAME APIS
IGDB API from browser JavaScript
IGDB's API at api.igdb.com/v4 sends no Access-Control-Allow-Origin header, and on 28 October 2026 its OPTIONS preflight answered 401, so fetch() from a web page fails before the query runs. Every request is a POST with a Client-ID and a Bearer app token, which the free cors.dev proxy refuses (405 for POST, 400 for Authorization). cors.dev Pro ($5 a month for 500,000 requests) forwards both from the origins you register, which suits internal tools, prototypes and bring-your-own-key apps; a public site on your own token belongs behind a backend, as IGDB recommends.
Why IGDB requests fail in the browser
Every IGDB request is a POST to https://api.igdb.com/v4/{endpoint}, such as /v4/games, with an Apicalypse query as the plain-text body and two headers: Client-ID and Authorization: Bearer <app access token>. The custom headers make the browser send an OPTIONS preflight first.
On 28 October 2026 that preflight answered 401 with no Access-Control-Allow-Origin header, and so did the POST itself, so the browser stops before your query is sent. IGDB's docs are explicit: "The IGDB API does not support browser requests, CORS, for security reasons. This is because the request would leak your access token!" IGDB recommends a backend proxy that authenticates and queries the API for your page.
Cover images are the exception. images.igdb.com sends Access-Control-Allow-Origin: *, so https://images.igdb.com/igdb/image/upload/t_cover_big/{image_id}.jpg loads straight into an <img> tag (a t_cover_big JPEG was 21,797 bytes).
Get a Client ID and an app access token
IGDB authenticates through Twitch, so the setup starts in the Twitch Developer Console.
- Secure your Twitch account
Sign up for a free Twitch account and enable two-factor authentication.
- Register an application
Enter
localhostas the OAuth Redirect URL, because IGDB does not use it. - Choose Confidential
The Client Type must be Confidential to generate a client secret.
- Create a secret
Press New Secret and note the Client ID and Client Secret.
- Request an app token
Send
POST https://id.twitch.tv/oauth2/token?client_id=...&client_secret=...&grant_type=client_credentialsfrom a server or your terminal. The JSON holdsaccess_token,expires_inin seconds andtoken_typebearer.
The Twitch token endpoint sends Access-Control-Allow-Origin: * and its preflight answered 200, so a browser could call it. Don't: the request carries your client secret, and anything in browser code is visible to every visitor.
The app token is a bearer secret as well, because anyone holding it can query IGDB as your application. IGDB's FAQ says a token stays active for 60 days and an application can hold 25 active tokens, with older tokens inactivated past that.
Write an Apicalypse query
The body is a list of clauses, each ending in a semicolon. A request with no fields clause returns only IDs.
fields name,rating;picks fields,fields *;returns all of them.fields name,cover.image_id;expands a related record in the same request.where platforms = (48,6);filters,sort rating desc;sorts.search "zelda";runs a text search.limitandoffsetpaginate: the default is 10 results, the maximum 500.
POST https://api.igdb.com/v4/multiquery runs up to 10 named queries in one request. IGDB allows 4 requests per second, answers 429 Too Many Requests above that, and accepts up to 8 open requests at once.
What each proxy tier does with IGDB requests
The free cors.dev proxy (https://proxy.cors.dev/ plus the full URL) needs no signup, but it accepts only GET and HEAD and forwards only Accept, Accept-Language, If-None-Match, If-Modified-Since and Range. Every IGDB request we sent through it failed, as the table shows, so the free tier cannot call IGDB.
| Request | Direct from the browser | Free proxy | cors.dev Pro |
|---|---|---|---|
| POST api.igdb.com/v4/games with Client-ID and Bearer token | Blocked: preflight 401, no CORS header | 405 method_not_allowed | Forwarded from registered origins |
| GET with an Authorization header | Blocked: no CORS header | 400 header_not_allowed | Forwarded from registered origins |
| GET with only Client-ID | Blocked: no CORS header | Client-ID dropped, IGDB answers 401 | Forwarded from registered origins |
| POST id.twitch.tv/oauth2/token | Allowed by CORS, but exposes the client secret | 405 method_not_allowed | Keep it on a server |
| GET images.igdb.com cover JPEG | Works, Access-Control-Allow-Origin * | Not needed | Not needed, load it directly |
The dropped header is named in X-Cors-Dropped-Headers: client-id, and the refusals carry X-Cors-Error, so you can tell a proxy refusal from an IGDB 401.
Call IGDB from a browser tool with cors.dev Pro
cors.dev Pro forwards POST bodies up to 1 MiB plus Authorization, Content-Type and custom headers such as Client-ID, but only from the origins you register. Sign in at cors.dev/signup, create a Connection with your exact page origins and api.igdb.com as the target host, and send its publishable key in X-Cors-Key. Pro is $5 a month for 500,000 requests, and the 7-day trial includes 1,000 requests with no card.
The proxy answers the browser's preflight itself with 204, so IGDB never sees an OPTIONS request. cors.dev Pro allows 3,000 requests a minute per account and 100 in flight, but IGDB's 4 requests per second and 8 open requests still apply.
This setup fits internal tools used by people you trust, prototypes, and bring-your-own-key apps where each user pastes their own Client ID and app token. Whoever uses the page can read the token in the network tab; cors.dev forwards it and does not hide it. The Authorization header guide covers the Connection setup in more detail.
const CORS_KEY = 'YOUR_CONNECTION_KEY'; // publishable, works only from your registered origins
async function igdb(endpoint, query, { clientId, appToken }) {
const response = await fetch(`https://proxy.cors.dev/https://api.igdb.com/v4/${endpoint}`, {
method: 'POST',
credentials: 'omit',
headers: { 'X-Cors-Key': CORS_KEY, 'Client-ID': clientId, Authorization: `Bearer ${appToken}` },
body: query,
});
if (response.status === 429) throw new Error('IGDB allows 4 requests per second');
if (!response.ok) throw new Error(`${response.status} ${response.headers.get('X-Cors-Error') ?? 'from IGDB'}`);
return response.json();
}
// The user pastes their own Twitch app credentials into your tool
const credentials = { clientId: 'USER_CLIENT_ID', appToken: 'USER_APP_TOKEN' };
const games = await igdb('games', 'search "zelda"; fields name,total_rating,cover.image_id; limit 10;', credentials);
for (const game of games) {
const cover = game.cover ? `https://images.igdb.com/igdb/image/upload/t_cover_big/${game.cover.image_id}.jpg` : '';
console.log(game.name, Math.round(game.total_rating ?? 0), cover);
}Keep the token on a server for public sites
A public site that runs on your own IGDB credentials needs a backend between the page and IGDB, and a small Cloudflare Worker is enough. It keeps the client ID and secret as Worker secrets, renews the app token before it expires, limits which endpoints callers can reach and sets Access-Control-Allow-Origin for your domain.
The backend is also where caching belongs. cors.dev Pro's X-Cors-Cache applies to GET requests only, so it cannot cache IGDB's POST queries, and IGDB says it prefers that you store the data and serve it to your users.
// A Cloudflare Worker: IGDB_CLIENT_ID and IGDB_CLIENT_SECRET are Worker secrets
const ALLOWED = new Set(['games', 'covers', 'platforms', 'genres', 'search']);
let cached = { token: '', expiresAt: 0 };
async function appToken(env) {
if (Date.now() < cached.expiresAt) return cached.token;
const url = new URL('https://id.twitch.tv/oauth2/token');
url.search = new URLSearchParams({ client_id: env.IGDB_CLIENT_ID, client_secret: env.IGDB_CLIENT_SECRET, grant_type: 'client_credentials' });
const response = await fetch(url, { method: 'POST' });
if (!response.ok) throw new Error(`Twitch token request failed: ${response.status}`);
const { access_token, expires_in } = await response.json();
cached = { token: access_token, expiresAt: Date.now() + (expires_in - 3600) * 1000 };
return access_token;
}
export default {
async fetch(request, env) {
const headers = { 'Access-Control-Allow-Origin': 'https://your-app.example', 'Content-Type': 'application/json' };
const endpoint = new URL(request.url).pathname.slice(1);
if (request.method !== 'POST' || !ALLOWED.has(endpoint)) return new Response('{"error":"not allowed"}', { status: 404, headers });
const upstream = await fetch(`https://api.igdb.com/v4/${endpoint}`, {
method: 'POST',
headers: { 'Client-ID': env.IGDB_CLIENT_ID, Authorization: `Bearer ${await appToken(env)}` },
body: await request.text(),
});
return new Response(upstream.body, { status: upstream.status, headers });
},
};Good questions.
Why does the IGDB API return 401 when I call it from JavaScript?
In a browser, the 401 usually comes from the preflight: IGDB answers OPTIONS with 401 and no CORS headers, so the request never runs. Outside the browser, a 401 means the Client-ID or the Bearer token is missing, wrong or expired. Check the token with curl from your terminal first, then route browser calls through a backend or cors.dev Pro.
Can I load IGDB cover images without a proxy?
Yes. images.igdb.com sends Access-Control-Allow-Origin: *, so request cover.image_id in your fields clause and load https://images.igdb.com/igdb/image/upload/t_cover_big/{image_id}.jpg directly.
How long does an IGDB access token last?
IGDB's FAQ says an app access token is active for 60 days, and expires_in in the token response gives the exact lifetime in seconds. An application can have 25 active tokens; requesting more inactivates the oldest ones, so cache the token instead of requesting one per page load.
Is the IGDB API free for commercial projects?
IGDB's docs say the API is free for both non-commercial and commercial projects. Non-commercial use falls under the Twitch Developer Service Agreement, and for monetized products IGDB offers a commercial partnership that asks for visible attribution to IGDB.com.
How do I stay under IGDB's 4 requests per second limit?
Fetch more per request: raise limit up to 500, expand related records with dot notation such as cover.image_id, and batch up to 10 named queries into one POST to /v4/multiquery. Cache results on your backend, which IGDB encourages, and retry a 429 after a short pause.