GuidesGAME APIS

Roblox API in browser JavaScript

Roblox's public web APIs on users.roblox.com, games.roblox.com and 5 other hosts answer without an Access-Control-Allow-Origin header, and their OPTIONS preflight returns 405, so fetch() from your page fails with a CORS error. All 13 public GET endpoints we tested on 28 October 2026 returned the same JSON through the free cors.dev proxy (no signup, GET and HEAD, about 600 requests a minute per IP). The POST username lookup needs cors.dev Pro ($5 a month for 500,000 requests, 7-day trial with 1,000 requests, no card), and Open Cloud API keys belong on your server.

Why Roblox API calls fail in the browser

Roblox splits its web API across one host per area: users.roblox.com, friends.roblox.com, groups.roblox.com, games.roblox.com, thumbnails.roblox.com, badges.roblox.com and apis.roblox.com. The Creator Hub docs file these under Legacy APIs, which "can incorporate breaking changes without notice" and are not recommended for production.

None of the 14 endpoints we tested on 28 October 2026 returned an Access-Control-Allow-Origin header. Roblox answers 200, but the browser withholds the response from your script, so fetch() rejects with TypeError: Failed to fetch in Chrome.

Preflighted requests fail earlier. An OPTIONS preflight to users.roblox.com returned 405 with {"errors":[{"code":0,"message":"MethodNotAllowed"}]}, which stops any JSON POST or request with custom headers before the real request is sent.

Public Roblox endpoints that need no key

Prepend https://proxy.cors.dev/ to the full Roblox URL and the free proxy returns the response with Access-Control-Allow-Origin set to your page origin. All 13 GET endpoints below answered 200 with the same bytes as a direct call, in two full runs 20 seconds apart.

The free tier needs no signup or key. It allows GET and HEAD at about 600 requests a minute per IP, with no daily or monthly quota, no caching and no browser cookies forwarded.

Roblox endpoints through cors.dev, checked October 2026
EndpointReturnsSizeRoblox limit per minuteFree proxy
users.roblox.com/v1/users/{id}Profile: name, display name, created, verified badge198 B30Yes
users.roblox.com/v1/users/searchFuzzy username search with cursor1.4 KB1 to 5Yes
users.roblox.com/v1/usernames/usersExact usernames to user IDs (POST)125 B500No, POST needs Pro
friends.roblox.com/v1/users/{id}/followers/countFollower count18 B1 to 100Yes
friends.roblox.com/v1/users/{id}/friends/countFriend count11 B100Yes
groups.roblox.com/v1/groups/{id}Group name, owner, member count, shout644 B7Yes
groups.roblox.com/v2/users/{id}/groups/rolesA user's groups and role in each544 B100 to 500Yes
apis.roblox.com/universes/v1/places/{placeId}/universeUniverse ID for a place ID24 B60Yes
games.roblox.com/v1/games?universeIds=Name, playing, visits, favorites, creator1.5 KB per game300Yes
games.roblox.com/v1/games/{placeId}/servers/PublicLive public servers with players, fps, ping1.6 KB per 103Yes
games.roblox.com/v1/games/votes?universeIds=Up and down votes65 B200Yes
thumbnails.roblox.com/v1/users/avatar-headshotHeadshot image URLs192 B120Yes
thumbnails.roblox.com/v1/games/iconsGame icon URLs171 B1,200Yes
badges.roblox.com/v1/universes/{id}/badgesA game's badges with cursor4.6 KB per 10100Yes

Fetch a profile, follower count and headshot

A profile card needs three calls on three hosts, and they can run in parallel. users/{id} returns name, displayName, description, created, isBanned and hasVerifiedBadge, the followers endpoint returns {"count":69892144} for user 156, and the headshot endpoint returns JSON with a state of Completed and an imageUrl.

