WIPWork in progress. New features arriving daily. Expect broken pages and rough edges.
BULLETIN
MAJOR ORDER · MAJOR ORDER · 3D 14H REMAINING 46K DIVERS DEPLOYED · PEAK 52K TODAYSUPER EARTH HOLDS 74% OF THE GALAXY · DAY 870 OF WAR 801TERMINID SPORES HAVE ENGULFED HEETH AND ANGEL'S VENTURE, SPAWNING HORDES OF TERMINIDS THAT OVERWHELMED OUR COLONIAL MILITIAS. CLEARLY, THE BNEW MAJOR ORDER TIEN KWAN IS HOME TO THE SOLE ARSENAL OF NEW EXOSUIT TECHNOLOGY. IT IS ONLY A MATTER OF TIME BEFORE THE AUTOMATONS DISCOVER MAJOR ORDER · MAJOR ORDER · 3D 14H REMAINING 46K DIVERS DEPLOYED · PEAK 52K TODAYSUPER EARTH HOLDS 74% OF THE GALAXY · DAY 870 OF WAR 801TERMINID SPORES HAVE ENGULFED HEETH AND ANGEL'S VENTURE, SPAWNING HORDES OF TERMINIDS THAT OVERWHELMED OUR COLONIAL MILITIAS. CLEARLY, THE BNEW MAJOR ORDER TIEN KWAN IS HOME TO THE SOLE ARSENAL OF NEW EXOSUIT TECHNOLOGY. IT IS ONLY A MATTER OF TIME BEFORE THE AUTOMATONS DISCOVER
WAR 801
SAT-LINK NOMINALDESIG HDS-OVERWATCH-7720OPERATOR A-1138CLEARANCE DELTA-3--:--:-- UTC
★ Public API // /api/v1/*

API REFERENCE

Every consumer-facing endpoint. JSON in, JSON out. Read endpoints are GET; collector endpoints are POST and gated by x-collector-secret when COLLECTOR_SECRET is set in the env. No auth otherwise.

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-DD or YYYY-MM-DD HH:MM:SS).
  • Optional: gameName ("Helldivers" | "Helldivers 2"), page (default 1), limit (default 50, max 500).
  • 400 if startDate / endDate are missing.
  • Rows have one of two shapes: Steam-collector rows fill steamPlayerCount / PS5PlayerCount / totalPlayerCount (~10 min cadence); ArrowHead-collector rows fill apiTotalPlayerCount (~15s cadence, deduped by WarStatus.time). Both share the same gameName column. Filter with WHERE apiTotalPlayerCount IS NOT NULL for the high-frequency cross-platform series, or WHERE steamPlayerCount IS NOT NULL for 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 env WAR_ID or 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 (alias planetID).
  • Optional: warId (alias warID), startDate, endDate, page (default 1), limit (default 100, max 1000).
  • Default range when startDate is 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 startDate is 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 tasks JSON column (added in migration 004) carrying the raw ArrowHead setting.tasks array: Array<{ type, values, valueTypes }>. Older rows captured before the migration may have tasks = NULL.
  • And a rewards JSON column (added in migration 005) carrying the raw ArrowHead setting.rewards array: Array<{ type, id32, amount }>. The rewardMedals INT column is now only populated when a reward entry matches the medals heuristic (type=1, amount>=2); a unique-item drop (cape etc, where amount===1) leaves rewardMedals=0.
  • This is the raw row shape. The /major-orders UI consumes the same table through getMajorOrderArchive() in src/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 through getMajorOrderDetail() in src/lib/data.ts, which adds completedAt (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 against message), 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: 4xx are validation ({ "error": "..." }), 503 is unavailable (DB / upstream down — wrapped by serviceUnavailable(err) in src/lib/api.ts).
  • Pagination: any endpoint with a page/limit query also returns totalResults and totalPages. limit is clamped per-endpoint.
  • Dates: all query params accept YYYY-MM-DD or YYYY-MM-DD HH:MM:SS (MySQL-friendly). Responses always serialize Date as ISO-8601.
  • War ID aliases: routes accept both warId and warID for forgiveness with legacy callers.
  • Caching: lookup tables (planets, sectors, factions) cache 1h; war state caches 30s; historical / live / collector routes are no-store or force-dynamic.