API
All endpoints live under /api/v1/*. Read endpoints are GET, collector endpoints are POST. JSON in, JSON out. Errors come back as { "error": "..." } with an appropriate HTTP status.
Every meaningful change to a route, query parameter, or response shape must also land in CHANGELOG.md.
Read endpoints
GET /api/v1/player-count
Live concurrent player counts. helldiversActive is the cross-platform live diver count from ArrowHead WarStatus (sum of planet players), the same number the homepage hero shows. Steam HD1 / HD2 totals are still returned for any external consumer that wants them. PS5 stays 0 (no public live source).
- Cache:
cache-control: no-store,dynamic = "force-dynamic". - Response:
{ "capturedAt": "ISO-8601", "helldiversActive": 213225, "helldivers1": { "steam": 0, "ps5": 0, "total": 0 }, "helldivers2": { "steam": 0, "ps5": 0, "total": 0 }, "all": { "steam": 0, "ps5": 0, "total": 0 }, "sources": { "helldivers1": "steam|unavailable", "helldivers2": "steam|unavailable", "helldiversActive": "arrowhead|unavailable" } }
GET /api/v1/divers/history?range=<24h|7d|30d|90d|all>
Time-bucketed diver-count series for the /divers chart. Picks the best upstream source per range (15s ArrowHead for short windows, 10-min Steam for long windows where ArrowHead doesn't have history yet) and bucket-aggregates to ~200 points so the client doesn't deal with thousands of samples.
- Cache:
no-store,force-dynamic. - 400 on invalid range.
- Response:
{ "range": "24h", "source": "api" | "steam", "bucketSeconds": 300, "from": "ISO-8601", "to": "ISO-8601", "points": [{ "t": "ISO-8601", "v": 56419 }], "stats": { "current": 56419, "peak": { "value": 63102, "at": "ISO-8601" }, "low": { "value": 41877, "at": "ISO-8601" }, "avg": 54920 } }
GET /api/v1/war
Current war (the latest listOfWars.warID) plus the joined planet war info for that war. planetName and sectorName come from listOfPlanets via LEFT JOIN.
- Cache:
revalidate = 30(planet war info is refreshed every minute by cron). - 404 with
{ "error": "no war records yet" }if the DB has never been seeded. - Response:
{ "war": { "warID": 801, "...": "..." }, "planets": [ { "indexID": 0, "warID": 801, "owner": 1, "health": 1000000, "planetName": "Super Earth", "sectorName": "Sol" }, "..." ] }
GET /api/v1/war/[warId]
Same shape as /api/v1/war but for an explicit warId. Optional ?planetID= filters to one planet.
- Cache:
revalidate = 30. - 400 on invalid
warId/planetID. 404 if the war is unknown.
GET /api/v1/planets
Full listOfPlanets lookup table (name, sector name, base/coords) for every war ever stored. Static data; cache hard.
- Cache:
revalidate = 3600.
GET /api/v1/sectors
Full listOfSectors lookup table. Static data.
- Cache:
revalidate = 3600.
GET /api/v1/war-stats
Fresh galaxy-wide cumulative stats (kills per faction, K.I.A., bullets, missions, time) sourced from ArrowHead's /Stats/War/<warId>/Summary. This is the endpoint the /war page polls every 10s to drive the live-ticking GALAXY STATISTICS panel.
- Cache:
cache-control: no-store,dynamic = "force-dynamic". Underlying upstream fetch is throttled by the hd2-live client (15s baseline + auto-backoff). - Response:
{ "at": "ISO-8601", "stats": { "missionsWon": 0, "missionsLost": 0, "missionTime": 0, "bugKills": 0, "automatonKills": 0, "illuminateKills": 0, "bulletsFired": 0, "bulletsHit": 0, "timePlayed": 0, "deaths": 0, "revives": 0, "friendlies": 0, "missionSuccessRate": 0, "accuracy": 0 } } - 503 with
{ "error": "..." }if the live API is unreachable for this tick.
GET /api/v1/factions
Full listOfFactions lookup table. Static data.
- Cache:
revalidate = 3600.
GET /api/v1/historical-player-count
Time-series of player-count snapshots from historicalPlayerCount.
- Required:
startDate,endDate(YYYY-MM-DDorYYYY-MM-DD HH:MM:SS). - Optional:
gameName("Helldivers"|"Helldivers 2"),page(default 1),limit(default 50, max 500). - 400 if
startDate/endDateare missing. - Rows have one of two shapes: Steam-collector rows fill
steamPlayerCount / PS5PlayerCount / totalPlayerCount(~10 min cadence); ArrowHead-collector rows fillapiTotalPlayerCount(~15s cadence, deduped byWarStatus.time). Both share the samegameNamecolumn. Filter withWHERE apiTotalPlayerCount IS NOT NULLfor the high-frequency cross-platform series, orWHERE steamPlayerCount IS NOT NULLfor the Steam history. - Response:
{ "items": [ "..." ], "page": 1, "limit": 50, "totalResults": 1234, "totalPages": 25 }
GET /api/v1/planet/[planetId]/history
Per-planet liberation series powering the /war inline PlanetStats panel and the /planet/[planetId] detail page. Pulls from historicalPlanetStatus.
- Optional:
warId(defaults to envWAR_IDor 801),hours(default 24, max 720 = 30 days). - 400 on invalid planet or war id.
- Response:
{ "warId": 801, "planetIndex": 168, "maxHealth": 1000000, "from": "ISO-8601", "to": "ISO-8601", "points": [ { "t": "ISO-8601", "libPct": 67.3, "players": 1820, "owner": 2, "health": 327892, "regenPerSecond": 1250 } ] }
GET /api/v1/historical-planet-status
Per-planet snapshot time-series written by the fullStatus collector.
- Required:
planetId(aliasplanetID). - Optional:
warId(aliaswarID),startDate,endDate,page(default 1),limit(default 100, max 1000). - Default range when
startDateis omitted: last 24 hours. - Response columns:
warID, planetIndex, owner, health, regenPerSecond, players, timestamp.
GET /api/v1/historical-war-summary
Galaxy-wide cumulative stats over time (one row per fullStatus tick).
- Optional:
warId,startDate,endDate,page,limit(default 100, max 1000). - Default range when
startDateis omitted: last 7 days. - Important: values are CUMULATIVE since war start. Subtract consecutive rows to get per-tick deltas (kills/hr, deaths/hr, etc.). This is how the homepage Kill Telemetry computes its real per-hour rates.
- Response columns:
warID, bugKills, automatonKills, illuminateKills, deaths, missionsWon, missionsLost, missionTime, bulletsFired, bulletsHit, timePlayed, revives, friendlies, timestamp.
GET /api/v1/historical-major-orders
Lifecycle list (one row per orderId32) of every major order seen on this war. Combine lastSeenAt + expiresInLastSeen to decide if an order is still active.
- Optional:
warId,page,limit(default 50, max 500). - Sorted by
lastSeenAt DESC. - Each row also has a
tasksJSON column (added in migration 004) carrying the raw ArrowHeadsetting.tasksarray:Array<{ type, values, valueTypes }>. Older rows captured before the migration may havetasks = NULL. - And a
rewardsJSON column (added in migration 005) carrying the raw ArrowHeadsetting.rewardsarray:Array<{ type, id32, amount }>. TherewardMedals INTcolumn is now only populated when a reward entry matches the medals heuristic (type=1, amount>=2); a unique-item drop (cape etc, whereamount===1) leavesrewardMedals=0. - This is the raw row shape. The
/major-ordersUI consumes the same table throughgetMajorOrderArchive()insrc/lib/data.ts, which adds derived fields (status,tasksDone,expiresAt) on top.
GET /api/v1/historical-major-orders/[orderId]/progress
Per-order progress time-series. progress is the raw ArrowHead int[] (one slot per task, non-zero = done).
- Optional:
startDate,endDate,page,limit(default 200, max 2000). - The
/major-orders/[orderId]UI consumes the same table throughgetMajorOrderDetail()insrc/lib/data.ts, which addscompletedAt(first snapshot where every task was done).
GET /api/v1/historical-dispatches
Archive of every dispatch ever captured.
- Optional:
warId,since(war-seconds),q(substring match againstmessage),page,limit(default 50, max 500). - Sorted by
publishedAt DESC(newest first).
Health endpoints
GET /api/v1/health/throttle
Snapshot of the in-process cache + auto-backoff state for the live ArrowHead client (src/lib/sources/hd2-live.ts). Each endpoint (status, summary, assignment, newsfeed, warinfo) starts at a 15s cache TTL. On HTTP 429 the TTL bumps by 15s, capped at 300s, and never decreases.
- Cache:
no-store. - Response:
{ "config": { "initialTtlS": 15, "backoffStepS": 15, "maxTtlS": 300 }, "endpoints": [ { "endpoint": "status", "ttlS": 15, "lastBumpAt": null, "lastSuccessAt": 1717000000000, "hits": 41, "misses": 7, "ok": 7, "rateLimited": 0, "errors": 0, "hitRate": 0.854 } ] } - Counters reset on Node process restart.
Collector endpoints
All collector endpoints are POST, dynamic = "force-dynamic". When COLLECTOR_SECRET is set in the env, the request must include x-collector-secret: <value> or it returns 401. Without the env var the routes are open (dev only).
Use these for: smoke-testing after deploy, backfilling a missed window, or running from Plesk Scheduled Tasks instead of the in-process node-cron.
POST /api/v1/collect/player-count
Runs the player-count collector once. Writes to historicalPlayerCount.
POST /api/v1/collect/war-info[?warId=...]
Runs the war-info collector once. Defaults to WAR_ID env (or 801). Pass ?warId= to force a season. Writes to listOfWars, listOfPlanets, listOfSectors, listOfFactions, listOfPlanetWarInfo.
POST /api/v1/collect/full-status[?warId=...]
Runs the full ArrowHead archival snapshot. Writes one tick to: historicalPlanetStatus, historicalWarSummary, historicalMajorOrders + historicalMajorOrderProgress, historicalDispatches.
This is the collector the homepage Kill Telemetry depends on for trend deltas.
POST /api/v1/collect/planet-stats[?warId=...]
Runs the per-planet statistics collector once. Polls api.helldivers2.dev/api/v1/planets (the one sanctioned community-API runtime dependency) and writes a row to historicalPlanetStats for every planet whose cumulative counters moved since the prior snapshot. Dormant planets are silent. Scheduled via in-process cron at */5 * * * * (every 5 minutes).
Conventions
- Errors:
4xxare validation ({ "error": "..." }),503is unavailable (DB / upstream down — wrapped byserviceUnavailable(err)insrc/lib/api.ts). - Pagination: any endpoint with a
page/limitquery also returnstotalResultsandtotalPages.limitis clamped per-endpoint. - Dates: all query params accept
YYYY-MM-DDorYYYY-MM-DD HH:MM:SS(MySQL-friendly). Responses always serializeDateas ISO-8601. - War ID aliases: routes accept both
warIdandwarIDfor forgiveness with legacy callers. - Caching: lookup tables (
planets,sectors,factions) cache 1h; war state caches 30s; historical / live / collector routes areno-storeorforce-dynamic.