GAMING STATS API ยท 3.8.3

Aggregate API

Overview

Base path: /aggregate Read a frontend-ready library and all current play sessions assembled from cached Steam, PSN, Epic, Playnite, and custom Valorant data. Aggregate requests never call providers or trigger refreshes. Raw provider routes remain available when the frontend needs provider-specific fields.

Authentication

Every aggregate data endpoint requires the same X-API-Key as the provider routes. Send it from a trusted backend and never expose it in browser JavaScript. Documentation is public. Responses use Cache-Control: private, no-store.

Requests

GET /aggregate/library - canonical games, editions, platform or storefront copies, source observations, selected artwork, and safely deduplicated playtime GET /aggregate/games/:canonicalGameId - authoritative canonical game page with playtime, artwork, activity, and progress GET /aggregate/now-playing - zero or more concurrent current sessions No request accepts a body or refreshes an upstream provider.

Now playing response

sessions is always an array because different devices can run different games at the same time. Steam on macOS and Playnite on Windows therefore remain separate sessions. Provider priority is applied only when two observations describe the same session. A direct Steam observation wins over a Playnite Steam helper, while Playnite remains in detectedBy as confirmation. Stale observations never create sessions. Steam does not reliably identify the current operating system, so its platform can be unknown. state is playing when at least one current game exists, offline when fresh sources report no current game, or unknown when no fresh presence source exists. Provider availability and Playnite heartbeats alone never produce an online state.

{
  "schemaVersion": 1,
  "provider": "aggregate",
  "accountRef": "owner",
  "generatedAt": "2026-09-20T12:30:00.000Z",
  "state": "playing",
  "sessionCount": 2,
  "sessions": [
    {
      "id": "steam:3628960",
      "game": {
        "id": "steam-3628960",
        "name": "Miscrits: World of Creatures",
        "edition": {
          "id": "standard",
          "name": "Standard"
        },
        "artwork": {
          "portraitUrl": null,
          "landscapeUrl": null,
          "squareUrl": null,
          "iconUrl": null,
          "all": []
        }
      },
      "platform": "unknown",
      "device": null,
      "startedAt": null,
      "observedAt": "2026-09-20T12:29:30.000Z",
      "primarySource": "steam",
      "detectedBy": [
        {
          "source": "steam",
          "providerGameId": "3628960",
          "observedAt": "2026-09-20T12:29:30.000Z",
          "startedAt": null,
          "platform": "unknown",
          "device": null
        }
      ],
      "confidence": "single-source"
    },
    {
      "id": "playnite:e14e27a4-50fe-4d86-9c44-03a57e9c4f65",
      "game": {
        "id": "valorant",
        "name": "VALORANT",
        "edition": {
          "id": "standard",
          "name": "Standard"
        },
        "artwork": {
          "portraitUrl": null,
          "landscapeUrl": null,
          "squareUrl": null,
          "iconUrl": null,
          "all": []
        }
      },
      "platform": "windows",
      "device": {
        "id": "playnite-device-id",
        "name": "Gaming PC"
      },
      "startedAt": "2026-09-20T11:36:28.168Z",
      "observedAt": "2026-09-20T12:29:30.000Z",
      "primarySource": "playnite",
      "detectedBy": [
        {
          "source": "playnite",
          "providerGameId": "e14e27a4-50fe-4d86-9c44-03a57e9c4f65",
          "observedAt": "2026-09-20T12:29:30.000Z",
          "startedAt": "2026-09-20T11:36:28.168Z",
          "platform": "windows",
          "device": {
            "id": "playnite-device-id",
            "name": "Gaming PC"
          }
        }
      ],
      "confidence": "single-source"
    }
  ],
  "sources": {
    "steam": {
      "status": "ready",
      "stale": false,
      "lastSuccessAt": "2026-09-20T12:29:30.000Z"
    },
    "psn": {
      "status": "ready",
      "stale": false,
      "lastSuccessAt": "2026-09-20T12:29:30.000Z"
    },
    "playnite": {
      "status": "ready",
      "stale": false,
      "lastSuccessAt": "2026-09-20T12:29:30.000Z"
    }
  }
}

Library model

games are canonical works. Each work contains editions, each edition contains independently played copies, and each copy contains its raw normalized provider observations. The frontend may show one work card, edition cards, or individual platform cards. Curated mappings in config/game-identities.json confirm cross-provider identity. Epic- or Steam-backed Playnite records merge into a unique direct provider title despite cosmetic trademark, accent, punctuation, or capitalization differences. Their observations use role helper-mirror and helperFor identifies the storefront. If no unique direct record exists, the Playnite record stays classified under that storefront rather than becoming a local copy. Other exact title matches remain separate and appear in possibleMatches.

Canonical game detail

GET /aggregate/games/:canonicalGameId is the authoritative page response. game.playtime is the only playtime total the frontend should display or calculate from. activity contains current sessions for this canonical game. progress keeps every Steam achievement set and PlayStation trophy set separate under one provider-neutral contract. Regional PSN title records that resolve to the same service and trophy-set ID appear once, with every title ID retained in providerGameIds and sourceRefs. There is no combined cross-source completion percentage. Each set has its own earned, available, known, and completionPercent values. Steam unlock imageUrl is state-aware: earned achievements prefer unlockedIconUrl, locked achievements prefer lockedIconUrl, and both fall back to iconUrl. Unknown states use the best available provider icon. progress.rarestUnlock is the earned unlock with the lowest reported source-population percentage across the available sets. Epic, Playnite, and Valorant report unsupported for unlocks rather than empty achievements. Failures from unrelated providers do not lower this game response, while a configured matched source that is unavailable appears in progress.sources.