Only that JSON needs the proxy. The image host tr.rbxcdn.com sends Access-Control-Allow-Origin: *, so the imageUrl goes straight into an <img> tag or a direct fetch() (a 150x150 headshot PNG was 15,250 bytes).

Profile, followers and headshot through the free proxy
const PROXY = 'https://proxy.cors.dev/';

async function roblox(url) {
  const response = await fetch(PROXY + url);
  if (response.status === 429) {
    throw new Error(`Rate limited, retry in ${response.headers.get('Retry-After')} s`);
  }
  if (!response.ok) throw new Error(`Roblox answered ${response.status}`);
  return response.json();
}

const userId = 156;
const headshotUrl = new URL('https://thumbnails.roblox.com/v1/users/avatar-headshot');
headshotUrl.search = new URLSearchParams({ userIds: userId, size: '150x150', format: 'Png', isCircular: 'false' });

const [user, followers, headshot] = await Promise.all([
  roblox(`https://users.roblox.com/v1/users/${userId}`),
  roblox(`https://friends.roblox.com/v1/users/${userId}/followers/count`),
  roblox(headshotUrl.href),
]);

document.querySelector('#avatar').src = headshot.data[0].imageUrl;
console.log(user.displayName, user.created, followers.count);

Leave badges.roblox.com/v1/users/{id}/badges out of public pages: it answered 401 with "Authentication token is missing", directly and through the proxy.

Game stats and live server lists

Game endpoints use two different IDs. The number in a game URL such as roblox.com/games/920587237/Adopt-Me is the place ID; game details, votes and badges want the universe ID.

  1. Resolve the universe ID

    apis.roblox.com/universes/v1/places/920587237/universe returns {"universeId":383310974} in 24 bytes.

  2. Load game stats

    games.roblox.com/v1/games?universeIds=383310974 takes comma-separated IDs. On 28 October 2026 it returned Adopt Me! with playing 230,915, visits 45,245,470,188 and favoritedCount 29,816,390, plus creator, genre and updated.

  3. List live servers

    games.roblox.com/v1/games/920587237/servers/Public?limit=10 takes the place ID and returned 10 servers in about 1.6 KB, each with id, playing, maxPlayers, fps and ping. Pass nextPageCursor back for the next page.

Place ID to game stats and live servers
const PROXY = 'https://proxy.cors.dev/';

async function getJson(url) {
  const response = await fetch(PROXY + url);
  if (!response.ok) throw new Error(`${response.status} from ${new URL(url).host}`);
  return response.json();
}

