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.

ScopeCovers
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.

TierPer minutePer dayWho 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:

HeaderMeaning
X-RateLimit-LimitRequests allowed this minute.
X-RateLimit-RemainingHow many of those are left.
X-RateLimit-ResetSeconds until the minute window resets.
X-RateLimit-Limit-DayRequests allowed today.
X-RateLimit-Remaining-DayHow many of those are left.
X-RateLimit-Reset-DaySeconds until midnight UTC.
Retry-AfterOn 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:

StateErrorWhat 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"
  }
}
StatusCodeMeaningWhat 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.

Your key

no scope needed

What your key is, what it may read, and how much of your quota is left. The one endpoint that needs no scope — a key has to be able to discover its own permissions.

GET /api/data/v1/meta

Describe the calling key.

Call this first. It answers "which key is this deployment actually using", which is the most common question when a staging box has the wrong one, and it reports both rate-limit windows so you can size a job before running it.

Example response
{
  "data": {
    "key": {
      "prefix": "k3f9a2b7c1",
      "name": "Guildbase production",
      "tier": "partner",
      "tier_label": "Partner",
      "issued_at": "2026-09-01T10:14:22+00:00",
      "expires_at": null,
      "source_restricted": true
    },
    "scopes": [
      { "scope": "servers", "label": "Servers", "note": "The server directory, individual servers, and their player history." }
    ],
    "limits": {
      "per_minute": 600,
      "remaining_this_minute": 599,
      "per_day": 500000,
      "remaining_today": 497310,
      "day_resets_in_seconds": 19842,
      "day_resets_at": "midnight UTC"
    },
    "pagination": { "default_per_page": 25, "max_per_page": 100 },
    "docs_url": "https://fivestatus.com/data-docs"
  },
  "meta": { "as_of": "2026-10-03T12:00:00+00:00", "retrieved_at": "2026-10-03T12:00:00+00:00" }
}

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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.

ParameterTypeDefaultNotes
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.