Overview
Base path: /psn Read cached PlayStation history, trophies and presence for the server-configured owner. Website requests never call Sony or trigger refreshes. This is an unofficial integration. All examples below are illustrative, not live account data.
Authentication
Every data endpoint requires X-API-Key. Send it from your website server or backend proxy; never put it in browser JavaScript. Documentation is public. Missing or invalid keys return HTTP 401 with {"error":"Invalid or missing API key"}. Data responses use Cache-Control: private, no-store. No account selector, credential upload, disconnect, or refresh HTTP endpoint exists.
Requests
GET /psn/library — PS4/PS5 played history and totals GET /psn/summary — trophy level and grade counts GET /psn/presence — latest observed activity GET /psn/games/:titleId — a played record, related editions and trophy details Use providerGameId from the library as titleId, not conceptId or a trophy-set ID. No request body or pagination parameters are required: the library snapshot contains the fully fetched upstream result.
Request example
Set API_BASE_URL to your deployment origin, without /psn. PSN_API_KEY is an existing API_KEYS read key, not a Sony credential.
curl --fail-with-body "$API_BASE_URL/psn/library" \
-H "X-API-Key: $PSN_API_KEY"Library response
Fields absent from Sony become null where supported. playtimeMinutes keeps fractional minutes; zero is distinct from unknown. platform is PS4, PS5, or null for Sony's unknown category. Games are sorted by known playtime descending. conceptId groups related editions without merging them; canonicalGameId is currently null. artwork.url is the compatibility image, localizedUrl is Sony's localized variant, and artwork.images preserves every safe typed cover, logo, master, background, hero, banner, portrait, and screenshot record. Duplicate record/concept media is collapsed by type, format, and URL. Sony does not supply dimensions here. coverage.complete means pagination completed for this history query, not a complete purchase inventory. Platforms describe the query filter. Totals sum records, not deduplicated lifetime hours across editions.
{
"schemaVersion": 1,
"provider": "psn",
"accountRef": "owner",
"status": "ready",
"stale": false,
"lastAttemptAt": "2026-09-16T12:00:00.000Z",
"lastSuccessAt": "2026-09-16T12:00:00.000Z",
"nextRefreshAt": "2026-09-16T12:15:00.000Z",
"coverage": {
"kind": "played-history",
"platforms": [
"PS4",
"PS5"
],
"complete": true,
"purchaseLibrary": false
},
"totals": {
"recordCount": 1,
"conceptCount": 1,
"knownRecordPlaytimeMinutes": 90.5,
"knownPlaytimeRecords": 1,
"unknownPlaytimeRecords": 0,
"unknownPlatformRecords": 0
},
"games": [
{
"providerGameId": "PPSA12345_00",
"conceptId": "10001234",
"canonicalGameId": null,
"name": "Example Game",
"platform": "PS5",
"playtimeMinutes": 90.5,
"playCount": 3,
"firstPlayedAt": "2026-09-01T12:00:00.000Z",
"lastPlayedAt": "2026-09-16T12:00:00.000Z",
"activityStatus": "played-history",
"artwork": {
"url": "https://image.api.playstation.com/example/primary.png",
"localizedUrl": "https://image.api.playstation.com/example/localized.png",
"width": null,
"height": null,
"images": [
{
"type": "GAMEHUB_COVER_ART",
"format": "IMAGE",
"url": "https://image.api.playstation.com/example/cover.png"
},
{
"type": "SCREENSHOT",
"format": "IMAGE",
"url": "https://image.api.playstation.com/example/screenshot.png"
}
]
}
}
]
}Summary response
Summary totals may include hidden titles. visibleTrophySets counts only visible sets, so it need not explain all summary trophies. Unknown trophy counts or level are null.
{
"schemaVersion": 1,
"provider": "psn",
"accountRef": "owner",
"status": "ready",
"stale": false,
"lastAttemptAt": "2026-09-16T12:00:00.000Z",
"lastSuccessAt": "2026-09-16T12:00:00.000Z",
"nextRefreshAt": "2026-09-16T12:15:00.000Z",
"trophyLevel": 1,
"earnedTrophies": {
"bronze": 1,
"silver": 0,
"gold": 0,
"platinum": 0
},
"visibleTrophySets": 1,
"totalsMayIncludeHiddenTitles": true
}Presence response
activity is playing, idle, or unavailable. online is true, false, or null. An online player without a reported game is idle; offline clears game fields. Reported games contain providerGameId, name, platform, and imageUrl. Missing upstream activity can produce activity: unavailable even when the fetch status is ready. Display observed status only while fresh; unavailable is not proof the user is offline.
{
"schemaVersion": 1,
"provider": "psn",
"accountRef": "owner",
"status": "ready",
"stale": false,
"lastAttemptAt": "2026-09-16T12:00:00.000Z",
"lastSuccessAt": "2026-09-16T12:00:00.000Z",
"nextRefreshAt": "2026-09-16T12:01:00.000Z",
"activity": "idle",
"online": false,
"platform": "PS5",
"games": []
}Game detail response
A known game returns HTTP 200 even if trophies are pending or unavailable. Inspect trophyStatus separately from the top-level status, which describes the detail snapshot. trophyStatus is pending, available, no-visible-synced-set, private, or unavailable. relatedEditions lists records sharing conceptId and can include this edition. Hidden trophy sets are excluded. Locked or unknown secret trophies show Hidden trophy with no description or image. earned: null means unknown, not locked. earnedRate is a percentage or null; zero is valid. Rare unlocks are selected only among earned trophies with known rarity in visible cached sets.
{
"schemaVersion": 1,
"provider": "psn",
"accountRef": "owner",
"status": "ready",
"stale": false,
"lastAttemptAt": "2026-09-16T12:00:00.000Z",
"lastSuccessAt": "2026-09-16T12:00:00.000Z",
"nextRefreshAt": "2026-09-16T13:00:00.000Z",
"game": {
"providerGameId": "PPSA12345_00",
"conceptId": "10001234",
"canonicalGameId": null,
"name": "Example Game",
"platform": "PS5",
"playtimeMinutes": 90.5,
"playCount": 3,
"firstPlayedAt": "2026-09-01T12:00:00.000Z",
"lastPlayedAt": "2026-09-16T12:00:00.000Z",
"activityStatus": "played-history",
"artwork": {
"url": "https://image.api.playstation.com/example/primary.png",
"localizedUrl": "https://image.api.playstation.com/example/localized.png",
"width": null,
"height": null,
"images": [
{
"type": "GAMEHUB_COVER_ART",
"format": "IMAGE",
"url": "https://image.api.playstation.com/example/cover.png"
},
{
"type": "SCREENSHOT",
"format": "IMAGE",
"url": "https://image.api.playstation.com/example/screenshot.png"
}
]
}
},
"relatedEditions": [
{
"providerGameId": "PPSA12345_00",
"name": "Example Game",
"platform": "PS5"
}
],
"trophyStatus": "available",
"trophySets": [
{
"id": "NPWR12345_00",
"service": "trophy2",
"name": "Example Game",
"platform": "PS5",
"imageUrl": null,
"progress": 100,
"defined": {
"bronze": 1,
"silver": 0,
"gold": 0,
"platinum": 0
},
"earned": {
"bronze": 1,
"silver": 0,
"gold": 0,
"platinum": 0
},
"complete": true,
"trophies": [
{
"id": 1,
"grade": "bronze",
"hidden": false,
"earned": true,
"name": "First steps",
"description": "Finish the introduction.",
"imageUrl": null,
"earnedAt": "2026-09-01T12:05:00.000Z",
"earnedRate": 75.5
}
],
"rarestUnlock": {
"id": 1,
"grade": "bronze",
"hidden": false,
"earned": true,
"name": "First steps",
"description": "Finish the introduction.",
"imageUrl": null,
"earnedAt": "2026-09-01T12:05:00.000Z",
"earnedRate": 75.5
},
"rarityCoverage": {
"earned": 1,
"earnedWithKnownRarity": 1
}
}
],
"rarestUnlock": {
"id": 1,
"grade": "bronze",
"hidden": false,
"earned": true,
"name": "First steps",
"description": "Finish the introduction.",
"imageUrl": null,
"earnedAt": "2026-09-01T12:05:00.000Z",
"earnedRate": 75.5,
"trophySetId": "NPWR12345_00",
"service": "trophy2"
},
"rarityComparison": "earned-trophies-with-known-rarity-in-visible-cached-sets"
}Freshness and errors
All successful resource snapshots include schemaVersion, provider, accountRef, status, stale, lastAttemptAt, lastSuccessAt, and nextRefreshAt. Unconfigured or disabled responses may omit timestamps and payload fields. Timestamps are UTC ISO strings or null; nextRefreshAt is scheduling information, not a delivery guarantee. HTTP 200: ready or bounded stale library/summary/presence; also known game details with separate trophyStatus. HTTP 400: malformed titleId (must match [A-Za-z0-9_-]{3,64}). HTTP 401: missing/invalid API key. HTTP 404: titleId not in available cached played history. HTTP 503: disabled, not-configured, reconnect-required, private, or unavailable main resource; also a game lookup when the library is unavailable. Never render unavailable data as an empty library or a zero trophy count. Keep the previous UI only when response freshness permits it.
Refresh behavior
Default cadence: library/summary 15 minutes, presence 60 seconds, and trophy details 60 minutes per title. Detail enrichment happens one title at a time, so it may initially be pending. These intervals are configurable. Presence expires after twice its interval, with a 90-second minimum, and failed presence clears the payload. Other snapshots can remain stale for up to 24 hours by default. Jobs back off on errors and rate limits. These routes do not consume Valorant scraping quota.
Owner setup and operations
Use Node 24. Configure PSN_ONLINE_ID, GAMING_DATA_DIR on persistent storage, and one encryption source: a 64-character hex GAMING_ENCRYPTION_KEY secret or private local GAMING_ENCRYPTION_KEY_FILE. Deploy with ENABLE_PSN=false until connection and initial refresh succeed. Sign in at https://www.playstation.com/ and then open https://ca.account.sony.com/api/v1/ssocookie in the same browser to obtain NPSSO. The CLI accepts the value or Sony JSON, verifies the configured account, and saves only an encrypted access/refresh session; it does not retain NPSSO. psn:refresh and psn:status operate only on PSN. One always-on replica and one writer per session are required. Keep credentials out of website code, chat, screenshots, command arguments, and Git. psn:disconnect deletes local state but does not revoke the session at Sony.
Local owner connection
Generate a private key file, configure the expected online ID and key path, then connect and perform the initial PSN-only refresh. Start with Valorant auto-refresh disabled when checking PSN in isolation.
npm run psn:init-key
export PSN_ONLINE_ID=your-psn-online-id
export GAMING_ENCRYPTION_KEY_FILE=.private/psn.key
npm run psn:connect
npm run psn:status
npm run psn:refresh
ENABLE_AUTO_REFRESH=false ENABLE_PSN=true npm startRailway connection
Mount persistent storage at /app/cache, set GAMING_DATA_DIR=/app/cache/gaming, use one always-on replica, and deploy initially with ENABLE_PSN=false. Generate the hosted encryption key through the pipe below so it is not printed or stored in shell history. Run railway ssh with no remote command. After the remote root@...:/app# prompt appears, run the remaining commands. Paste a fresh NPSSO only into the hidden prompt. Railway command-mode SSH can echo input without forwarding it, so never use railway ssh -- node scripts/psn/cli.js connect. railway run executes locally and must not create the hosted session. If a credential appears or is shared, cancel it, use PlayStation Account Management to sign out on all devices, and generate a fresh NPSSO. After refresh, set ENABLE_PSN=true, allow the redeploy, and confirm connected/ready status after restart.
node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))" | railway variable set GAMING_ENCRYPTION_KEY --stdin --skip-deploys
railway ssh
# After the remote prompt appears:
npm run psn:connect:production
npm run psn:status
npm run psn:refresh
exit
railway variable set ENABLE_PSN=true
railway ssh -- node scripts/psn/cli.js status