Data API · v1
The FiveM census, as JSON
Every public FiveM server on the Cfx.re list, sampled continuously: live and
historical player counts, the full server directory, which resources the
industry runs, and what every server has installed or removed since we started
watching. The same dataset behind
our public analytics pages, with the
queries those pages cannot express.
- Base URL
https://fivestatus.com/api/data/v1
- Auth
- API key, in a header
- Methods
GET only
- Format
- JSON, UTF-8
Getting started
Keys are issued by hand. Tell us what you are building, roughly how often you
need to read, and which of the scopes below you need, and
we will send you a key and the tier that fits.
Ask in our Discord.
Once you have one, this is the whole thing:
curl -s "https://fivestatus.com/api/data/v1/ecosystem" \
-H "Authorization: Bearer fsk_k3f9a2b7c1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Start with /api/data/v1/meta. It tells you which key you are using,
which scopes it holds and how much of each rate window is left — which saves
discovering all three by trial and error in production.
Authentication
Send your key in a header. Either of these works:
Authorization: Bearer fsk_k3f9a2b7c1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-API-Key: fsk_k3f9a2b7c1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authorization: Bearer is the one to prefer. X-API-Key
exists because a good deal of the tooling people reach for — n8n, Zapier, an
HTTP wrapper inside a FiveM resource — makes a custom header easy and a
scheme-prefixed Authorization header awkward.
Not in the query string
?api_key= is refused with a token_in_query error
rather than quietly accepted. Query strings are recorded in web server access
logs, in proxy logs, in browser history, and in the Referer header
of every outbound link from a page — a header is recorded in none of those. It
would be two lines to support and it is a genuinely bad idea, so the API tells
you why instead of letting you ship it.
Not from a browser
This API deliberately sends no CORS header, so a browser will
refuse a fetch to it from your page. That is not an oversight —
calling it from front-end JavaScript means shipping your key to every visitor,
where anyone can read it out of the network tab and spend your quota. Proxy the
API through your own server, where your key belongs, and cache the result there.
If you want public, anonymous, browser-callable data for your own server, that
already exists: a FiveStatus status page has an open
/api/v1/status/<slug> endpoint with CORS enabled and no key
required.
How a key is stored
We keep a hash of your key and the first ten characters of it, never the key
itself, so it cannot be read back to you — or out of us. If you lose it, we
regenerate it. A regeneration invalidates the previous key
immediately, with no grace window: the reason to rotate is
nearly always that the old one is somewhere it should not be.
Scopes
Each key holds a set of scopes, and each group of endpoints needs one. They are
coarse on purpose — five families, not one per endpoint — because a scope list
long enough that nobody reads it before ticking all of them is a scope list that
achieves nothing.
| Scope | Covers |
ecosystem |
Industry-wide totals and the historical curve. |
servers |
The server directory, individual servers, and their player history. |
resources |
Resource adoption, and which servers run a given resource. |
changes |
The install and removal log, and what is gaining or losing ground. |
segments |
Breakdowns by region and by framework. |
A request to an endpoint your key is not scoped for returns
403 missing_scope, and the message lists the scopes it does have.
/api/data/v1/meta needs no scope at all.
Rate limits
Two windows, per key rather than per address, because they stop different
things. The per-minute window stops a loop — a client retrying with no backoff,
which is the common failure and is almost never deliberate. The daily window
stops a crawl — a client politely staying inside the burst limit while walking
the entire server list every hour, which no integration needs and which a
per-minute limit cannot distinguish from normal use.
| Tier | Per minute | Per day | Who it is for |
trial |
30 |
2,000 |
For trying the API out. Enough to build against, not enough to run on. |
standard |
120 |
50,000 |
The default. Comfortably more than a dashboard or a Discord bot needs. |
partner |
600 |
500,000 |
For products serving this data on to their own users. |
internal |
2,000 |
5,000,000 |
Our own services. Effectively unmetered. |
Your key can override either window — if you need more than a tier gives, ask,
rather than working around it. /api/data/v1/meta reports what your
key actually has.
The headers
Every response carries all six, whether it was served or refused:
| Header | Meaning |
X-RateLimit-Limit | Requests allowed this minute. |
X-RateLimit-Remaining | How many of those are left. |
X-RateLimit-Reset | Seconds until the minute window resets. |
X-RateLimit-Limit-Day | Requests allowed today. |
X-RateLimit-Remaining-Day | How many of those are left. |
X-RateLimit-Reset-Day | Seconds until midnight UTC. |
Retry-After | On a 429 only. Seconds to wait — honour this. |
They are on successful responses too, which is the point: a client can only pace
itself if it can see the quota draining before it runs out.
What a refusal costs you
Nothing. A refused request does not consume daily quota — otherwise a client in
a retry loop would burn a whole day's allowance against a wall in about a
minute, turning a brief misconfiguration into an outage lasting until midnight.
Refusals are still refused by the burst window; they are just not billed.
Read error.window on a 429 before retrying.
minute clears in under sixty seconds; day does not
clear until 00:00 UTC, and the Retry-After on it is hours rather
than seconds.
Caching
Responses carry Cache-Control: private, max-age=30.
The census moves every few minutes, so honouring that costs you nothing in
freshness and a good deal of your quota. private because the
request carried a credential — do not let a shared proxy hold these.
Every response also carries meta.as_of: the moment the underlying
measurement was taken, as distinct from meta.retrieved_at. Use
as_of when you display a figure, or you will label a four-minute-old
sample as live.
Key states
Three ways a key stops working, and they mean different things:
| State | Error | What it means |
| Expired |
key_expired |
It had an end date, set when it was issued. Ask for a renewal. |
| Suspended |
key_suspended |
We switched it off, and it can be switched back on. The reason is in the message. |
| Revoked |
key_revoked |
Permanent. A revoked key is never reinstated and its prefix is never reissued. |
Source restrictions
A key can be locked to specific addresses or CIDR blocks, v4 or v6. It is the
strongest control available here and worth asking for if your integration runs
from a fixed address: a leaked key is a problem, but a leaked key that only works
from one datacentre is a much smaller one.
A request from anywhere else returns 403 address_not_allowed. The
response does not say which addresses are allowed, and will not — if the key has
leaked, that response would be a free "tell me your allow-list" oracle for
whoever holds it. meta.key.source_restricted tells you whether a
restriction exists; ask us what is on it.
Response shape
Every successful response is { "data": …, "meta": … } — including
the ones returning a single object. Two shapes would mean two code paths in
every client, and adding pagination to an endpoint later would become a breaking
change rather than a new field in meta.
{
"data": [ … ],
"meta": {
"as_of": "2026-10-03T11:58:00+00:00",
"retrieved_at": "2026-10-03T12:00:04+00:00",
"page": 1, "per_page": 25, "total": 35912, "pages": 1437, "has_more": true
}
}
Conventions that hold everywhere:
- Times are ISO 8601 with an offset. A missing time is
null, never an empty string.
-
Codes travel with their labels —
region: {code, name} rather
than a bare "BR" — so you do not need a copy of our lookup
tables to render a row.
-
null means "we do not know", and is never substituted with
zero. A null change_24h_percent is a gap in the series, not a
flat day.
-
Every response has an
X-Request-Id header. Quote it if you
report a problem.
What is never in a response
-
Players. The Cfx master list ships every connected player's
name and identifiers in every frame. Our decoder walks past those bytes and
there is no column for them anywhere in the dataset, so there is nothing
here to return. You cannot learn who is online from this API, by design.
-
Server addresses.
host:port is not in any
payload. One request returning a hundred rows of it would be a
machine-readable target list for thirty-five thousand game servers whose
owners have no relationship with us, and the attack that enables is the one
FiveM servers actually suffer. connect_url reaches the same
server through Cfx's own resolution.
Parameters & paging
Paging is page and per_page.
per_page defaults to 25 and is capped at
100 — a larger value is clamped rather than refused, and
meta.per_page reports what you actually got. Branch on
meta.has_more rather than computing it from
page × per_page < total, which every client gets wrong at least
once.
sort takes a fixed set of names per endpoint, listed with each one
below. They are deliberately our vocabulary rather than our column names, so a
schema change is not a breaking change for you — and an unrecognised value is a
422 rather than being passed anywhere near SQL. order
is asc or desc, defaulting to descending, because
every sortable measure here is a count and nobody wants the emptiest first.
Window parameters (hours, days) are clamped to what is
actually stored and the response says what it used. Per-server history is pruned
on a schedule, so a request for a year of it would otherwise return a partial
series implying a complete one.
Every list endpoint adds id or name as a final
tiebreaker, so paging is stable. Without one, the thousands of servers sharing a
player count of zero could order differently between two queries and you would
see rows repeat on one page and vanish from another.
Errors
Every failure — authentication, validation, 404, 429, ours — comes back in one
shape. Branch on error.code, which is part of the contract;
error.message is for whoever reads your logs and may be reworded.
{
"error": {
"code": "missing_scope",
"message": "This key does not have the \"changes\" scope, which this endpoint needs. It currently has: servers, resources.",
"docs": "https://fivestatus.com/data-docs#scopes"
}
}
| Status | Code | Meaning | What to do |
| 401 |
no_token |
No key was sent. |
Add an Authorization: Bearer header. |
| 401 |
malformed_token |
The value sent is not the right shape for a key. |
Check for a truncated copy, a stray "Bearer", or a shell that ate part of it. |
| 401 |
unknown_token |
No key matches. Also returned when the prefix is right and the secret is wrong — deliberately indistinguishable. |
If the key was regenerated, the previous one stopped working immediately. |
| 401 |
token_in_query |
The key was sent as ?api_key= or ?token=. |
Move it to a header. Query strings are recorded in access logs, proxy logs and Referer headers. |
| 403 |
missing_scope |
The key is valid but not for this endpoint. The message lists the scopes it does have. |
Ask us to add the scope. |
| 403 |
key_suspended |
Temporarily switched off. The reason is in the message where there is one. |
Get in touch — suspensions are reversible. |
| 403 |
key_revoked |
Permanently withdrawn. |
A revoked key is never reinstated. You need a new one. |
| 403 |
key_expired |
Past its expiry date, which is in the message. |
Ask for a renewal. |
| 403 |
address_not_allowed |
The key is restricted to specific source addresses and this request did not come from one. |
Tell us the address your traffic leaves from. We do not echo the allow-list back, on purpose. |
| 404 |
not_found |
No such endpoint, server or resource. |
We only hold servers currently or recently on the Cfx list, and resources above the tracking floor. |
| 405 |
method_not_allowed |
Something other than GET. |
The API is read-only. |
| 422 |
invalid_request |
A parameter was rejected. error.fields names each one. |
An unrecognised sort is the usual cause — they are a fixed list, not column names. |
| 429 |
rate_limited |
A rate window is spent. error.window is minute or day. |
Honour Retry-After. A daily refusal carries a much longer one than a burst refusal. |
| 429 |
too_many_attempts |
Too many failed authentications from this address. |
Stop retrying with a key that does not work. Clears after a minute. |
| 500 |
server_error |
Ours. |
Quote the X-Request-Id header when you report it. |
Endpoints
Everything is GET, relative to
https://fivestatus.com/api/data/v1. The scope each group needs is on its
heading.
Ecosystem
scope: ecosystem
FiveM as one set of numbers. Every one of these reads the census table — one small row per sample of the whole ecosystem — so they are cheap to poll.
GET
/api/data/v1/ecosystem
Players, servers and capacity right now, with the 24-hour movement.
The change figures compare against the nearest census to 24 hours before the latest one, within an hour either side. They are null rather than zero when there is no comparable point, so a gap in the series is never served as "no change". On an instance that has never run a census, data is null and meta.note says so — that is a 200, not an error.
Example response
{
"data": {
"captured_at": "2026-10-03T11:58:00+00:00",
"players_online": 184203,
"servers_online": 35912,
"servers_populated": 9841,
"slots_advertised": 4120338,
"fill_rate_percent": 4.47,
"regions_seen": 38,
"resources_distinct": 1284113,
"servers_empty": 26071,
"servers_full": 118,
"change_24h_percent": { "players": 3.1, "servers": -0.4, "servers_populated": 1.8 },
"sampled": "every 10 minutes"
},
"meta": { "as_of": "2026-10-03T11:58:00+00:00", "retrieved_at": "2026-10-03T12:00:04+00:00" }
}
GET
/api/data/v1/ecosystem/history
The players-and-servers curve, thinned for charting.
One point per census, downsampled to roughly the number of points you ask for. The maximum of each bucket is taken, not the average, so a peak survives the thinning — an averaged curve quietly erases the busiest hour of the week. meta.aggregation states this on every response.
| Parameter | Type | Default | Notes |
hours |
integer |
24 |
How far back to look. Capped at 336 (a fortnight). |
points |
integer |
180 |
How many points you want back. Capped at 2000. Fewer are returned when fewer exist. |
Example response
{
"data": [
{ "at": "2026-10-02T12:00:00+00:00", "players": 171402, "servers": 35740 },
{ "at": "2026-10-02T12:08:00+00:00", "players": 172880, "servers": 35751 }
],
"meta": { "hours": 24, "points": 180, "aggregation": "maximum per bucket", "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/ecosystem/daily
Peak, mean and trough per day.
A different question from the curve, not a longer version of it: this is the one to use for "is FiveM growing". Aggregated in the database, so a year is a few hundred rows rather than a million samples. samples is on every row because a day built from four censuses is not comparable with one built from a hundred and forty.
| Parameter | Type | Default | Notes |
days |
integer |
30 |
Capped at 365. |
Example response
{
"data": [
{
"date": "2026-10-02",
"players": { "peak": 201455, "average": 164820, "min": 98112 },
"servers": { "peak": 35980, "average": 35744 },
"samples": 144
}
],
"meta": { "days": 30, "as_of": "2026-10-03T12:00:00+00:00" }
}
Servers
scope: servers
The directory, and one server at a time. Filtering, sorting and paging all happen in the database — nothing here loads the list into memory to filter it.
GET
/api/data/v1/servers
Every listed server, filtered and sorted however you ask.
Offline servers are excluded unless you ask for them, because "servers on the list now" is almost always what is meant and a default that included every server ever seen would inflate any count drawn from this endpoint. The resource filter is the directory answering the question from the other direction: not what a server runs, but who runs a given thing.
| Parameter | Type | Default | Notes |
q |
string |
— |
Matches hostname, project name, or a join-code prefix. |
region |
string |
— |
Region code, as returned by /regions. A code, not a label. |
framework |
string |
— |
Framework slug, as returned by /frameworks. |
resource |
string |
— |
Only servers running this resource. Case-insensitive. |
locale |
string |
— |
The locale a server advertises, e.g. pt-BR. |
gametype |
string |
— |
Substring match on the advertised gametype. |
min_players |
integer |
— |
Servers with at least this many players. |
max_players |
integer |
— |
Servers with at most this many players. |
include_offline |
boolean |
false |
Include servers that have left the list. |
sort |
string |
players |
One of players, peak_24h, average_24h, upvotes, resources, max_players, first_seen, last_seen. Anything else is a 422. |
order |
string |
desc |
asc or desc. |
page |
integer |
1 |
|
per_page |
integer |
25 |
Capped at 100. A larger value is clamped, not refused, and meta.per_page reports what you actually got. |
Example response
{
"data": [
{
"join_code": "ab12cd",
"name": "Nightfall Roleplay",
"project_name": "Nightfall",
"players": 1284,
"max_players": 2048,
"fill_rate": 62.7,
"region": { "code": "BR", "name": "Brazil" },
"framework": { "slug": "esx", "label": "ESX" },
"gametype": "Roleplay",
"map": "San Andreas",
"build": "7290",
"locale": "pt-BR",
"resource_count": 412,
"upvotes": 9120,
"players_24h": { "peak": 1402, "average": 1104, "min": 612, "samples": 144, "changes": 139 },
"population": {
"verdict": "measured",
"label": "Moves like a real population",
"note": "The count changes across the day the way a server people join and leave does.",
"score": 81
},
"online": true,
"first_seen_at": "2026-03-02T08:11:00+00:00",
"last_seen_at": "2026-10-03T11:58:00+00:00",
"connect_url": "https://cfx.re/join/ab12cd",
"web_url": "https://fivestatus.com/fivem/servers/ab12cd"
}
],
"meta": {
"page": 1, "per_page": 25, "total": 35912, "pages": 1437, "has_more": true,
"sort": "players", "order": "desc",
"as_of": "2026-10-03T12:00:00+00:00"
}
}
GET
/api/data/v1/servers/{joinCode}
One server, with its full resource list and its place on the list.
Everything a directory row carries, plus the resource names, the advertised tags, and rank_by_players. The rank is null for an empty or delisted server, because a ranking among servers with no players would be a ranking of nothing. resources.established_at being null means we have not read that server's list yet — which is a different thing from a server that runs nothing.
GET
/api/data/v1/servers/{joinCode}/history
One server's population, hour by hour.
Hourly because that is how it is stored: the minute-by-minute ingest accumulates into the current hour's row. The mean is derived from the stored sum and sample count. meta.retention_days tells you how far back this server's history actually goes, and days is clamped to it rather than returning a short series labelled as a long one.
| Parameter | Type | Default | Notes |
days |
integer |
7 |
Clamped to the retention window reported in meta.retention_days. |
Example response
{
"data": [
{
"hour": "2026-10-03T11:00:00+00:00",
"players": { "average": 1104, "peak": 1402, "min": 980 },
"max_players": 2048,
"samples": 6
}
],
"meta": { "join_code": "ab12cd", "days": 7, "retention_days": 30, "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/servers/{joinCode}/resources
Just the resource list.
The same list /servers/{joinCode} returns, on its own, for walking the directory to build a dependency graph without re-reading everything else per server. Names are lowercased and sorted, so two reads of the same server compare equal.
GET
/api/data/v1/servers/{joinCode}/standing
Rank, percentile, peer comparison, busiest hour and listing consistency.
Its own endpoint because every figure in it is an aggregate over the rest of the list, and a caller walking the directory should not pay for them per row. peers compares against the median of servers in the same region and framework — the population is heavily skewed, and a mean would tell almost every server it is below average; it is null for a peer group under five. listed is how often the server was advertising itself on the Cfx list, not uptime — a server can stop listing and keep running perfectly well.
Example response
{
"data": {
"join_code": "ab12cd",
"standing": {
"players": { "rank": 41, "out_of": 9841, "percentile": 99.6, "change_7d": 3, "change_30d": -2,
"rank_7d_ago": 44, "rank_30d_ago": 39 },
"listed": 9841
},
"peers": { "peers": 412, "median_players": 38, "multiple": 33.8, "percentile": 99 },
"listed": { "hours_seen": 712, "hours_possible": 720, "percent": 98.9, "measured_days": 30, "partial": false },
"busiest_hour": { "utc": 0, "local": 21, "region_offset": "UTC−3", "quietest_utc": 7, "hours_observed": 336 }
},
"meta": { "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/servers/{joinCode}/daily
Daily peak, mean and rank.
Ninety points rather than the 2,160 the hourly history would be for the same window. The rank is recorded, not derived — it cannot be recomputed later because the field it was measured against has moved since.
| Parameter | Type | Default | Notes |
days |
integer |
90 |
Capped at the retention window (two years). |
Example response
{
"data": [
{ "date": "2026-10-02", "rank": 44, "peak": 1402, "average": 1104 },
{ "date": "2026-10-03", "rank": 41, "peak": 1511, "average": 1180 }
],
"meta": { "join_code": "ab12cd", "days": 90, "points": 90, "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/servers/{joinCode}/changes
What this server has installed and removed.
Grouped by the moment it happened, with installs and removals as separate arrays — a restart that swaps a framework produces both at the same timestamp, and the pairing is the interesting part.
| Parameter | Type | Default | Notes |
per_page |
integer |
25 |
How many moments to return. Capped at 100. |
Example response
{
"data": [
{
"detected_at": "2026-10-01T04:22:00+00:00",
"installed": ["ox_inventory", "ox_lib"],
"removed": ["qb-inventory"]
}
],
"meta": { "join_code": "ab12cd", "count": 1, "as_of": "2026-10-03T12:00:00+00:00" }
}
Resources
scope: resources
Which resources the industry runs, how far each one reaches, and exactly which servers run it. Only resources above a noise floor are tracked: over a million distinct names exist across the list and nearly all of them are one community's private scripts.
GET
/api/data/v1/resources
Every tracked resource, searchable.
The search is a containment match, not a prefix: q=inventory finds ox_inventory and qb-inventory. Both reach numbers are on every row, because a script on fifty thousand empty servers and one on five thousand busy ones are popular in completely different ways and a response carrying only one of them lets you draw the wrong chart without noticing.
| Parameter | Type | Default | Notes |
q |
string |
— |
Matches anywhere in the name. Case-insensitive. |
library |
boolean |
— |
Filter to, or away from, known libraries. |
min_servers |
integer |
— |
Only resources installed on at least this many servers. |
sort |
string |
players_reached |
One of players_reached, servers, name, first_seen. |
order |
string |
desc (asc for name) |
asc or desc. |
page |
integer |
1 |
|
per_page |
integer |
25 |
Capped at 100. |
Example response
{
"data": [
{
"name": "ox_lib",
"servers": 21480,
"players_reached": 142309,
"is_library": true,
"first_seen_at": "2026-01-14T00:00:00+00:00",
"last_seen_at": "2026-10-03T11:58:00+00:00",
"web_url": "https://fivestatus.com/fivem/resources/ox_lib"
}
],
"meta": {
"page": 1, "per_page": 25, "total": 31204, "pages": 1249, "has_more": true,
"sort": "players_reached", "order": "desc",
"tracking_note": "Only resources running on more than a handful of servers are tracked. Private scripts are not listed.",
"as_of": "2026-10-03T12:00:00+00:00"
}
}
GET
/api/data/v1/resources/{name}
One resource: reach, share of the industry, and which way it is moving.
share is worked out against the live totals and carries its own denominators, so the percentages are checkable rather than asserted — "on 8,000 servers" means nothing without knowing whether that is a fifth of FiveM or a fiftieth. momentum is the part a snapshot cannot give you: a resource on 8,000 servers looks identical whether it arrived on 800 of them this week or lost 800.
Example response
{
"data": {
"name": "ox_inventory",
"servers": 9084,
"players_reached": 61422,
"is_library": false,
"first_seen_at": "2026-02-03T00:00:00+00:00",
"last_seen_at": "2026-10-03T11:58:00+00:00",
"web_url": "https://fivestatus.com/fivem/resources/ox_inventory",
"share": {
"of_servers_percent": 25.3,
"of_players_percent": 33.4,
"servers_online": 35912,
"players_online": 184203
},
"momentum": { "net_7d": 318, "net_30d": 1204, "installs_30d": 1601, "removals_30d": 397 }
},
"meta": { "as_of": "2026-10-03T12:00:00+00:00", "retrieved_at": "2026-10-03T12:00:01+00:00" }
}
GET
/api/data/v1/resources/{name}/servers
Which servers run it — full server rows, paginated.
Each row is the same server object the directory returns, so you can go from "who runs this" to the population, region, framework and 24-hour shape of every one of them without a request per server. meta.totals covers the whole filtered match, not the page in hand — that is the number most callers actually want, and reading it off a page would be wrong by however many pages there are.
| Parameter | Type | Default | Notes |
region |
string |
— |
Narrow to one region code. |
framework |
string |
— |
Narrow to one framework slug. |
min_players |
integer |
— |
Only servers with at least this many players. |
sort |
string |
players |
One of players, peak_24h, upvotes, resources. |
order |
string |
desc |
asc or desc. |
page |
integer |
1 |
|
per_page |
integer |
25 |
Capped at 100. |
Example response
{
"data": [
{ "join_code": "ab12cd", "name": "Nightfall Roleplay", "players": 1284, "…": "full server object" }
],
"meta": {
"page": 1, "per_page": 25, "total": 9084, "pages": 364, "has_more": true,
"resource": "ox_inventory",
"sort": "players", "order": "desc",
"totals": { "servers": 9084, "players": 61422, "slots_advertised": 912440 },
"as_of": "2026-10-03T12:00:00+00:00"
}
}
GET
/api/data/v1/resources/{name}/history
Installs and removals per day.
Zero-filled, because a gap in a series and a day on which nothing happened look identical on a chart and mean opposite things.
| Parameter | Type | Default | Notes |
days |
integer |
30 |
Capped at 180. |
Example response
{
"data": [
{ "date": "2026-10-01", "installs": 61, "removals": 14, "net": 47 },
{ "date": "2026-10-02", "installs": 44, "removals": 19, "net": 25 }
],
"meta": { "resource": "ox_inventory", "days": 30, "net_7d": 318, "net_30d": 1204, "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/resources/{name}/standing
Where it ranks, and which way the rank is moving.
Rank by servers and by players reached, each with a percentile and the places gained over 7 and 30 days. A positive change is an improvement even though the rank number went down. The changes are null, never zero, when there is no recording from that far back — zero means measured and unmoved, and the two are different claims.
Example response
{
"data": {
"servers": {
"rank": 36, "out_of": 31204, "percentile": 99.9,
"change_7d": 0, "change_30d": 9,
"rank_7d_ago": 36, "rank_30d_ago": 45
},
"players": {
"rank": 166, "out_of": 31204, "percentile": 99.5,
"change_7d": 2, "change_30d": -4,
"rank_7d_ago": 168, "rank_30d_ago": 162
},
"tracked": 31204
},
"meta": { "resource": "ox_inventory", "recorded_days": 94, "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/resources/{name}/adoption
How many servers ran it, day by day.
The absolute level over time, which is a different series from /history — that one is installs and removals, recorded as events, going back as far as the change log. This is recorded once a day and only exists from the day the recording started: a resource's adoption count is overwritten on every census pass, so there is no version of it from before that to recover. meta.recording_since tells you when that was. Days with no recording are absent rather than zero, so leave a gap rather than drawing a cliff.
| Parameter | Type | Default | Notes |
days |
integer |
90 |
Capped at the retention window (two years). |
Example response
{
"data": [
{ "date": "2026-10-01", "servers": 9040, "players": 60880, "rank_servers": 37, "rank_players": 168 },
{ "date": "2026-10-02", "servers": 9062, "players": 61105, "rank_servers": 36, "rank_players": 166 }
],
"meta": {
"resource": "ox_inventory", "days": 90, "points": 90,
"recording_since": "2026-07-01", "recorded_days": 94,
"gaps": "Days with no recording are absent rather than zero.",
"as_of": "2026-10-03T12:00:00+00:00"
}
}
GET
/api/data/v1/resources/{name}/related
What else is installed alongside it.
Co-occurrence over a sample — the busiest hundred servers running the resource — and the response says so in meta.sample_servers and meta.method. The honest version of this query unnests every server running a popular library and groups several million rows to produce a top ten; the sample gives the same answer for one indexed fetch.
| Parameter | Type | Default | Notes |
limit |
integer |
10 |
How many co-occurring names to return. Capped at 50. |
Example response
{
"data": [
{ "name": "ox_lib", "servers_in_sample": 98, "share_of_sample_percent": 98.0 },
{ "name": "oxmysql", "servers_in_sample": 95, "share_of_sample_percent": 95.0 }
],
"meta": {
"resource": "ox_inventory",
"sample_servers": 100,
"method": "Co-occurrence across the busiest servers running this resource, not the full population.",
"as_of": "2026-10-03T12:00:00+00:00"
}
}
GET
/api/data/v1/resources/newcomers
Resources first tracked recently.
What is new rather than what is big. "First seen" means first seen above the tracking floor, which is not the same as first released — meta.note repeats that, because it is the one easy misreading here.
| Parameter | Type | Default | Notes |
days |
integer |
14 |
Capped at 90. |
per_page |
integer |
25 |
Capped at 100. |
Changes
scope: changes
What the industry is installing and removing. The only part of the API about movement rather than state — and the only part that cannot be rebuilt from a fresh census, because the transitions only exist because they were recorded as they happened.
GET
/api/data/v1/changes
The feed, newest first.
Grouped by server and moment by default, so a server restarting with a new framework reads as one recognisable migration rather than twenty-four unrelated events. Pass resource and the shape changes to one entry per event — the grouping is noise once the resource is fixed, and meta.grouping tells you which shape you have.
| Parameter | Type | Default | Notes |
hours |
integer |
72 |
Capped at 720 (30 days). |
resource |
string |
— |
Narrow to one resource. Switches the response to one entry per event, paginated. |
action |
string |
— |
installed or removed. Only meaningful with resource. |
page |
integer |
1 |
Only with resource. |
per_page |
integer |
25 |
Capped at 100. |
Example response
{
"data": [
{
"detected_at": "2026-10-03T04:22:00+00:00",
"installed": ["ox_inventory", "ox_lib"],
"removed": ["qb-inventory"],
"server": { "join_code": "ab12cd", "name": "Nightfall Roleplay", "players": 1284, "region": "BR" }
}
],
"meta": { "hours": 72, "count": 25, "grouping": "one entry per server per moment", "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/changes/movers
What is gaining and losing ground.
Installs and removals are always reported beside the net, never instead of it: "+641" turns out to be 641 installs and no removals on one reading and 4,000 against 3,359 on another, and those are completely different situations. A resource that moved on fewer than three servers is excluded as noise — meta.minimum_servers_moved states the floor so these counts can be reconciled against the raw feed.
| Parameter | Type | Default | Notes |
direction |
string |
both |
up, down or both. |
hours |
integer |
72 |
Capped at 720. |
per_page |
integer |
25 |
How many per direction. Capped at 100. |
Example response
{
"data": {
"gaining": [
{
"name": "ox_inventory",
"installs": 681, "removals": 40, "net": 641,
"players_gained": 190114,
"servers": 9084,
"web_url": "https://fivestatus.com/fivem/resources/ox_inventory"
}
],
"losing": [
{ "name": "qb-inventory", "installs": 22, "removals": 410, "net": -388, "players_gained": 1140, "servers": 4012, "web_url": "…" }
]
},
"meta": { "hours": 72, "minimum_servers_moved": 3, "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/changes/summary
Headline figures for the window, and for everything recorded.
Both, because a total with no start date is meaningless and a windowed figure alone cannot tell you whether the dataset is a fortnight or a year deep.
| Parameter | Type | Default | Notes |
hours |
integer |
72 |
Capped at 720. |
Example response
{
"data": {
"window": {
"hours": 72, "changes": 14208, "installs": 9140, "removals": 5068,
"servers_changed": 4011, "resources_moved": 2284
},
"all_time": { "changes": 812044, "recording_since": "2026-08-16T00:00:00+00:00" }
},
"meta": { "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/changes/migrations
Which framework servers are actually moving between.
The direction adoption counts cannot show. A framework holding steady at eight thousand servers looks identical whether it is stable or losing four hundred a month and replacing them. The signal is a server dropping its old core and adding the new one in the same restart, which puts both rows in the change log at the same instant.
These are a floor, not a total: a server that rebuilds across several restarts, or one that was never on the list before, is not counted. A server carrying both qbx_core and qb-core counts as Qbox, the same precedence used everywhere else.
| Parameter | Type | Default | Notes |
days |
integer |
30 |
Capped at the change-log retention window. |
per_page |
integer |
25 |
How many flows to return. Capped at 100. |
Example response
{
"data": {
"flows": [
{ "from": { "slug": "qbcore", "label": "QBCore" }, "to": { "slug": "qbox", "label": "Qbox" },
"servers": 412, "players": 38100 }
],
"net": [
{ "slug": "qbox", "label": "Qbox", "arrived": 412, "left": 31, "net": 381, "players_arrived": 38100 }
],
"adopted_without_replacing": [ { "slug": "ox_core", "label": "Ox Core", "servers": 22, "players": 900 } ],
"dropped_without_replacing": [ { "slug": "esx", "label": "ESX", "servers": 14, "players": 410 } ],
"recent": [
{ "join_code": "ab12cd", "hostname": "Nightfall Roleplay", "players": 1284,
"from": { "slug": "qbcore", "label": "QBCore" }, "to": { "slug": "qbox", "label": "Qbox" },
"at": "2026-10-01T04:22:00+00:00" }
]
},
"meta": {
"days": 30, "moves": 588, "capped": false,
"basis": "A core removed and another added in the same restart…",
"as_of": "2026-10-03T12:00:00+00:00"
}
}
GET
/api/data/v1/changes/churn
Change volume per hour.
Zero-filled for the same reason the daily series is.
| Parameter | Type | Default | Notes |
hours |
integer |
72 |
Capped at 336. |
Regions and frameworks
scope: segments
The two ways the list divides up. Snapshots by default, with history available for either.
GET
/api/data/v1/regions
Players and servers by advertised region.
"Advertised" is the operative word: the region is what a server publishes about itself, not a geolocation of its address. A server can advertise nothing, so no region set is a row here rather than an omission — dropping it would make the figures add up to less than the list and leave you hunting for the difference.
Example response
{
"data": [
{ "code": "BR", "name": "Brazil", "servers": 6841, "players": 48120, "web_url": "https://fivestatus.com/fivem/servers?region=BR" },
{ "code": "unset", "name": "No region set", "servers": 1204, "players": 3390, "web_url": null }
],
"meta": { "count": 38, "basis": "The region each server advertises on the list, not a geolocation of its address.", "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/frameworks
Servers and players by detected framework.
Detected, not declared — nothing on the server list says which framework a server runs, so it is inferred from the resource list. A server whose list we have not established yet is counted as not advertised rather than guessed at. meta.vocabulary returns every slug the detector can produce, so you can build a filter without discovering the values by observation.
Example response
{
"data": [
{ "slug": "esx", "label": "ESX", "servers": 11204, "players": 72019 },
{ "slug": "qbcore", "label": "QBCore", "servers": 8140, "players": 51330 }
],
"meta": {
"count": 7,
"vocabulary": [ { "slug": "esx", "label": "ESX" }, { "slug": "qbox", "label": "Qbox" } ],
"basis": "Inferred from each server's resource list…",
"as_of": "2026-10-03T12:00:00+00:00"
}
}
GET
/api/data/v1/regions/busy-times
When each region plays, in its own clock.
The one endpoint here that returns anything but UTC, and deliberately: "Thailand peaks at 15:00" is true and useless, "Thailand peaks at 22:00 local" is the same fact in a form you can act on. Both are returned. For a country spanning several timezones the local figure uses the offset most of that country's population keeps — Brasília for Brazil, Eastern for the US and Canada, Moscow for Russia — so those rows are approximate at the edges, and region_offset says which offset was applied. swing is peak divided by trough: high means a clear evening, near 1 means a region that never really sleeps.
| Parameter | Type | Default | Notes |
days |
integer |
14 |
Capped at 90. |
Example response
{
"data": [
{
"code": "BR", "name": "Brazil", "players_average": 23373,
"peak": { "hour_utc": 0, "hour_local": 21, "players": 41980 },
"quiet": { "hour_utc": 10, "hour_local": 7, "players": 10402 },
"region_offset": "UTC−3",
"swing": 4.0,
"players_by_hour_utc": [ 41980, 38110, "… 24 values" ],
"players_by_hour_local": [ 12400, 11010, "… 24 values, re-indexed to local time" ]
}
],
"meta": {
"days": 14,
"global_peak": { "hour_utc": 20, "players": 210405, "days": 14 },
"basis": "Averaged per hour of day across the window…",
"as_of": "2026-10-03T12:00:00+00:00"
}
}
GET
/api/data/v1/regions/growth
Which regions are growing and which are not.
Compares the window against the window before it, not today against a day in the past: one day is mostly weather — a single large server going offline moves a mid-sized region by several per cent. Regions below min_players are excluded, because three players becoming six is not 100% growth.
| Parameter | Type | Default | Notes |
days |
integer |
7 |
Window length, and the length of the comparison window before it. Capped at 90. |
min_players |
integer |
50 |
Exclude regions averaging fewer than this. |
Example response
{
"data": [
{
"code": "TH", "name": "Thailand",
"players": 4120, "players_before": 3790, "players_change_percent": 8.7,
"servers": 310, "servers_change_percent": 2.1,
"window_days": 7
}
],
"meta": { "days": 7, "minimum_players": 50, "basis": "Mean players over the window against the mean over the window before it.", "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/regions/frameworks
Which frameworks each region runs.
Framework adoption is usually one global league table, and that table is mostly a statement about Brazil — which carries more players than the next several regions together. Split by region it becomes the more useful observation that different parts of the industry settled on different things.
| Parameter | Type | Default | Notes |
limit |
integer |
10 |
How many regions, busiest first. Capped at 50. |
Example response
{
"data": [
{
"code": "BR", "name": "Brazil", "servers": 6841, "players": 48120,
"frameworks": [
{ "slug": "esx", "label": "ESX", "servers": 3100, "players": 24000, "share_percent": 45.3 },
{ "slug": "qbcore", "label": "QBCore", "servers": 2010, "players": 15400, "share_percent": 29.4 }
]
}
],
"meta": { "regions": 10, "basis": "Current servers, grouped by region and detected framework.", "as_of": "2026-10-03T12:00:00+00:00" }
}
GET
/api/data/v1/segments/history
One region or framework over time.
This is what makes "is Qbox taking share from QBCore" answerable rather than only "how much does each have today". segment takes a code or slug, not a label — an empty series for a segment that plainly exists is almost always that mistake, and meta.note says so when it happens.
| Parameter | Type | Default | Notes |
kind |
string |
— (required) |
region or framework. |
segment |
string |
— (required) |
The code or slug, e.g. BR or qbcore. |
hours |
integer |
168 |
Capped at 1440 (60 days). |
Example response
{
"data": [
{ "hour": "2026-10-03T11:00:00+00:00", "servers": 8140, "players": 51330, "samples": 6 }
],
"meta": { "kind": "framework", "segment": "qbcore", "hours": 168, "note": null, "as_of": "2026-10-03T12:00:00+00:00" }
}
Worked examples
How much of FiveM runs a given resource
One request. share carries its own denominators, so the percentage
is checkable rather than asserted, and momentum tells you which way
it is going.
curl -s "https://fivestatus.com/api/data/v1/resources/ox_inventory" \
-H "Authorization: Bearer $FIVESTATUS_KEY" \
| jq '{servers: .data.servers,
players: .data.players_reached,
share: .data.share.of_servers_percent,
net_30d: .data.momentum.net_30d}'
Every server running it, and what they look like
meta.totals is the whole match, not this page — use it for the
headline figure and page through data for the rows.
curl -s "https://fivestatus.com/api/data/v1/resources/ox_inventory/servers?per_page=100&sort=players" \
-H "Authorization: Bearer $FIVESTATUS_KEY" \
| jq '.meta.totals, (.data[] | {name, players, region: .region.name, framework: .framework.label})'
Paging through a whole list
page=1
while : ; do
body=$(curl -s "https://fivestatus.com/api/data/v1/servers?per_page=100&page=$page" \
-H "Authorization: Bearer $FIVESTATUS_KEY")
echo "$body" | jq -r '.data[] | [.join_code, .players] | @tsv'
[ "$(echo "$body" | jq -r '.meta.has_more')" = "true" ] || break
page=$((page + 1))
# Stay inside the burst window rather than discovering it with a 429.
sleep 0.5
done
Is one framework taking share from another
for fw in qbcore qbox ; do
curl -s "https://fivestatus.com/api/data/v1/segments/history?kind=framework&segment=$fw&hours=720" \
-H "Authorization: Bearer $FIVESTATUS_KEY" \
| jq --arg fw "$fw" '{framework: $fw,
first: .data[0].servers,
last: .data[-1].servers}'
done
Handling a 429 properly
# Read the window, not just the status. A daily refusal does not clear
# for hours, and retrying every second until midnight helps nobody.
response=$(curl -s -w '\n%{http_code}' "https://fivestatus.com/api/data/v1/servers" \
-H "Authorization: Bearer $FIVESTATUS_KEY" -D /tmp/h)
if [ "$(echo "$response" | tail -1)" = "429" ]; then
window=$(echo "$response" | head -n -1 | jq -r '.error.window')
wait=$(grep -i '^retry-after:' /tmp/h | tr -dc '0-9')
echo "rate limited on the $window window; sleeping ${wait}s"
sleep "$wait"
fi
Fair use & attribution
Where the data comes from
All of it is derived from the public Cfx.re master server list, sampled
continuously, plus per-server resource lists fetched from the same public API.
None of it comes from the server owners, and none of them have a relationship
with us — the overwhelming majority have never heard of FiveStatus. Please
describe it the way we do: a sample of a public list, not a directory anyone
opted into.
Describing a server's population
population.verdict describes how a number behaves, not
whether anybody is lying. static means the count did not change
across a day; it does not mean the players are fake, and rendering it as "fake"
is a defamation claim with our name attached to it. The note on
every verdict is the wording to use — it travels in the payload for exactly this
reason.
Attribution
If you publish figures from this API, credit FiveStatus with a link to
https://fivestatus.com/fivem. Every server and
resource object carries a web_url pointing at the public page for
it, which is the easiest way to do it.
What will get a key suspended
- Re-publishing the dataset wholesale as a competing directory.
- Working around the rate limits with multiple keys rather than asking for a higher tier.
- Presenting the population verdicts as accusations.
- Sharing a key outside the organisation it was issued to.
None of those is a trap — if you are unsure whether what you are building is
fine, ask first and we will tell you. We would far rather raise your limits than
switch you off.
Versioning
The path carries v1. Within it we will add fields and endpoints,
and we will not remove or repurpose a field or rename an
error.code. So parse defensively: ignore fields you do not know
rather than failing on them.