API reference
A read-only HTTPS API for Steam ban data. No key, no signup. Responses are JSON.
Version 2 adds three things worth having if you poll: include, so a request only fetches the sections you read; batches of 100 accounts; and ETag revalidation, so an unchanged account returns an empty 304. Version 1 is unchanged and stays online.
Your first request
Pick your shell and paste this in. You should get JSON back.
curl "https://rustbanned.net/api/v2/players/76561198012345678?include=bans" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)"curl ships with macOS, most Linux distributions, and Windows 10 build 1803 and later, so there is nothing to install. The three commands differ only in how each shell continues a line: \ in bash and zsh, a backtick in PowerShell, and no continuation in cmd. In Windows PowerShell 5.1 curl is an alias for Invoke-WebRequest, which does not accept -H, so call curl.exe by name.
User-Agent should name your app and give a contact address. Every v2 endpoint returns 403 if it is missing or shorter than 5 characters, so call the API from a server, bot, or plugin. Browser JavaScript cannot set its own User-Agent, though the endpoints do send Access-Control-Allow-Origin: *.
The essentials
- Base URL
https://rustbanned.net/api/v2- Transport
- HTTPS only. HSTS is on, so an
http://fallback will not work. - Authentication
- None.
- Required header
User-Agent, at least 5 characters, on every endpoint.- Response format
- JSON, always wrapped in the envelope. The image proxy returns bytes, and friend scans can stream events.
- Versioning
- The major version is in the path. Fields are added, never removed or retyped.
- Rate limiting
- Enforced by Cloudflare at the edge, 5,000 requests per minute per IP.
- Batch maximum
- 100 identifiers per request — Steam's own per-call maximum.
The envelope
Every response has the same three keys. Check ok first: on true the payload is under data, on false the reason is under error.
Meta
| Field | Type | Description |
|---|---|---|
meta.generatedAt | string | When the response was produced, ISO 8601. |
meta.cached | boolean | True when every part of the payload came from the shared cache. |
meta.ageSeconds | integer | Age of the oldest upstream document behind the response. |
meta.durationMs | number | Time the origin spent on the request. |
meta.pagination | object | limit, offset, total, returned, hasMore. Collections only. |
{
"ok": true,
"data": { "…": "whatever the endpoint returns" },
"meta": {
"generatedAt": "2026-09-03T12:00:00.000Z",
"cached": true,
"ageSeconds": 42,
"durationMs": 1.71
}
}{
"ok": false,
"error": {
"code": "player_not_found",
"message": "No Steam account matches \"somevanity\".",
"status": 404,
"docs": "https://rustbanned.net/api-docs/v2#error-player_not_found"
},
"meta": {
"generatedAt": "2026-09-03T12:00:00.000Z",
"durationMs": 1.42
}
}Identifiers and include
Anywhere {steamId} appears you may pass a SteamID64, a vanity name, or a full profile URL. Percent-encode the URL form so the slashes survive the path. Resolution is cached for seven days.
76561198012345678SteamID64playernamevanity namehttps%3A%2F%2Fsteamcommunity.com%2Fid%2Fplayernameprofile URL, encodedsteamcommunity.com/groups/…group URLs are not accountsinclude decides how much upstream work a lookup does. Each section is one Steam call, so a narrower request is a faster one.
profile- Name, avatar, visibility, account age. One call.
bans- The ban record. One call, cached six hours.
rust- Rust playtime. One call per account — not batched by Steam.
level- Steam level. One call per account. Not in the default set.
all- Every section above.
Caching
Every successful response carries a weak ETag covering the payload. The timing fields in meta are excluded from it, so the validator stays stable for as long as the data does. Send it back as If-None-Match and an unchanged resource answers 304 with no body.
Responses are cached at the edge as well, so repeat traffic is often answered without reaching the origin. X-Cache says whether the payload came from cache, and X-Data-Age how old the Steam data behind it is.
Response headers
ETag- Weak validator over the payload. Send it back as If-None-Match.
Cache-Control- public, max-age=0, s-maxage=…, stale-while-revalidate=… — tuned per endpoint.
X-Cache- HIT when the payload came from the shared cache, MISS when Steam was called.
X-Data-Age- Seconds since the underlying Steam data was fetched. Also in meta.ageSeconds.
Server-Timing- Per-stage origin timings, readable in browser devtools.
cf-ray- Added by Cloudflare. Identifies the request in our logs — quote it if you report a problem.
Retry-After- On 429 only. Seconds to wait before retrying.
# Keep the ETag from the first response
$ curl -si "https://rustbanned.net/api/v2/players/76561198012345678/bans" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)" | grep -i etag
etag: W/"nr5M9VZRl4w2AuOC0P9x9VOHsWs"
# Send it back. Nothing changed, so there is no body to download.
$ curl -so /dev/null -w '%{http_code}\n' \
"https://rustbanned.net/api/v2/players/76561198012345678/bans" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)" \
-H 'If-None-Match: W/"nr5M9VZRl4w2AuOC0P9x9VOHsWs"'
304Rate limits
Rate limiting happens at the edge, in Cloudflare, before a request reaches the API. The limit is 5,000 requests per minute per IP. Ordinary use never approaches it; hitting it means a script, a scraper, or a client stuck in a retry loop.
Over the limit you get 429 and an error page rather than JSON, so check Content-Type before parsing. Honour Retry-After, back off exponentially, and do not retry in a tight loop — sustained pressure gets the IP blocked outright.
Caching is what keeps you comfortably under it. A cached response and a 304 both count as requests, but neither costs an upstream call, so conditional requests and batching are the two things that matter. One batch of 100 is one request; a hundred single lookups are a hundred.
Errors
Failures use the same envelope with ok: false. Branch on error.code, which is part of the contract — messages are written for humans and may be reworded. Not every failure comes from the API, though — Cloudflare sits in front of it and answers some requests itself, with an HTML page instead of the envelope. Check Content-Type before parsing.
| Code | Status | Meaning |
|---|---|---|
200 | OK | The request succeeded. |
304 | Not modified | Your If-None-Match matched the current ETag. No body is sent. |
400 | Bad request | A parameter is missing, malformed, or fails validation. |
403 | Forbidden | The User-Agent requirement, or a host outside the CDN allowlist. |
404 | Not found | The account or resource does not exist. |
413 | Payload too large | A POST body over 64 KB. |
415 | Unsupported media type | A POST body that is not application/json. |
429 | Too many requests | Rate limited. Honour Retry-After. |
500 | Internal server error | An unexpected failure. The Ray ID identifies it in our logs. |
503 | Service unavailable | Steam is unreachable, rate limiting this server, or a credential is missing. |
504 | Gateway timeout | Steam did not respond in time. |
Error codes
| error.code | Status | Meaning |
|---|---|---|
bad_request | 400 | The request could not be understood. |
missing_parameter | 400 | A required parameter is missing. |
invalid_parameter | 400 | A parameter was rejected as invalid. |
invalid_steam_input | 400 | Not a SteamID64, vanity name, or profile URL. |
invalid_json | 400 | The request body is not valid JSON. |
missing_user_agent | 403 | A descriptive User-Agent header is required. |
forbidden_host | 403 | That host is not on the Steam CDN allowlist. |
player_not_found | 404 | No Steam account matches that identifier. |
not_found | 404 | The requested resource does not exist. |
method_not_allowed | 405 | That HTTP method is not supported here. |
payload_too_large | 413 | The body is larger than the accepted maximum. |
unsupported_media_type | 415 | The body must be sent as application/json. |
rate_limited | 429 | Rate limit exceeded. Retry after the reset. |
internal_error | 500 | An unexpected error occurred. |
not_configured | 503 | The server is missing an upstream credential. |
upstream_unavailable | 503 | Steam is not responding. |
upstream_rate_limited | 503 | Steam rate limited this server. |
upstream_timeout | 504 | Steam did not respond in time. |
Errors from the edge
Cloudflare validates every request against a published schema before it reaches the origin. A path, query string, or JSON body that does not match is rejected there and answered with error 1020 — an HTML page carrying a cf-ray, not the JSON envelope.
A 1020 is a malformed request rather than an outage, so retrying it changes nothing. The usual causes are an identifier that is not a SteamID64, vanity name, or profile URL; an include value outside the documented set; a limit past its maximum; more than 100 identifiers in one batch; or an unexpected field in a POST body. Correct the request and it goes through. If you are certain it is well-formed, send us the Ray ID.
| Cloudflare code | Status | Meaning |
|---|---|---|
1020 | 403 | Rejected by API Shield schema validation, or by a WAF rule. |
1015 | 429 | Rate limited at the edge. Honour Retry-After before retrying. |
Retrying after 429
Back off exponentially and honour Retry-After. A tight retry loop keeps the limit active and can get the IP blocked outright. The same backoff suits 502 and 503, which are usually transient. Note that a 429 comes from the edge as an error page, not as the JSON envelope.
/**
* Rate limiting happens at the edge, so a 429 arrives as an error page rather
* than the JSON envelope. Read the status before the body.
*/
export async function call(url: string, init: RequestInit = {}) {
for (let attempt = 0; attempt < 5; attempt++) {
const response = await fetch(url, {
...init,
headers: { "User-Agent": "YourAppName/1.0 (contact@example.com)", ...(init.headers ?? {}) }
});
if (response.status === 429 || response.status >= 500) {
const retryAfter = Number(response.headers.get("retry-after"));
const wait = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: 2 ** attempt * 500;
await new Promise((r) => setTimeout(r, wait));
continue;
}
if (!response.headers.get("content-type")?.includes("application/json")) {
throw new Error(`Expected JSON, got HTTP ${response.status}`);
}
const body = await response.json();
if (!body.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body.data;
}
throw new Error("gave up after five attempts");
}All endpoints
/api/v2/players/{steamId}Look up an account
Returns the profile, ban record and Rust playtime for one account.
Takes a SteamID64, a vanity name, or a profile URL. Resolving a name costs an extra Steam call the first time and nothing for the next seven days. include controls how many calls the lookup makes: bans alone is one, the default set is three, and all is four.
Parameters
steamIdstringin queryrequired- In the path. A SteamID64, a vanity name, or a profile URL — percent-encode the URL form. Maximum 256 characters.
includestringin queryoptional- Comma-separated sections: profile, bans, rust, level, or all. Defaults to profile,bans,rust. Every section you leave out is one Steam call this request does not make.
Responses
200Lookup completed.304Your If-None-Match matched; the body is unchanged and omitted.400Malformed identifier or an unknown include value.403The User-Agent header is absent or shorter than 5 characters.404No Steam account matched.429Rate limited. Back off before retrying.503Steam is unavailable.
Returns one account object.
curl "https://rustbanned.net/api/v2/players/76561198012345678" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)"{
"ok": true,
"data": {
"steamId": "76561198012345678",
"steamId2": "STEAM_0:0:26039975",
"steamId3": "[U:1:52079950]",
"accountId": 52079950,
"query": "76561198012345678",
"profile": {
"personaName": "PlayerName",
"profileUrl": "https://steamcommunity.com/id/playername/",
"vanity": "playername",
"avatar": {
"small": "https://avatars.steamstatic.com/abc.jpg",
"medium": "https://avatars.steamstatic.com/abc_medium.jpg",
"full": "https://avatars.steamstatic.com/abc_full.jpg"
},
"visibility": "public",
"createdAt": "2013-06-14T18:22:41.000Z",
"createdAtUnix": 1371234161,
"accountAgeDays": 4464,
"countryCode": "US",
"friendListPrivate": false
},
"bans": {
"banned": true,
"vac": { "banned": true, "count": 1 },
"game": { "banned": false, "count": 0 },
"community": { "banned": false },
"economy": { "status": "none", "banned": false },
"lastBan": { "daysSince": 412, "estimatedAt": "2024-07-18T00:00:00.000Z" },
"rustGameBan": false
},
"rust": { "hours": 1234.5, "minutes": 74070, "owned": true }
},
"meta": {
"generatedAt": "2026-09-03T12:00:00.000Z",
"cached": true,
"ageSeconds": 42,
"durationMs": 1.71
}
}The account object
Sections are present only when you ask for them. Fields may be added over time; they are never removed or retyped within v2.
| Field | Type | Description |
|---|---|---|
steamId | string | The 64-bit id, as text. |
steamId2 | string | Legacy form, STEAM_0:0:26039975. |
steamId3 | string | Modern form, [U:1:52079950]. |
accountId | number | 32-bit account id. |
query | string | The identifier you sent, echoed back for correlation. |
profile.personaName | string | Current display name. |
profile.profileUrl | string | Steam community URL. |
profile.vanity | string | null | Vanity slug, when the profile uses one. |
profile.avatar | object | small, medium and full image URLs. |
profile.visibility | "public" | "private" | Steam community visibility. |
profile.createdAt | string | null | Account creation, ISO 8601. Null when hidden. |
profile.createdAtUnix | number | null | The same moment as a Unix timestamp. |
profile.accountAgeDays | number | null | Whole days since creation. |
profile.countryCode | string | null | Two-letter country code, when set. |
profile.friendListPrivate | boolean | True when the friend list cannot be read. |
bans.banned | boolean | True when any ban below is active. |
bans.vac | object | banned, and count of VAC bans. |
bans.game | object | banned, and count of game bans. |
bans.community | object | banned. |
bans.economy | object | status ("none", "probation", "banned") and banned. |
bans.lastBan | object | null | daysSince and estimatedAt. Null on a clean account. |
bans.rustGameBan | boolean | null | True when a game ban is attributed to Rust. Null when Steam does not break it down per app. |
rust.hours | number | null | Playtime in hours. Null when the library is hidden. |
rust.minutes | number | null | The same playtime in minutes. |
rust.owned | boolean | null | Whether Rust is in the visible library. |
steamLevel | number | null | Steam level. Only with include=level or all. |
/api/v2/playersBatch lookup
Looks up as many as 100 accounts in one request.
100 is Steam's own per-call maximum, so a full batch costs one profile call and one ban call in total rather than a pair per player. Results keep request order and echo the identifier you sent as query, so they line up with your own list. A bad identifier does not fail the request: it is reported in unresolved (nothing to resolve to) or notFound (resolved, but no such account). Use POST when the list is too long for a URL; the GET form is cacheable and POST is not.
Parameters
idsstringin queryrequired- GET only. Comma-separated identifiers, up to 100. Mixing SteamID64s, vanity names and profile URLs is fine.
idsstring[]in bodyrequired- POST only. Same identifiers as an array, up to 100 entries.
includestringin queryoptional- Comma-separated sections: profile, bans, rust, level, or all. Defaults to profile,bans,rust. Every section you leave out is one Steam call this request does not make.
Responses
200Batch completed. Individual identifiers may still have failed to resolve.400ids is missing, empty, or holds more than 100 entries.403The User-Agent header is absent or shorter than 5 characters.413The POST body is larger than 64 KB.429Rate limited. A batch is charged one unit per ten identifiers.503Steam is unavailable.
Returns the accounts that resolved, plus unresolved and notFound lists.
curl "https://rustbanned.net/api/v2/players?ids=76561198012345678,playername&include=bans" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)"{
"ok": true,
"data": {
"players": [
{
"steamId": "76561198012345678",
"query": "76561198012345678",
"bans": { "banned": true, "vac": { "banned": true, "count": 1 } }
},
{
"steamId": "76561198087654321",
"query": "playername",
"bans": { "banned": false, "vac": { "banned": false, "count": 0 } }
}
],
"unresolved": ["not-a-real-vanity"],
"notFound": [],
"requested": 3,
"returned": 2
},
"meta": { "cached": false, "ageSeconds": 0, "durationMs": 118.4 }
}/api/v2/players/{steamId}/bansBan record only
The ban record on its own, without the profile or playtime.
One Steam call on a miss, a six-hour shared cache and a five-minute edge cache — the endpoint to use if you are polling. With If-None-Match, an unchanged account returns an empty 304.
Parameters
steamIdstringin queryrequired- In the path. A SteamID64, a vanity name, or a profile URL — percent-encode the URL form. Maximum 256 characters.
Responses
200Lookup completed.304Your If-None-Match matched.400Malformed identifier.403The User-Agent header is absent or shorter than 5 characters.404No Steam account matched.429Rate limited.503Steam is unavailable.
Returns the identifiers and the ban object.
curl "https://rustbanned.net/api/v2/players/76561198012345678/bans" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)"{
"ok": true,
"data": {
"steamId": "76561198012345678",
"steamId2": "STEAM_0:0:26039975",
"steamId3": "[U:1:52079950]",
"accountId": 52079950,
"bans": {
"banned": true,
"vac": { "banned": true, "count": 2 },
"game": { "banned": true, "count": 1 },
"community": { "banned": false },
"economy": { "status": "none", "banned": false },
"lastBan": { "daysSince": 88, "estimatedAt": "2026-06-07T00:00:00.000Z" },
"rustGameBan": true
}
},
"meta": { "cached": true, "ageSeconds": 120, "durationMs": 1.9 }
}/api/v2/players/{steamId}/friendsScan a friend list
Checks every account on a player's friend list for bans.
Returns plain JSON by default. Add stream=true (or send Accept: text/event-stream) and the scan reports progress as it runs, as named Server-Sent Events. A private friend list is not an error: the response is 200 with friendList.private set and an empty array. Friends are returned banned-first, then by Rust playtime. The scan itself costs one call per 25 friends for profiles and one per 25 for bans; playtime is the expensive part, so see includeRust.
Parameters
steamIdstringin queryrequired- In the path. A SteamID64, a vanity name, or a profile URL — percent-encode the URL form. Maximum 256 characters.
limitintegerin queryoptional- How many friends to inspect, 1–250. Defaults to 100.
offsetintegerin queryoptional- Index into the friend list. Page through a large list with limit and offset; pages never overlap.
includeRust"all" | "banned" | "none"in queryoptional- Which friends get Rust playtime. Defaults to all. Steam has no batch form for playtime, so it is one call per friend — "banned" prices only the friends who carry a ban, which is usually a fraction of a list. The legacy true and false are still accepted.
streambooleanin queryoptional- Defaults to false. When true the response is an event stream instead of a single JSON body, and is never cached.
Responses
200Scan completed, or the friend list is private.400Malformed identifier, or limit/offset out of range.403The User-Agent header is absent or shorter than 5 characters.404No Steam account matched.429Rate limited.503Steam is unavailable.
Returns the friend list summary and one entry per friend checked.
curl "https://rustbanned.net/api/v2/players/76561198012345678/friends?limit=100" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)"{
"ok": true,
"data": {
"steamId": "76561198012345678",
"friendList": { "total": 214, "checked": 100, "banned": 7, "private": false },
"friends": [
{
"steamId": "76561198087654321",
"steamId2": "STEAM_0:1:63694296",
"steamId3": "[U:1:127388593]",
"accountId": 127388593,
"personaName": "Friend",
"profileUrl": "https://steamcommunity.com/profiles/76561198087654321/",
"avatarUrl": "https://avatars.steamstatic.com/def_medium.jpg",
"friendSince": "2019-02-11T20:43:00.000Z",
"banned": true,
"bans": { "banned": true, "game": { "banned": true, "count": 1 } },
"rust": { "hours": 3120.7, "minutes": 187242, "owned": true }
}
]
},
"meta": {
"cached": false,
"ageSeconds": 0,
"durationMs": 942.5,
"pagination": { "limit": 100, "offset": 0, "total": 214, "returned": 100, "hasMore": true }
}
}event: open
data: {"steamId":"76561198012345678"}
event: progress
data: {"checked":25,"total":100,"friendListTotal":214}
event: progress
data: {"checked":50,"total":100,"friendListTotal":214}
event: result
data: {"steamId":"76561198012345678","friendList":{"total":214,"checked":100,"banned":7,"private":false},"friends":[…]}Stream events
Events are named, so switch on the event type rather than sniffing the payload.
open- { steamId } — sent immediately, so you can render a spinner.
progress- { checked, total, friendListTotal } — emitted every 25 friends.
result- The same object the JSON form returns.
error- The standard error object. The stream then closes.
/api/v2/players/{steamId}/statsRust statistics
Normalised Rust gameplay statistics.
Combat, survival, building, resources, NPC kills, items and miscellany, with K/D, accuracy and headshot rate already computed. A player who hides game details, or has never played Rust, is not an error: the response is 200 with available: false and a reason, which means that answer caches like any other.
Parameters
steamIdstringin queryrequired- In the path. A SteamID64, a vanity name, or a profile URL — percent-encode the URL form. Maximum 256 characters.
rawbooleanin queryoptional- Defaults to false. When true, Steam's raw stat keys are returned alongside the normalised object.
Responses
200Statistics returned, or reported unavailable with a reason.400Malformed identifier.403The User-Agent header is absent or shorter than 5 characters.404No Steam account matched.429Rate limited.503Steam is unavailable.
Returns the normalised statistics object, or available: false with a reason.
curl "https://rustbanned.net/api/v2/players/76561198012345678/stats" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)"{
"ok": true,
"data": {
"steamId": "76561198012345678",
"appId": 252490,
"available": true,
"reason": null,
"stats": {
"combat": {
"kills": 4821, "deaths": 3907, "kd": 1.23, "headshots": 1502,
"headshotRate": 21.4, "bulletsFired": 184203, "bulletsHit": 7018,
"accuracy": 3.8, "wounded": 812, "woundedAssisted": 260, "beenPickedUp": 143
},
"survival": { "deaths": 3907, "suicides": 212, "fallingDeaths": 96, "…": 0 },
"building": { "structuresBuilt": 21044, "upgradesBuilt": 8123, "repairsDone": 1902 },
"resources": { "woodHarvested": 4210332, "stoneHarvested": 2810442, "…": 0 },
"npc": { "scientistsKilled": 1204, "bearsKilled": 96, "…": 0 },
"items": { "itemsDropped": 8123, "itemsPickedUp": 91224, "itemsCrafted": 1422 },
"misc": { "barrelsBroken": 18422, "heliHits": 1204, "rocketsFired": 611 }
},
"raw": null
},
"meta": { "cached": true, "ageSeconds": 60, "durationMs": 2.4 }
}{
"ok": true,
"data": {
"steamId": "76561198087654321",
"appId": 252490,
"available": false,
"reason": "private",
"stats": null,
"raw": null
},
"meta": { "cached": true, "ageSeconds": 12, "durationMs": 1.1 }
}/api/v2/players/{steamId}/inventoryRust inventory
Rust skins and items from a player's Steam inventory.
Each item carries both iconUrl (straight from the Steam CDN) and proxyUrl (the same image through this API, cached and CORS-friendly). A private inventory returns 200 with visibility: "private" and an empty list.
Parameters
steamIdstringin queryrequired- In the path. A SteamID64, a vanity name, or a profile URL — percent-encode the URL form. Maximum 256 characters.
limitintegerin queryoptional- Items to return, 1–100. Defaults to 50.
Responses
200Inventory returned, or reported private.400Malformed identifier, or limit out of range.403The User-Agent header is absent or shorter than 5 characters.404No Steam account matched.429Rate limited.503Steam Community is unavailable.
Returns the inventory summary and up to limit items.
curl "https://rustbanned.net/api/v2/players/76561198012345678/inventory?limit=50" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)"{
"ok": true,
"data": {
"steamId": "76561198012345678",
"appId": 252490,
"visibility": "public",
"totalItems": 168,
"items": [
{
"assetId": "24175372918",
"classId": "3111478780",
"name": "Big Grin",
"marketHashName": "Big Grin",
"type": "Facemask Skin",
"iconUrl": "https://community.cloudflare.steamstatic.com/economy/image/abc/75fx75f",
"proxyUrl": "/api/v2/images?url=https%3A%2F%2Fcommunity.cloudflare.steamstatic.com%2F…",
"tradable": true,
"marketable": true
}
]
},
"meta": { "cached": false, "ageSeconds": 0, "durationMs": 210.7 }
}/api/v2/resolveResolve an identifier
Turns a vanity name or profile URL into a SteamID64.
Also returns the legacy STEAM_0: and modern [U:1: forms and the 32-bit account id, so you do not have to do the arithmetic. Results are cached for seven days. If you look the same accounts up repeatedly, resolve once and store the SteamID64.
Parameters
inputstringin queryrequired- A SteamID64, vanity name, or profile URL. Maximum 256 characters.
Responses
200Resolved.304Your If-None-Match matched.400Missing or malformed input.403The User-Agent header is absent or shorter than 5 characters.404No Steam account matched.429Rate limited.503Steam is unavailable.
Returns every identifier form for the account, and how the input was read.
curl "https://rustbanned.net/api/v2/resolve?input=https%3A%2F%2Fsteamcommunity.com%2Fid%2Fplayername" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)"{
"ok": true,
"data": {
"query": "https://steamcommunity.com/id/playername",
"steamId": "76561198012345678",
"steamId2": "STEAM_0:0:26039975",
"steamId3": "[U:1:52079950]",
"accountId": 52079950,
"source": "vanity_url"
},
"meta": { "cached": true, "ageSeconds": 3600, "durationMs": 1.2 }
}/api/v2/imagesImage proxy
Serves Steam CDN images with long-lived caching and open CORS.
Item icons are content-addressed and never change, so they are cached for seven days and served immutable. Avatars change often enough to be passed through with a short browser cache instead. Only Steam CDN hosts are accepted; anything else is refused with forbidden_host.
Parameters
urlstringin queryrequired- A percent-encoded Steam CDN image URL, exactly as returned in items[].iconUrl.
Responses
200The image, with its original content type.400Missing or unparseable url.403The host is not a Steam CDN host.429Rate limited.503The Steam CDN did not return an image.
Returns image bytes, not JSON.
curl "https://rustbanned.net/api/v2/images?url=https%3A%2F%2Fcommunity.cloudflare.steamstatic.com%2Feconomy%2Fimage%2Fabc%2F75fx75f" \
-H "User-Agent: YourAppName/1.0 (contact@example.com)" \
--output item.jpgWatching a roster
The common case: an admin watching a list of players for new bans. Batch the roster, ask only for bans, and keep the ETag.
100 accounts then cost two Steam calls on a cold cache and none on a warm one, and an unchanged roster returns an empty 304. Ban records are cached for six hours, so polling faster than that returns the same answer.
/**
* A ban-watch loop. Storing the ETag means an unchanged roster costs an
* empty 304 rather than a full payload, on both ends.
*/
const etags = new Map<string, string>();
async function checkRoster(steamIds: string[]) {
const url = `https://rustbanned.net/api/v2/players?ids=${steamIds.join(",")}&include=bans`;
const previous = etags.get(url);
const response = await fetch(url, {
headers: {
"User-Agent": "YourAppName/1.0 (contact@example.com)",
...(previous ? { "If-None-Match": previous } : {})
}
});
if (response.status === 304) return null; // nothing changed
const tag = response.headers.get("etag");
if (tag) etags.set(url, tag);
const { data } = await response.json();
return data.players.filter((p) => p.bans.banned);
}Migrating from v1
Optional. v1 is frozen, not deprecated — it stays online and unchanged for everything already built on it. Move when there is something in v2 you want.
Endpoints
| v1 | v2 | Notes |
|---|---|---|
GET /api/lookup?steamId=… | GET /api/v2/players/{steamId} | The identifier moves into the path. Add ?include= to trim the response. |
POST /api/lookup {input} | GET /api/v2/players/{steamId} | Percent-encode a profile URL into the path. POST is only for batches now. |
POST /api/lookup/batch {steamIds} | GET or POST /api/v2/players | ids replaces steamIds, and the cap rises from 50 to 100. |
POST /api/friends {steamId} | GET /api/v2/players/{steamId}/friends | Plain JSON by default; add ?stream=true for progress events. |
GET /api/rust-stats?steamId=… | GET /api/v2/players/{steamId}/stats | Private stats are a 200 with available:false, not a 403. |
GET /api/inventory?steamId=… | GET /api/v2/players/{steamId}/inventory | Private inventories are a 200 with visibility:"private". |
GET /api/image?url=… | GET /api/v2/images?url=… | Same allowlist, plus CORS headers and its own rate-limit budget. |
Fields
| v1 | v2 | Notes |
|---|---|---|
{ error: string } | { ok: false, error: { code, message, status } } | Branch on error.code, never on the message. |
steamId | steamId, steamId2, steamId3, accountId | All four forms are returned; no client-side conversion. |
profile.personaname | profile.personaName | Every field is camelCase. |
profile.timecreated | profile.createdAt, createdAtUnix, accountAgeDays | ISO 8601 alongside the epoch, plus the age in days. |
bans.VACBanned | bans.vac.banned | Grouped per ban type, with the count beside it. |
bans.NumberOfVACBans | bans.vac.count | |
bans.NumberOfGameBans | bans.game.count | bans.game.banned is the boolean. |
bans.CommunityBanned | bans.community.banned | |
bans.EconomyBan | bans.economy.status | bans.economy.banned is the boolean. |
bans.DaysSinceLastBan | bans.lastBan.daysSince | lastBan is null on a clean account. |
(none) | bans.banned | One boolean for is this account banned at all. |
(none) | bans.rustGameBan | True when Steam attributes a game ban to Rust. |
rustHours | rust.hours | rust.minutes and rust.owned come with it. |
friendsPrivate | profile.friendListPrivate | Same derivation, clearer name. |
Before you ship
Send a real User-Agent
Name your app and give a contact address. Anything shorter than 5 characters is refused with
missing_user_agent.Check ok, not the status code alone
Every failure carries a stable
error.code. Branch on it.Check Content-Type before parsing
A Cloudflare challenge, a rate limit, or a
1020block page is HTML. Parsing it as JSON throws something unhelpful.Ask only for what you read
include=bansis one Steam call; the default set is three. On friend scans,includeRust=bannedis the equivalent saving.Batch instead of looping
One request with 100 ids does the work of 100 requests, and counts as one.
Store the ETag
Send
If-None-Matchon every poll. Unchanged data then costs an empty304.Back off on 429
Honour
Retry-Afterand retry with an exponential delay. A tight retry loop is what turns a rate limit into a block.Handle private as data, not failure
Private stats, inventories and friend lists are
200responses with a flag.Log the Ray ID
Every response carries
cf-ray. It is the one identifier that lets us find your request.
Support
Email contact@rustbanned.net about blocked requests or unexpected errors. Include your public IP, the User-Agent you sent, the endpoint and method, the HTTP status and error.code, roughly when it happened with the timezone, and the Cloudflare Ray ID from the cf-ray response header — error pages show the same value.