[
  {
    "case": "steam-only",
    "status": "ready",
    "progress": {
      "status": "available",
      "setCount": 1,
      "rarestUnlock": {
        "id": "steam:570:achievement:FIRST",
        "source": "steam",
        "kind": "achievement",
        "rarityPercent": 12.5
      },
      "sets": [
        {
          "id": "steam:570:achievements",
          "source": "steam",
          "kind": "achievement",
          "summary": {
            "earned": 1,
            "available": 2,
            "known": 2,
            "completionPercent": 50
          }
        }
      ],
      "sources": [
        {
          "source": "steam",
          "providerGameIds": [
            "570"
          ],
          "status": "available"
        }
      ]
    }
  },
  {
    "case": "psn-only",
    "status": "ready",
    "progress": {
      "status": "available",
      "setCount": 1,
      "rarestUnlock": {
        "id": "psn:trophy2:NPWR12345_00:trophy:1",
        "source": "psn",
        "kind": "trophy",
        "grade": "bronze",
        "rarityPercent": 2.5
      },
      "sets": [
        {
          "id": "psn:trophy2:NPWR12345_00",
          "source": "psn",
          "kind": "trophy",
          "summary": {
            "earned": 1,
            "available": 2,
            "known": 2,
            "completionPercent": 50
          }
        }
      ],
      "sources": [
        {
          "source": "psn",
          "providerGameIds": [
            "PPSA12345_00"
          ],
          "status": "available"
        }
      ]
    }
  },
  {
    "case": "matched-steam-and-psn",
    "status": "ready",
    "progress": {
      "status": "available",
      "setCount": 2,
      "completionCalculation": "reported-per-progress-set-no-cross-source-merge",
      "sets": [
        {
          "id": "psn:trophy2:NPWR12345_00",
          "source": "psn",
          "kind": "trophy",
          "summary": {
            "earned": 1,
            "available": 2,
            "known": 2,
            "completionPercent": 50
          }
        },
        {
          "id": "steam:570:achievements",
          "source": "steam",
          "kind": "achievement",
          "summary": {
            "earned": 1,
            "available": 2,
            "known": 2,
            "completionPercent": 50
          }
        }
      ]
    }
  },
  {
    "case": "partial-provider-failure",
    "status": "partial",
    "progress": {
      "status": "partial",
      "setCount": 1,
      "sets": [
        {
          "id": "steam:570:achievements",
          "source": "steam",
          "kind": "achievement",
          "summary": {
            "earned": 1,
            "available": 2,
            "known": 2,
            "completionPercent": 50
          }
        }
      ],
      "sources": [
        {
          "source": "steam",
          "providerGameIds": [
            "570"
          ],
          "status": "available"
        },
        {
          "source": "psn",
          "providerGameIds": [
            "PPSA12345_00"
          ],
          "status": "unavailable"
        }
      ]
    }
  },
  {
    "case": "no-unlock-data",
    "status": "ready",
    "progress": {
      "status": "unsupported",
      "setCount": 0,
      "unlockCount": 0,
      "rarestUnlock": null,
      "sets": [],
      "sources": [
        {
          "source": "epic",
          "providerGameIds": [
            "epic-id"
          ],
          "status": "unsupported"
        }
      ]
    }
  }
]

Artwork model

Every game and copy has artwork.portraitUrl, artwork.landscapeUrl, artwork.squareUrl, artwork.iconUrl, and artwork.all. The four selected fields are independent and can be null when no source supplies that role. squareUrl is reserved for genuine square promotional artwork suitable for a large tile. Small Steam application and community icons, Playnite application icons, and Epic Android icons only qualify for iconUrl and remain retained in artwork.all. artwork.all retains every distinct safe URL from every source, including logos, backgrounds, characters, and screenshots, plus provider, provider game ID, source role, original type, semantic roles, dimensions, content type, provider metadata, and every source that contributed an identical URL. Only identical URLs are deduplicated. Authoritative provider images are selected ahead of helper mirrors. No generic coverUrl or backgroundUrl alias is returned.

Playtime rules

A copy selects one authoritative lifetime instead of adding overlapping observations. Steam lifetime already includes its Windows, macOS, and Linux components. Deck and disconnected values overlap that lifetime. Direct Epic playtime wins over its Playnite mirror; a positive Playnite value is a fallback when Epic is unknown, while zero does not turn unknown into zero. The configured Playnite Valorant lifetime includes backlog hours and tracked sessions, so it wins with second precision. Custom Valorant hours remain a lower-precision fallback when Playnite lifetime is unavailable. The two values are never added. Regional PSN IDs grouped into one copy use the largest value rather than a sum. Independent platform and storefront copies can be added at edition and work levels. Every choice is exposed through rule, selectedFrom, and excluded.

Freshness and errors

The library returns HTTP 200 when at least one provider is usable and status is ready or partial. HTTP 503 means every source is unavailable. Now playing returns HTTP 200 for playing or offline and HTTP 503 for unknown when no fresh presence source exists. HTTP 401 means a missing or invalid read key. A canonical game lookup returns HTTP 404 when its ID is absent. sources always carries provider freshness so the frontend can explain partial results.

Frontend guidance

For live activity, render every entry in sessions. Do not choose one global winner. Use primarySource for the label and detectedBy for optional attribution. Use artwork.landscapeUrl for wide cards, portraitUrl for poster cards, squareUrl for square tiles, and iconUrl for compact icons. Do not read coverUrl or backgroundUrl. For the library, use the canonical game id for routes and UI state. Treat playtime.status unknown as missing data, not zero. Display work or edition totals only when the product wants a combined view, and keep copy breakdowns available. A complete implementation handoff is linked from the repository documentation.