const placeId = 920587237; // the number in roblox.com/games/920587237/...
const { universeId } = await getJson(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
const { data: [game] } = await getJson(`https://games.roblox.com/v1/games?universeIds=${universeId}`);
console.log(`${game.name}: ${game.playing} playing, ${game.visits} visits`);

const servers = await getJson(`https://games.roblox.com/v1/games/${placeId}/servers/Public?limit=10`);
for (const server of servers.data) {
  console.log(server.id, `${server.playing}/${server.maxPlayers} players`, `${server.ping} ms`);
}

Rate limits and 429 responses

Every response from the 13 GET endpoints carried x-ratelimit-limit and x-ratelimit-remaining headers with a 60-second window (w=60). The limits in the table run from 1 to 1,200 requests a minute. Past the limit Roblox answers 429 with Retry-After (we saw Retry-After: 5), and cors.dev passes that header through to your code.

Some limits differed between our test server and the proxy: followers count allowed 1 a minute from our server and 100 through the proxy, user search 5 and 1. The free proxy fetches from cors.dev IP addresses that other free users share.

  • Read x-ratelimit-remaining before polling again.
  • Back off for the Retry-After seconds on a 429.
  • Poll server lists no more than the 3 a minute Roblox allows.

For dashboards where many visitors load the same server list, cors.dev Pro adds optional GET caching. Send X-Cors-Cache: 60 (1 to 300 seconds) and repeat requests for the same URL inside that window can come from cache instead of Roblox, reported in X-Cors-Cache-Status. That works for the servers endpoint, which sent no cache headers; cors.dev never caches a response marked no-store or no-cache, as the games and users responses were on 28 October 2026. Cache hits still count as cors.dev requests.

Username lookups, POST and Open Cloud

The exact username lookup is POST https://users.roblox.com/v1/usernames/users with a body like {"usernames":["builderman"],"excludeBannedUsers":true}, which returned id 156 directly. Through the free proxy it answers 405 with X-Cors-Error: method_not_allowed, because the free tier is GET and HEAD only.

The GET alternative, users.roblox.com/v1/users/search?keyword=builderman&limit=10, matches fuzzily (it also returned accounts such as BuildermanAt3AM), so compare name exactly, and its limit was 1 to 5 requests a minute.

cors.dev Pro ($5 a month for 500,000 requests) forwards POST with a JSON body up to 1 MiB, at up to 3,000 requests a minute per account and 100 in flight. The 7-day trial includes 1,000 requests and needs no card. Sign in at cors.dev/signup, create a Connection with your exact page origins and the Roblox hosts you call, and send its publishable key in X-Cors-Key.

Username to user ID with POST on cors.dev Pro
const CORS_KEY = 'YOUR_CONNECTION_KEY'; // publishable, works only from your registered origins

const response = await fetch('https://proxy.cors.dev/https://users.roblox.com/v1/usernames/users', {
  method: 'POST',
  headers: { 'X-Cors-Key': CORS_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ usernames: ['builderman', 'Roblox'], excludeBannedUsers: true }),
});

if (!response.ok) throw new Error(`${response.status} ${response.headers.get('X-Cors-Error') ?? ''}`);
const { data } = await response.json();
for (const user of data) console.log(user.requestedUsername, user.id);

Keep Open Cloud (apis.roblox.com/cloud/...) on your server. It needs an x-api-key header, answered 403 "Invalid authentication data provided" without one (on the preflight too), and Roblox's docs say the key "is equivalent to a password for your application". A key in browser code is visible to every visitor, and cors.dev does not hide it. Never put a .ROBLOSECURITY cookie in browser code or send it through any proxy.

Good questions.

Why does api.roblox.com fail with a 502 through a CORS proxy?

api.roblox.com no longer resolves, so the proxy answers 502 with upstream_unreachable. Roblox shut that host down on 22 May 2023 and moved its endpoints to apis.roblox.com and per-area hosts such as users.roblox.com and games.roblox.com. Point your code at the current host for each endpoint.

Do Roblox avatar images need a CORS proxy?

The image files do not, but the thumbnails JSON does. thumbnails.roblox.com sends no CORS header, so fetch it through https://proxy.cors.dev/. The imageUrl it returns lives on tr.rbxcdn.com, which sends Access-Control-Allow-Origin: *, so load the image directly.

How do I convert a Roblox place ID to a universe ID in JavaScript?

Call https://apis.roblox.com/universes/v1/places/{placeId}/universe, which returns JSON like {"universeId":383310974}. The endpoint sends no CORS header, so call it through https://proxy.cors.dev/ from the browser. Roblox allowed 60 of these lookups a minute in our test on 28 October 2026.

How many requests a minute can I send to the Roblox games API?

Roblox reports the limit in the x-ratelimit-limit header with a 60-second window. On 28 October 2026 it was 300 a minute for games.roblox.com/v1/games?universeIds=, 200 for votes and 3 for the public server list. A 429 comes with a Retry-After header that tells you how many seconds to wait.

Can I call Roblox Open Cloud from the browser with cors.dev?

cors.dev Pro forwards custom headers such as x-api-key from the origins you register, which suits an internal tool or an app where each user pastes their own key. A key shipped in public page code is visible to every visitor, and Roblox treats it like a password, so production Open Cloud calls with your own key belong on a backend.