# Schemas

Every named object of the games-api API: the fields each route reads or answers.

The named schemas of `openapi.yaml`, games-api 0.7.0. A route's body or response links here; a field that is itself a named schema links to its own section.

- [`Error`](#schema-error)
- [`Period`](#schema-period)
- [`AgeBand`](#schema-ageband)
- [`Player`](#schema-player)
- [`PlayerWithToken`](#schema-playerwithtoken)
- [`ConsumeResponse`](#schema-consumeresponse)
- [`SaveMeta`](#schema-savemeta)
- [`SaveServerCopy`](#schema-saveservercopy)
- [`Save`](#schema-save)
- [`LeaderboardEntry`](#schema-leaderboardentry)
- [`LeaderboardTop`](#schema-leaderboardtop)
- [`EventBatch`](#schema-eventbatch)
- [`EventIn`](#schema-eventin)
- [`Catalog`](#schema-catalog)
- [`CatalogGame`](#schema-cataloggame)
- [`CatalogRating`](#schema-catalograting)
- [`RatingSummary`](#schema-ratingsummary)
- [`Comment`](#schema-comment)
- [`GameReviews`](#schema-gamereviews)
- [`PlayerReview`](#schema-playerreview)
- [`CommentAdmin`](#schema-commentadmin)
- [`CommentPage`](#schema-commentpage)
- [`GameRatingAdmin`](#schema-gameratingadmin)
- [`FeedbackKind`](#schema-feedbackkind)
- [`FeedbackStatus`](#schema-feedbackstatus)
- [`FeedbackInput`](#schema-feedbackinput)
- [`FeedbackAccepted`](#schema-feedbackaccepted)
- [`FeedbackAdmin`](#schema-feedbackadmin)
- [`FeedbackUpdate`](#schema-feedbackupdate)
- [`FeedbackPage`](#schema-feedbackpage)
- [`MediaSlot`](#schema-mediaslot)
- [`MediaFile`](#schema-mediafile)
- [`GameMedia`](#schema-gamemedia)
- [`GameTexts`](#schema-gametexts)
- [`CategoryIcon`](#schema-categoryicon)
- [`Category`](#schema-category)
- [`CategoryInput`](#schema-categoryinput)
- [`Tag`](#schema-tag)
- [`TagInput`](#schema-taginput)
- [`OtherSite`](#schema-othersite)
- [`Site`](#schema-site)
- [`SiteInput`](#schema-siteinput)
- [`PublishResult`](#schema-publishresult)
- [`Distribution`](#schema-distribution)
- [`Game`](#schema-game)
- [`GameInput`](#schema-gameinput)
- [`GameVersion`](#schema-gameversion)
- [`SiteGame`](#schema-sitegame)
- [`SiteGameInput`](#schema-sitegameinput)
- [`Leaderboard`](#schema-leaderboard)
- [`LeaderboardInput`](#schema-leaderboardinput)
- [`ScoreAdmin`](#schema-scoreadmin)
- [`PlayerAdmin`](#schema-playeradmin)
- [`PlayerPage`](#schema-playerpage)
- [`PlayerExport`](#schema-playerexport)
- [`EventCount`](#schema-eventcount)
- [`EventDaily`](#schema-eventdaily)
- [`StatsTotals`](#schema-statstotals)
- [`StatsDay`](#schema-statsday)
- [`StatsGameRow`](#schema-statsgamerow)
- [`StatsKeyCount`](#schema-statskeycount)
- [`StatsOverview`](#schema-statsoverview)
- [`GameStatsDay`](#schema-gamestatsday)
- [`GameStatsTotals`](#schema-gamestatstotals)
- [`GameStatsBoard`](#schema-gamestatsboard)
- [`GameStats`](#schema-gamestats)
- [`WebStatsRow`](#schema-webstatsrow)
- [`WebStats`](#schema-webstats)
- [`AuditEntry`](#schema-auditentry)
- [`GameToken`](#schema-gametoken)
- [`AccountName`](#schema-accountname)
- [`NewPassword`](#schema-newpassword)
- [`TotpCode`](#schema-totpcode)
- [`MembershipRole`](#schema-membershiprole)
- [`Scope`](#schema-scope)
- [`ConsoleMe`](#schema-consoleme)
- [`SignInResult`](#schema-signinresult)
- [`TotpEnrolment`](#schema-totpenrolment)
- [`ConsoleSessionInfo`](#schema-consolesessioninfo)
- [`InvitationPreview`](#schema-invitationpreview)
- [`InvitationRequestInput`](#schema-invitationrequestinput)
- [`InvitationRequestStatus`](#schema-invitationrequeststatus)
- [`InvitationRequest`](#schema-invitationrequest)
- [`InvitationRequestPage`](#schema-invitationrequestpage)
- [`Membership`](#schema-membership)
- [`AccountAdmin`](#schema-accountadmin)
- [`AccountPage`](#schema-accountpage)
- [`AccountPatch`](#schema-accountpatch)
- [`ImpersonationStarted`](#schema-impersonationstarted)
- [`InvitationGrant`](#schema-invitationgrant)
- [`Invitation`](#schema-invitation)
- [`InvitationInput`](#schema-invitationinput)
- [`ApprovalOwner`](#schema-approvalowner)
- [`ApprovalGame`](#schema-approvalgame)
- [`ApprovalSite`](#schema-approvalsite)
- [`ApprovalDomains`](#schema-approvaldomains)
- [`ApprovalVersion`](#schema-approvalversion)
- [`Approvals`](#schema-approvals)
- [`RejectInput`](#schema-rejectinput)
- [`Member`](#schema-member)
- [`MemberInput`](#schema-memberinput)
- [`SiteKeyScope`](#schema-sitekeyscope)
- [`SiteKey`](#schema-sitekey)
- [`SiteKeyCreated`](#schema-sitekeycreated)
- [`SiteKeyInput`](#schema-sitekeyinput)
- [`RetentionShare`](#schema-retentionshare)
- [`StatsCohort`](#schema-statscohort)
- [`StatsFunnel`](#schema-statsfunnel)
- [`ConsoleStatsDay`](#schema-consolestatsday)
- [`ConsoleStatsTotals`](#schema-consolestatstotals)
- [`ConsoleGameStats`](#schema-consolegamestats)
- [`ServerPlayerInput`](#schema-serverplayerinput)
- [`ServerPlayer`](#schema-serverplayer)

<h2 id="schema-error"><code>Error</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | one of `"unauthorized"`, `"forbidden"`, `"not_found"`, `"invalid"`, `"conflict"`, `"too_large"`, `"rate_limited"`, `"unavailable"`, `"internal"` | yes |  |
| `message` | string | yes |  |
| `details` | object | no |  |

<h2 id="schema-period"><code>Period</code></h2>

Type: one of `"day"`, `"week"`, `"all"`; default `"all"`.

<h2 id="schema-ageband"><code>AgeBand</code></h2>

Type: one of `"all"`, `"kids"`, `"teen"`, `"adult"`.

<h2 id="schema-player"><code>Player</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `nickname` | string | yes |  |
| `guest` | boolean | yes |  |
| `site` | string | yes |  |
| `locale` | string | yes |  |
| `emailMasked` | string or null | no | a***@example.com for the account page |
| `hasPassword` | boolean | yes | True when the account set a password (ADR 0011); false for a guest |
| `createdAt` | string (date-time) | no |  |

<h2 id="schema-playerwithtoken"><code>PlayerWithToken</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `player` | [Player](/docs/api/reference/schemas/#schema-player) | yes |  |
| `token` | string | yes |  |

<h2 id="schema-consumeresponse"><code>ConsumeResponse</code></h2>

What magic-link consume, consume-code and sign-in answer (ADR 0011)

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `player` | [Player](/docs/api/reference/schemas/#schema-player) | yes |  |
| `token` | string | yes |  |
| `merged` | boolean | yes | True when a guest was merged into the account by this call |
| `redirect` | string | no | Path on the site to go to; magic-link consume only, when the link carried one |

<h2 id="schema-savemeta"><code>SaveMeta</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `gameId` | string | yes |  |
| `version` | integer | yes |  |
| `etag` | string | yes |  |
| `bytes` | integer | yes |  |
| `updatedAt` | string (date-time) | yes |  |

<h2 id="schema-saveservercopy"><code>SaveServerCopy</code></h2>

The 409 body. When nothing is stored yet, version is 0, etag is empty and updatedAt and data are null.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `gameId` | string | yes |  |
| `version` | integer | yes |  |
| `etag` | string | yes |  |
| `bytes` | integer | yes |  |
| `updatedAt` | string (date-time) or null | yes |  |
| `data` | any or null | yes |  |

<h2 id="schema-save"><code>Save</code></h2>

Type: [SaveMeta](/docs/api/reference/schemas/#schema-savemeta) + object.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `gameId` | string | yes |  |
| `version` | integer | yes |  |
| `etag` | string | yes |  |
| `bytes` | integer | yes |  |
| `updatedAt` | string (date-time) | yes |  |
| `data` | any | yes |  |

<h2 id="schema-leaderboardentry"><code>LeaderboardEntry</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `rank` | integer | yes |  |
| `nickname` | string | yes |  |
| `score` | integer | yes |  |
| `meta` | object or null | no |  |
| `isMe` | boolean | yes |  |

<h2 id="schema-leaderboardtop"><code>LeaderboardTop</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `period` | [Period](/docs/api/reference/schemas/#schema-period) | yes | default `"all"` |
| `entries` | [LeaderboardEntry](/docs/api/reference/schemas/#schema-leaderboardentry)[] | yes |  |
| `total` | integer | yes |  |
| `me` | [LeaderboardEntry](/docs/api/reference/schemas/#schema-leaderboardentry) or null | no |  |

<h2 id="schema-eventbatch"><code>EventBatch</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `events` | [EventIn](/docs/api/reference/schemas/#schema-eventin)[] | yes | at most 50 items |

<h2 id="schema-eventin"><code>EventIn</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | `frame_loaded` is reserved: accepted with a player token from the hub, refused with a game token (ADR 0015); pattern `^[a-z][a-z0-9_]{0,39}$` |
| `props` | object | no |  |
| `props.*` | string or number or boolean | no |  |
| `ts` | integer | yes | Unix milliseconds on the client |
| `gameId` | string | no |  |
| `gameVersion` | string | no |  |
| `sessionId` | string (uuid) | no |  |
| `host` | string | no |  |
| `locale` | string | no |  |

<h2 id="schema-catalog"><code>Catalog</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `site` | object | yes |  |
| `site.slug` | string | yes |  |
| `site.name` | string | yes |  |
| `site.domains` | string[] | yes |  |
| `site.locales` | string[] | yes |  |
| `site.policy` | object | yes |  |
| `site.policy.accountsAllowed` | boolean | no |  |
| `site.policy.personalizedAdsAllowed` | boolean | no |  |
| `site.policy.ageBand` | [AgeBand](/docs/api/reference/schemas/#schema-ageband) | no |  |
| `site.navCategories` | string[] | yes | Category slugs of the top menu, in menu order; only slugs that exist |
| `site.otherSites` | [OtherSite](/docs/api/reference/schemas/#schema-othersite)[] | yes |  |
| `site.analyticsDomain` | string | yes | The domain registered at CausalIT (causalitDomain, else the first domain); the front's tag reports to it |
| `generatedAt` | string (date-time) | yes |  |
| `categories` | [Category](/docs/api/reference/schemas/#schema-category)[] | yes |  |
| `tags` | [Tag](/docs/api/reference/schemas/#schema-tag)[] | yes | Every tag of the site, in site order; game tags are slugs of this list |
| `games` | [CatalogGame](/docs/api/reference/schemas/#schema-cataloggame)[] | yes |  |

<h2 id="schema-cataloggame"><code>CatalogGame</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `slug` | string | yes |  |
| `order` | integer | yes |  |
| `featured` | boolean | yes |  |
| `categories` | string[] | yes |  |
| `tags` | string[] | yes |  |
| `orientation` | one of `"portrait"`, `"landscape"`, `"any"` | yes |  |
| `minutes` | integer or null | yes | Typical run length in minutes; null when not set |
| `mobile` | boolean | yes |  |
| `daily` | boolean | yes |  |
| `coverColor` | string or null | no |  |
| `publishedAt` | string (date-time) or null | no | When the live version was activated; null without one |
| `bundleUrl` | string (uri) or null | yes | `https://play.filudi.com/<slug>/<version>/index.html`; null when no live version |
| `version` | string or null | yes |  |
| `boards` | object[] | yes |  |
| `boards[].name` | string | no |  |
| `boards[].sort` | one of `"desc"`, `"asc"` | no |  |
| `texts` | object | yes | Keyed by locale |
| `texts.*` | [GameTexts](/docs/api/reference/schemas/#schema-gametexts) | no |  |
| `media` | [GameMedia](/docs/api/reference/schemas/#schema-gamemedia) | yes | Filled slots only; a missing key is an empty slot |
| `rating` | [CatalogRating](/docs/api/reference/schemas/#schema-catalograting) | yes | Stars of a game on the site (ADR 0012). Follows ratings within the catalog's 60-second cache. |

<h2 id="schema-catalograting"><code>CatalogRating</code></h2>

Stars of a game on the site (ADR 0012). Follows ratings within the catalog's 60-second cache.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `average` | number or null | yes | Mean of the stars, rounded to one decimal; null when count is 0 |
| `count` | integer | yes | at least 0 |

<h2 id="schema-ratingsummary"><code>RatingSummary</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `average` | number or null | yes | Mean of the stars, rounded to one decimal; null when count is 0 |
| `count` | integer | yes | at least 0 |
| `histogram` | integer[] | yes | Ratings per star count; index 0 is one star, index 4 is five stars; 5 to 5 items |

<h2 id="schema-comment"><code>Comment</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `nickname` | string | yes | The author's current nickname, read when the list is served |
| `body` | string | yes | Plain text with newlines; render as text |
| `createdAt` | string (date-time) | yes |  |
| `editedAt` | string (date-time) or null | yes |  |

<h2 id="schema-gamereviews"><code>GameReviews</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `rating` | [RatingSummary](/docs/api/reference/schemas/#schema-ratingsummary) | yes |  |
| `comments` | [Comment](/docs/api/reference/schemas/#schema-comment)[] | yes |  |
| `nextCursor` | string or null | yes | Null on the last page |

<h2 id="schema-playerreview"><code>PlayerReview</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `stars` | integer or null | yes | Null when the player has not rated the game; 1 to 5 |
| `comments` | [Comment](/docs/api/reference/schemas/#schema-comment)[] | yes | The player's visible comments on the game, newest first |

<h2 id="schema-commentadmin"><code>CommentAdmin</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `gameId` | string (uuid) | yes |  |
| `game` | string | yes | Game slug |
| `player` | object | yes |  |
| `player.id` | string (uuid) | yes |  |
| `player.nickname` | string | yes |  |
| `player.emailMasked` | string or null | yes |  |
| `body` | string | yes |  |
| `locale` | string | yes |  |
| `hidden` | boolean | yes |  |
| `hiddenBy` | string or null | yes | Username of the moderator |
| `hiddenAt` | string (date-time) or null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `editedAt` | string (date-time) or null | yes |  |

<h2 id="schema-commentpage"><code>CommentPage</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | [CommentAdmin](/docs/api/reference/schemas/#schema-commentadmin)[] | yes |  |
| `nextCursor` | string or null | yes |  |
| `total` | integer | yes |  |

<h2 id="schema-gameratingadmin"><code>GameRatingAdmin</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `gameId` | string (uuid) | yes |  |
| `game` | string | yes | Game slug |
| `average` | number or null | yes |  |
| `count` | integer | yes |  |
| `histogram` | integer[] | yes | Index 0 is one star; 5 to 5 items |

<h2 id="schema-feedbackkind"><code>FeedbackKind</code></h2>

Type: one of `"bug"`, `"suggestion"`, `"other"`.

<h2 id="schema-feedbackstatus"><code>FeedbackStatus</code></h2>

Type: one of `"new"`, `"seen"`, `"done"`.

<h2 id="schema-feedbackinput"><code>FeedbackInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `site` | string | yes | Site slug |
| `game` | string | no | Game slug; absent when the message is about the site |
| `kind` | [FeedbackKind](/docs/api/reference/schemas/#schema-feedbackkind) | yes |  |
| `message` | string | yes | Counted after trim; 10 to 2000 characters |
| `email` | string (email) or string | no | Where the team may reply; "" counts as no email |
| `page` | string | no | Path of the page, without the query string; at most 512 characters; pattern `^/` |
| `locale` | string | no | The page locale; kept when it is a locale of the site |

<h2 id="schema-feedbackaccepted"><code>FeedbackAccepted</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |

<h2 id="schema-feedbackadmin"><code>FeedbackAdmin</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `kind` | [FeedbackKind](/docs/api/reference/schemas/#schema-feedbackkind) | yes |  |
| `status` | [FeedbackStatus](/docs/api/reference/schemas/#schema-feedbackstatus) | yes |  |
| `message` | string | yes |  |
| `email` | string or null | yes | In clear: the reporter gave it to be contacted |
| `gameId` | string (uuid) or null | yes |  |
| `game` | string or null | yes | Game slug; null for the site or a deleted game |
| `player` | object or null | yes |  |
| `page` | string or null | yes |  |
| `userAgent` | string or null | yes |  |
| `locale` | string or null | yes |  |
| `note` | string or null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

<h2 id="schema-feedbackupdate"><code>FeedbackUpdate</code></h2>

At least one field; an empty body is a 400.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | [FeedbackStatus](/docs/api/reference/schemas/#schema-feedbackstatus) | no |  |
| `note` | string or null | no | Null clears the note; "" is stored as null; at most 2000 characters |

<h2 id="schema-feedbackpage"><code>FeedbackPage</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | [FeedbackAdmin](/docs/api/reference/schemas/#schema-feedbackadmin)[] | yes |  |
| `nextCursor` | string or null | yes |  |
| `total` | integer | yes |  |

<h2 id="schema-mediaslot"><code>MediaSlot</code></h2>

ADR 0007 gives the ratio, pixel range, file types and size limit of each slot

Type: one of `"image-square"`, `"image-landscape"`, `"image-portrait"`, `"video-landscape"`, `"video-portrait"`.

<h2 id="schema-mediafile"><code>MediaFile</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string (uri) | yes | `https://play.filudi.com/<slug>/media/<kind>-<format>-<sha256:16>.<ext>`; immutable |
| `type` | one of `"image/avif"`, `"image/webp"`, `"image/jpeg"`, `"image/png"`, `"video/mp4"` | yes |  |
| `bytes` | integer | yes |  |
| `width` | integer | yes |  |
| `height` | integer | yes |  |
| `durationMs` | integer | no | Videos only |

<h2 id="schema-gamemedia"><code>GameMedia</code></h2>

Filled slots only; a missing key is an empty slot

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `images` | object | yes |  |
| `images.square` | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | no |  |
| `images.landscape` | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | no |  |
| `images.portrait` | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | no |  |
| `videos` | object | yes |  |
| `videos.landscape` | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | no |  |
| `videos.portrait` | [MediaFile](/docs/api/reference/schemas/#schema-mediafile) | no |  |

<h2 id="schema-gametexts"><code>GameTexts</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | yes |  |
| `tagline` | string | yes | One line under the title |
| `description` | string | yes |  |
| `howToPlay` | string[] | no |  |
| `controls` | object[] | no |  |
| `controls[].keys` | string[] | no |  |
| `controls[].does` | string | no |  |
| `seoTitle` | string | no |  |
| `seoDescription` | string | no |  |

<h2 id="schema-categoryicon"><code>CategoryIcon</code></h2>

Stroke icons drawn by hub-core (`categoryIconSvg`), the same list in the admin picker

Type: one of `"puzzle"`, `"joystick"`, `"calendar"`, `"keyboard"`, `"letters"`, `"brain"`, `"cards"`, `"dice"`, `"target"`, `"rocket"`, `"timer"`, `"trophy"`, `"heart"`, `"star"`, `"sparkle"`, `"grid"`, `"map"`, `"music"`, `"bolt"`, `"leaf"`.

<h2 id="schema-category"><code>Category</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | no |  |
| `slug` | string | yes |  |
| `order` | integer | yes |  |
| `icon` | [CategoryIcon](/docs/api/reference/schemas/#schema-categoryicon) or null | no |  |
| `texts` | object | yes |  |
| `texts.*.title` | string | no |  |
| `texts.*.navTitle` | string | no | Short form for the menu, chips and card meta: "Puzzle" for "Puzzle games" |
| `texts.*.description` | string | no |  |
| `gameCount` | integer | no | Admin routes only; games of the site in this category |
| `createdAt` | string (date-time) | no |  |
| `updatedAt` | string (date-time) | no |  |

<h2 id="schema-categoryinput"><code>CategoryInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `slug` | string | no | On create only; ignored on PATCH; pattern `^[a-z][a-z0-9-]{0,39}$` |
| `icon` | [CategoryIcon](/docs/api/reference/schemas/#schema-categoryicon) or null | no |  |
| `texts` | object | no | Keyed by locale; the site's default locale needs a title |
| `texts.*.title` | string | no | at most 120 characters |
| `texts.*.navTitle` | string | no | at most 24 characters |
| `texts.*.description` | string | no | at most 1000 characters |

<h2 id="schema-tag"><code>Tag</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | no |  |
| `slug` | string | yes |  |
| `order` | integer | yes |  |
| `texts` | object | yes | Keyed by locale |
| `texts.*.title` | string | no |  |
| `gameCount` | integer | no | Admin routes only; games of the site carrying this tag |
| `createdAt` | string (date-time) | no |  |
| `updatedAt` | string (date-time) | no |  |

<h2 id="schema-taginput"><code>TagInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `slug` | string | no | On create only; ignored on PATCH; pattern `^[a-z][a-z0-9-]{0,39}$` |
| `texts` | object | no | Keyed by locale; the site's default locale needs a title |
| `texts.*.title` | string | no | at most 60 characters |

<h2 id="schema-othersite"><code>OtherSite</code></h2>

A sister site of the footer

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | 1 to 60 characters |
| `url` | string (uri) | yes | https only |
| `description` | string | no | at most 120 characters |

<h2 id="schema-site"><code>Site</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `domains` | string[] | yes | Approved domains; allowed origins once the site is approved |
| `locales` | string[] | yes |  |
| `approvedAt` | string (date-time) or null | yes | Null while the site waits for staff (ADR 0015); then its domains are not allowed origins |
| `pendingDomains` | string[] | yes | Domains the owner proposed; approval moves them into `domains` |
| `firstParty` | boolean | yes | The owner's own site: lists any approved game, whatever its distribution |
| `accountsAllowed` | boolean | yes |  |
| `personalizedAdsAllowed` | boolean | yes |  |
| `ageBand` | [AgeBand](/docs/api/reference/schemas/#schema-ageband) | yes |  |
| `cdnPullZoneId` | string or null | no | Bunny pull zone in front of the site; publish purges it |
| `navCategories` | string[] | yes | Category slugs of the top menu, in order |
| `otherSites` | [OtherSite](/docs/api/reference/schemas/#schema-othersite)[] | yes |  |
| `causalitDomain` | string or null | no | Domain registered at CausalIT; null means the first domain |
| `causalitKeys` | object | yes | Whether each key is stored. The keys themselves are never returned. |
| `causalitKeys.read` | boolean | yes |  |
| `causalitKeys.write` | boolean | yes |  |
| `createdAt` | string (date-time) | no |  |
| `updatedAt` | string (date-time) | no |  |

<h2 id="schema-siteinput"><code>SiteInput</code></h2>

On PATCH the slug may be repeated unchanged; the API refuses a different slug.
`slug`, `domains`, `firstParty` and `cdnPullZoneId` are for staff only; owners and members write
`pendingDomains` and the CausalIT fields (ADR 0015). A key field set to null clears the stored key;
absent leaves it.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `slug` | string | no | pattern `^[a-z][a-z0-9-]{1,39}$` |
| `name` | string | no | 1 to 80 characters |
| `domains` | string[] | no | Lowercase hostnames, the first is canonical. Staff only.; at least 1 item |
| `pendingDomains` | string[] | no | Lowercase hostnames the owner proposes; each works once staff approves it. Replaces the whole list.; at most 10 items |
| `firstParty` | boolean | no | Staff only |
| `locales` | string[] | no | Lowercase BCP 47, unique, the first is the default; at least 1 item |
| `accountsAllowed` | boolean | no |  |
| `personalizedAdsAllowed` | boolean | no |  |
| `ageBand` | [AgeBand](/docs/api/reference/schemas/#schema-ageband) | no |  |
| `cdnPullZoneId` | string or null | no | pattern `^[0-9]{1,12}$` |
| `navCategories` | string[] | no | Slugs of existing categories of the site, unique, in menu order; at most 12 items |
| `otherSites` | [OtherSite](/docs/api/reference/schemas/#schema-othersite)[] | no | at most 10 items |
| `causalitDomain` | string or null | no | pattern `^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$` |
| `causalitReadKey` | string or null | no | 8 to 200 characters; write only |
| `causalitWriteKey` | string or null | no | 8 to 200 characters; write only |

<h2 id="schema-publishresult"><code>PublishResult</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `purged` | boolean | yes |  |
| `reason` | one of `"no_zone"`, `"no_key"` | no | Why nothing was purged; absent when purged is true |
| `at` | string (date-time) | yes |  |

<h2 id="schema-distribution"><code>Distribution</code></h2>

Where a game may be listed (ADR 0015). `first_party`: sites with `firstParty` only. `any_portal`: any approved site.

Type: one of `"first_party"`, `"any_portal"`.

<h2 id="schema-game"><code>Game</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `orientation` | one of `"portrait"`, `"landscape"`, `"any"` | yes |  |
| `minutes` | integer or null | yes | Typical run length in minutes; null when not set |
| `mobile` | boolean | yes |  |
| `daily` | boolean | yes |  |
| `coverColor` | string or null | no |  |
| `approvedAt` | string (date-time) or null | yes | Null while the game waits for staff (ADR 0015): no live version, no listing on an approved site |
| `distribution` | [Distribution](/docs/api/reference/schemas/#schema-distribution) | yes | Where a game may be listed (ADR 0015). `first_party`: sites with `firstParty` only. `any_portal`: any approved site. |
| `listedOn` | object[] | yes | Sites whose catalog carries the game |
| `listedOn[].siteId` | string (uuid) | yes |  |
| `listedOn[].slug` | string | yes |  |
| `listedOn[].name` | string | yes |  |
| `versions` | [GameVersion](/docs/api/reference/schemas/#schema-gameversion)[] | yes |  |
| `media` | [GameMedia](/docs/api/reference/schemas/#schema-gamemedia) | yes | Filled slots only; a missing key is an empty slot |
| `sites` | string[] | no | Slugs of sites listing this game |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

<h2 id="schema-gameinput"><code>GameInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `slug` | string | no | pattern `^[a-z][a-z0-9-]{1,39}$` |
| `name` | string | no |  |
| `orientation` | one of `"portrait"`, `"landscape"`, `"any"` | no |  |
| `minutes` | integer or null | no | 1 to 600 |
| `mobile` | boolean | no |  |
| `daily` | boolean | no |  |
| `coverColor` | string or null | no | pattern `^#[0-9A-Fa-f]{6}$` |
| `distribution` | [Distribution](/docs/api/reference/schemas/#schema-distribution) | no | Where a game may be listed (ADR 0015). `first_party`: sites with `firstParty` only. `any_portal`: any approved site. |

<h2 id="schema-gameversion"><code>GameVersion</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `version` | string | yes |  |
| `state` | one of `"draft"`, `"live"`, `"retired"` | yes |  |
| `bundlePath` | string | yes |  |
| `entry` | string | yes |  |
| `sizeBytes` | integer | yes |  |
| `fileCount` | integer | yes |  |
| `notes` | string or null | no |  |
| `createdBy` | string | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `activatedAt` | string (date-time) or null | no |  |
| `retiredAt` | string (date-time) or null | no |  |
| `reviewRequestedAt` | string (date-time) or null | yes | Set by `POST /v1/admin/versions/{versionId}/review`; cleared on activate, retire and review reject |

<h2 id="schema-sitegame"><code>SiteGame</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `gameId` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `order` | integer | yes |  |
| `featured` | boolean | yes |  |
| `visible` | boolean | yes |  |
| `categories` | string[] | yes |  |
| `tags` | string[] | yes |  |
| `texts` | object | yes |  |
| `texts.*` | [GameTexts](/docs/api/reference/schemas/#schema-gametexts) | no |  |
| `liveVersion` | string or null | no |  |
| `updatedAt` | string (date-time) | no |  |

<h2 id="schema-sitegameinput"><code>SiteGameInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `featured` | boolean | no |  |
| `visible` | boolean | no |  |
| `categories` | string[] | no |  |
| `tags` | string[] | no |  |
| `texts` | object | no |  |
| `texts.*` | [GameTexts](/docs/api/reference/schemas/#schema-gametexts) | no |  |

<h2 id="schema-leaderboard"><code>Leaderboard</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `name` | string | yes |  |
| `sort` | one of `"desc"`, `"asc"` | yes |  |
| `min` | integer | yes |  |
| `max` | integer | yes |  |
| `keep` | one of `"best"`, `"latest"` | yes |  |
| `ratePerMinute` | integer | yes |  |

<h2 id="schema-leaderboardinput"><code>LeaderboardInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | pattern `^[a-z][a-z0-9_]{0,31}$` |
| `sort` | one of `"desc"`, `"asc"` | yes |  |
| `min` | integer | yes |  |
| `max` | integer | yes |  |
| `keep` | one of `"best"`, `"latest"` | no | default `"best"` |
| `ratePerMinute` | integer | no | default `10` |

<h2 id="schema-scoreadmin"><code>ScoreAdmin</code></h2>

A developer who does not edit the player's site gets no `playerId`, `site`, `hiddenBy` or `sessionId` (ADR 0014).

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `playerId` | string (uuid) | no |  |
| `nickname` | string | yes |  |
| `site` | string | no |  |
| `score` | integer | yes |  |
| `meta` | object or null | no |  |
| `hidden` | boolean | yes |  |
| `hiddenBy` | string or null | no |  |
| `sessionId` | string (uuid) | no |  |
| `createdAt` | string (date-time) | yes |  |

<h2 id="schema-playeradmin"><code>PlayerAdmin</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `site` | string | yes |  |
| `nickname` | string | yes |  |
| `guest` | boolean | yes |  |
| `emailMasked` | string or null | no |  |
| `externalId` | string or null | no | A portal backend's own user id, set through `POST /v1/server/players`; null otherwise |
| `hasPassword` | boolean | yes | Whether the account set a password; the admin cannot set one |
| `locale` | string | no |  |
| `createdAt` | string (date-time) | yes |  |
| `lastSeenAt` | string (date-time) | yes |  |
| `deleteAt` | string (date-time) or null | no |  |
| `mergedIntoId` | string (uuid) or null | no |  |
| `saves` | [SaveMeta](/docs/api/reference/schemas/#schema-savemeta)[] | no |  |
| `bestScores` | object[] | no |  |
| `bestScores[].game` | string | yes |  |
| `bestScores[].board` | string | yes |  |
| `bestScores[].score` | integer | yes |  |
| `bestScores[].createdAt` | string (date-time) | yes |  |

<h2 id="schema-playerpage"><code>PlayerPage</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | [PlayerAdmin](/docs/api/reference/schemas/#schema-playeradmin)[] | yes |  |
| `nextCursor` | string or null | yes | Null on the last page |
| `total` | integer | yes | Rows matching the filters |

<h2 id="schema-playerexport"><code>PlayerExport</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `exportedAt` | string (date-time) | yes |  |
| `player` | [Player](/docs/api/reference/schemas/#schema-player) | yes |  |
| `email` | string or null | no |  |
| `saves` | [Save](/docs/api/reference/schemas/#schema-save)[] | yes |  |
| `scores` | object[] | yes |  |
| `events` | object[] | yes | Most recent 20 000 of the last 13 months |
| `eventsTruncated` | boolean | no | True when older events were left out |
| `ratings` | object[] | yes | The player's ratings on the site (ADR 0012) |
| `ratings[].game` | string | yes | Game slug |
| `ratings[].stars` | integer | yes | 1 to 5 |
| `ratings[].createdAt` | string (date-time) | yes |  |
| `ratings[].updatedAt` | string (date-time) | yes |  |
| `comments` | object[] | yes | Every comment of the player, hidden ones included |
| `comments[].id` | string (uuid) | yes |  |
| `comments[].game` | string | yes | Game slug |
| `comments[].body` | string | yes |  |
| `comments[].hidden` | boolean | yes |  |
| `comments[].createdAt` | string (date-time) | yes |  |
| `comments[].editedAt` | string (date-time) or null | yes |  |
| `feedback` | object[] | yes | Feedback the player sent with a token |
| `feedback[].id` | string (uuid) | yes |  |
| `feedback[].kind` | [FeedbackKind](/docs/api/reference/schemas/#schema-feedbackkind) | yes |  |
| `feedback[].status` | [FeedbackStatus](/docs/api/reference/schemas/#schema-feedbackstatus) | yes |  |
| `feedback[].message` | string | yes |  |
| `feedback[].game` | string or null | yes | Game slug; null for the site or a deleted game |
| `feedback[].page` | string or null | yes |  |
| `feedback[].createdAt` | string (date-time) | yes |  |

<h2 id="schema-eventcount"><code>EventCount</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `game` | string or null | no |  |
| `name` | string | yes |  |
| `host` | string | yes |  |
| `count` | integer | yes |  |
| `players` | integer | yes |  |

<h2 id="schema-eventdaily"><code>EventDaily</code></h2>

Type: [EventCount](/docs/api/reference/schemas/#schema-eventcount) + object.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `game` | string or null | no |  |
| `name` | string | yes |  |
| `host` | string | yes |  |
| `count` | integer | yes |  |
| `players` | integer | yes |  |
| `day` | string (date) | yes |  |

<h2 id="schema-statstotals"><code>StatsTotals</code></h2>

Counts over a range of days. Players and accounts count rows created in the range.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `players` | integer | yes | Players created |
| `accounts` | integer | yes | Players that linked an email |
| `activePlayers` | integer | yes | Distinct players with a play session |
| `sessions` | integer | yes |  |
| `scores` | integer | yes |  |
| `saves` | integer | yes | Saves written or updated |
| `linksSent` | integer | yes | Magic links created |
| `linksConsumed` | integer | yes |  |
| `events` | integer | yes | Rows of event_daily summed |
| `avgSessionSec` | number or null | yes | Mean of last_seen_at minus started_at |
| `medianSessionSec` | number or null | yes | Median of the same lengths |
| `returningPlayers` | integer | yes | Players of the range with a session on at least two distinct days |
| `retentionD1` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |
| `retentionD7` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |
| `retentionD30` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |

<h2 id="schema-statsday"><code>StatsDay</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `day` | string (date) | yes |  |
| `players` | integer | yes |  |
| `accounts` | integer | yes |  |
| `activePlayers` | integer | yes |  |
| `wau` | integer | yes | Distinct players with a session in the 7 days ending that day |
| `mau` | integer | yes | Distinct players with a session in the 30 days ending that day |
| `returningPlayers` | integer | yes | Active players whose first session is before that day |
| `sessions` | integer | yes |  |
| `scores` | integer | yes |  |
| `saves` | integer | yes |  |
| `linksSent` | integer | yes |  |
| `linksConsumed` | integer | yes |  |
| `events` | integer | yes |  |
| `medianSessionSec` | number or null | yes |  |

<h2 id="schema-statsgamerow"><code>StatsGameRow</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `gameId` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes | The game's internal name |
| `sessions` | integer | yes |  |
| `players` | integer | yes | Distinct players |
| `scores` | integer | yes |  |
| `avgSessionSec` | number or null | no |  |

<h2 id="schema-statskeycount"><code>StatsKeyCount</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | yes |  |
| `count` | integer | yes |  |
| `players` | integer | no | Players, when the dimension has them. For event names: distinct players per day, summed over the range |

<h2 id="schema-statsoverview"><code>StatsOverview</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `from` | string (date) | yes |  |
| `to` | string (date) | yes |  |
| `days` | [StatsDay](/docs/api/reference/schemas/#schema-statsday)[] | yes | One per day of the range |
| `totals` | [StatsTotals](/docs/api/reference/schemas/#schema-statstotals) | yes | Counts over a range of days. Players and accounts count rows created in the range. |
| `previous` | [StatsTotals](/docs/api/reference/schemas/#schema-statstotals) | yes | Counts over a range of days. Players and accounts count rows created in the range. |
| `cohorts` | [StatsCohort](/docs/api/reference/schemas/#schema-statscohort)[] | yes |  |
| `funnel` | [StatsFunnel](/docs/api/reference/schemas/#schema-statsfunnel) | yes | Distinct players of the range at each step |
| `devices` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per device: phone, tablet, desktop, unknown |
| `games` | [StatsGameRow](/docs/api/reference/schemas/#schema-statsgamerow)[] | yes | Every game of the site catalog, most sessions first |
| `hosts` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per host |
| `locales` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Players per locale, all time |
| `events` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | event_daily counts per event name, top 20 |
| `catalog` | object | yes |  |
| `catalog.games` | integer | yes | Games in the site catalog |
| `catalog.visible` | integer | yes |  |
| `catalog.live` | integer | yes | Visible games with a live version |
| `catalog.categories` | integer | yes |  |
| `catalog.tags` | integer | yes |  |
| `generatedAt` | string (date-time) | yes |  |

<h2 id="schema-gamestatsday"><code>GameStatsDay</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `day` | string (date) | yes |  |
| `sessions` | integer | yes |  |
| `players` | integer | yes | Distinct players with a session |
| `wau` | integer | yes | Distinct players with a session in the 7 days ending that day |
| `mau` | integer | yes | Distinct players with a session in the 30 days ending that day |
| `newPlayers` | integer | yes | Players whose first session on this game is that day |
| `returningPlayers` | integer | yes | Players with a session that day and a first session before it |
| `scores` | integer | yes |  |
| `saves` | integer | yes | Saves written or updated |
| `events` | integer | yes |  |
| `avgSessionSec` | number or null | no |  |
| `medianSessionSec` | number or null | yes |  |

<h2 id="schema-gamestatstotals"><code>GameStatsTotals</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `sessions` | integer | yes |  |
| `players` | integer | yes |  |
| `newPlayers` | integer | yes |  |
| `returningPlayers` | integer | yes | Players of the range with a session on at least two distinct days |
| `scores` | integer | yes |  |
| `saves` | integer | yes |  |
| `events` | integer | yes |  |
| `avgSessionSec` | number or null | no |  |
| `medianSessionSec` | number or null | yes |  |
| `retentionD1` | number or null | no | Share (0 to 1) of the range's new players who played again the next day, counting only players whose first day is at least two days before the current UTC day; null below 10 such players |
| `retentionD7` | number or null | no | Share of the range's new players who played again between day 2 and day 7, counting only players whose first day is at least eight days before the current UTC day; null below 10 such players |
| `retentionD30` | number or null | yes | Share of the range's new players who played again between day 8 and day 30, counting only players whose first day is at least 31 days before the current UTC day; null below 10 such players |

<h2 id="schema-gamestatsboard"><code>GameStatsBoard</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes |  |
| `sort` | one of `"desc"`, `"asc"` | yes |  |
| `scores` | integer | yes |  |
| `players` | integer | yes |  |
| `best` | integer or null | no | Best score of the range by the board's sort |
| `median` | number or null | no |  |

<h2 id="schema-gamestats"><code>GameStats</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `gameId` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `from` | string (date) | yes |  |
| `to` | string (date) | yes |  |
| `days` | [GameStatsDay](/docs/api/reference/schemas/#schema-gamestatsday)[] | yes |  |
| `totals` | [GameStatsTotals](/docs/api/reference/schemas/#schema-gamestatstotals) | yes |  |
| `previous` | [GameStatsTotals](/docs/api/reference/schemas/#schema-gamestatstotals) | yes |  |
| `cohorts` | [StatsCohort](/docs/api/reference/schemas/#schema-statscohort)[] | yes |  |
| `funnel` | [StatsFunnel](/docs/api/reference/schemas/#schema-statsfunnel) | yes | Distinct players of the range at each step |
| `hosts` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per host |
| `locales` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per player locale |
| `devices` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per device: phone, tablet, desktop, unknown |
| `boards` | [GameStatsBoard](/docs/api/reference/schemas/#schema-gamestatsboard)[] | yes |  |
| `events` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Per event name, top 20 |
| `versions` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per bundle version |
| `generatedAt` | string (date-time) | yes |  |

<h2 id="schema-webstatsrow"><code>WebStatsRow</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `key` | string | yes |  |
| `visitors` | integer | yes |  |
| `pageviews` | integer | yes |  |

<h2 id="schema-webstats"><code>WebStats</code></h2>

When `configured` is false every other field is absent.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `configured` | boolean | yes | False when the site has no CausalIT read key |
| `domain` | string | no |  |
| `period` | string | no |  |
| `start` | string (date-time) | no |  |
| `end` | string (date-time) | no |  |
| `summary` | object | no |  |
| `summary.visitors` | integer | yes |  |
| `summary.pageviews` | integer | yes |  |
| `summary.sessions` | integer | yes |  |
| `summary.bounceRate` | number or null | no | Percentage, 0 to 100 |
| `summary.avgDurationSec` | number or null | no |  |
| `series` | object[] | no |  |
| `series[].bucket` | string | yes |  |
| `series[].visitors` | integer | yes |  |
| `series[].pageviews` | integer | yes |  |
| `pages` | [WebStatsRow](/docs/api/reference/schemas/#schema-webstatsrow)[] | no |  |
| `sources` | [WebStatsRow](/docs/api/reference/schemas/#schema-webstatsrow)[] | no |  |
| `countries` | [WebStatsRow](/docs/api/reference/schemas/#schema-webstatsrow)[] | no |  |
| `devices` | [WebStatsRow](/docs/api/reference/schemas/#schema-webstatsrow)[] | no |  |
| `goals` | object[] | no |  |
| `goals[].name` | string | yes |  |
| `goals[].kind` | one of `"path"`, `"event"` | yes |  |
| `goals[].pattern` | string | no |  |
| `goals[].conversions` | integer | yes |  |
| `goals[].visitors` | integer | yes |  |
| `goals[].rate` | number or null | no | Conversion rate, percentage, 0 to 100 |

<h2 id="schema-auditentry"><code>AuditEntry</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `actor` | string | yes | The account's email (ADR 0015) |
| `actorEmail` | string or null | no |  |
| `impersonator` | string or null | no | The staff member's email when the row was written under impersonation; null otherwise |
| `site` | string or null | no | Site slug |
| `action` | string | yes |  |
| `targetType` | string | yes |  |
| `targetId` | string | yes |  |
| `before` | object or null | no |  |
| `after` | object or null | no |  |
| `createdAt` | string (date-time) | yes |  |

<h2 id="schema-gametoken"><code>GameToken</code></h2>

What `POST /v1/sdk/game-tokens` and `POST /v1/server/game-tokens` answer (ADR 0013)

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `token` | string | yes | `fg1.<payload>.<hmac>`, valid 24 hours |
| `expiresAt` | string (date-time) | yes |  |

<h2 id="schema-accountname"><code>AccountName</code></h2>

Display name, shown to other members and in the audit log

Type: string; 1 to 80 characters.

<h2 id="schema-newpassword"><code>NewPassword</code></h2>

The policy of `PUT /v1/account/password`: 8 to 128 characters after NFC normalization, not equal to the email

Type: string; 8 to 128 characters; write only.

<h2 id="schema-totpcode"><code>TotpCode</code></h2>

Six digits, "123456" or "123 456"

Type: string; pattern `^[0-9 ]{6,7}$`.

<h2 id="schema-membershiprole"><code>MembershipRole</code></h2>

A member has every right of the owner except membership changes

Type: one of `"owner"`, `"member"`.

<h2 id="schema-scope"><code>Scope</code></h2>

One game or site of the account, for the console's scope switcher

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | one of `"game"`, `"site"` | yes |  |
| `id` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes |
| `approved` | boolean | yes | False while the game or site waits for staff |

<h2 id="schema-consoleme"><code>ConsoleMe</code></h2>

The signed-in account, as `GET /v1/admin/me` and `GET /v1/console/me` answer it (ADR 0015)

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `accountId` | string (uuid) | yes |  |
| `name` | string | yes |  |
| `email` | string (email) | yes |  |
| `staff` | boolean | yes |  |
| `totpEnabled` | boolean | yes |  |
| `canCreateGames` | boolean | yes |  |
| `canCreatePortals` | boolean | yes |  |
| `roles` | object | yes | The identity shape of ADR 0014, computed from `staff` and the memberships |
| `roles.admin` | boolean | yes | True for staff with TOTP enabled |
| `roles.editorOf` | string[] | yes | Slugs of the sites the account is a member of |
| `roles.developerOf` | string[] | yes | Slugs of the games the account is a member of |
| `scopes` | [Scope](/docs/api/reference/schemas/#schema-scope)[] | yes |  |
| `impersonatedBy` | object or null | yes | The staff member behind this session; null outside impersonation |

<h2 id="schema-signinresult"><code>SignInResult</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `totpRequired` | boolean | yes | True when a code is due on `POST /v1/console/sign-in/totp`; `me` is then absent |
| `me` | [ConsoleMe](/docs/api/reference/schemas/#schema-consoleme) | no | The signed-in account, as `GET /v1/admin/me` and `GET /v1/console/me` answer it (ADR 0015) |

<h2 id="schema-totpenrolment"><code>TotpEnrolment</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `secret` | string | yes | Base32, for manual entry; shown once, not active until confirmed |
| `otpauthUrl` | string (uri) | yes | `otpauth://totp/...` for the QR code |

<h2 id="schema-consolesessioninfo"><code>ConsoleSessionInfo</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `current` | boolean | yes | The session that made this request |
| `impersonation` | boolean | yes | Opened by a staff member on this account |
| `userAgent` | string or null | yes | First 300 characters of the browser's User-Agent |
| `createdAt` | string (date-time) | yes |  |
| `lastSeenAt` | string (date-time) | yes | Written at most once a minute |
| `expiresAt` | string (date-time) | yes |  |

<h2 id="schema-invitationpreview"><code>InvitationPreview</code></h2>

What the accept page shows before the invitee sets a name and a password

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `emailMasked` | string | yes | a***@example.com |
| `inviterName` | string or null | yes | Null when the inviter's account is gone |
| `staff` | boolean | yes |  |
| `canCreateGames` | boolean | yes |  |
| `canCreatePortals` | boolean | yes |  |
| `grants` | object[] | yes |  |
| `grants[].kind` | one of `"game"`, `"site"` | yes |  |
| `grants[].slug` | string | yes |  |
| `grants[].name` | string | yes |  |
| `grants[].role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes |
| `expiresAt` | string (date-time) | yes |  |

<h2 id="schema-invitationrequestinput"><code>InvitationRequestInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string (email) | yes | at most 254 characters |
| `wants` | array of one of `"games"`, `"portals"` | yes | What they build; 1 to 2 items; unique items |
| `message` | string | yes | Counted after trim; 1 to 500 characters |

<h2 id="schema-invitationrequeststatus"><code>InvitationRequestStatus</code></h2>

Type: one of `"new"`, `"invited"`, `"dismissed"`.

<h2 id="schema-invitationrequest"><code>InvitationRequest</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `emailMasked` | string | yes |  |
| `email` | string (email) | yes | In clear |
| `wants` | array of one of `"games"`, `"portals"` | yes |  |
| `message` | string | yes |  |
| `status` | [InvitationRequestStatus](/docs/api/reference/schemas/#schema-invitationrequeststatus) | yes |  |
| `handledBy` | string or null | yes | Name of the staff member who invited or dismissed |
| `handledAt` | string (date-time) or null | yes |  |
| `createdAt` | string (date-time) | yes |  |

<h2 id="schema-invitationrequestpage"><code>InvitationRequestPage</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | [InvitationRequest](/docs/api/reference/schemas/#schema-invitationrequest)[] | yes |  |
| `nextCursor` | string or null | yes | Null on the last page |
| `total` | integer | yes | Rows matching the filter |

<h2 id="schema-membership"><code>Membership</code></h2>

One right of an account, as the staff account page lists them

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `kind` | one of `"game"`, `"site"` | yes |  |
| `targetId` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes |
| `approved` | boolean | yes | False while the game or site waits for staff |

<h2 id="schema-accountadmin"><code>AccountAdmin</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `emailMasked` | string | yes |  |
| `email` | string (email) | yes | In clear; staff only |
| `name` | string | yes |  |
| `staff` | boolean | yes |  |
| `totpEnabled` | boolean | yes |  |
| `canCreateGames` | boolean | yes |  |
| `canCreatePortals` | boolean | yes |  |
| `disabledAt` | string (date-time) or null | yes |  |
| `lastSignInAt` | string (date-time) or null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `memberships` | [Membership](/docs/api/reference/schemas/#schema-membership)[] | yes |  |
| `sessions` | integer | yes | Open sessions |

<h2 id="schema-accountpage"><code>AccountPage</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `items` | [AccountAdmin](/docs/api/reference/schemas/#schema-accountadmin)[] | yes |  |
| `nextCursor` | string or null | yes | Null on the last page |
| `total` | integer | yes | Rows matching the search |

<h2 id="schema-accountpatch"><code>AccountPatch</code></h2>

At least one field; an empty body is a 400.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | [AccountName](/docs/api/reference/schemas/#schema-accountname) | no | Display name, shown to other members and in the audit log; 1 to 80 characters |
| `email` | string (email) | no | Changed at once |
| `staff` | boolean | no | A new staff member gets admin rights once TOTP is enabled |
| `canCreateGames` | boolean | no |  |
| `canCreatePortals` | boolean | no |  |
| `disabled` | boolean | no | True revokes every session and refuses sign-in; false enables again |

<h2 id="schema-impersonationstarted"><code>ImpersonationStarted</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `consoleUrl` | string (uri) | yes | The console (`CONSOLE_URL`), to open in a new tab |
| `expiresAt` | string (date-time) | yes | One hour after the start |

<h2 id="schema-invitationgrant"><code>InvitationGrant</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | one of `"game"`, `"site"` | yes |  |
| `targetId` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes |

<h2 id="schema-invitation"><code>Invitation</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `emailMasked` | string | yes |  |
| `staff` | boolean | yes |  |
| `canCreateGames` | boolean | yes |  |
| `canCreatePortals` | boolean | yes |  |
| `grants` | [InvitationGrant](/docs/api/reference/schemas/#schema-invitationgrant)[] | yes |  |
| `createdBy` | string or null | yes | Name of the inviter; null when the account is gone |
| `expiresAt` | string (date-time) | yes |  |
| `acceptedAt` | string (date-time) or null | yes |  |
| `revokedAt` | string (date-time) or null | yes |  |
| `createdAt` | string (date-time) | yes |  |

<h2 id="schema-invitationinput"><code>InvitationInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string (email) | yes | at most 254 characters |
| `staff` | boolean | no | default `false` |
| `canCreateGames` | boolean | no | default `false` |
| `canCreatePortals` | boolean | no | default `false` |
| `grants` | object[] | no | Memberships created on accept. Each item names exactly one of `game` and `site`.; at most 50 items |
| `grants[].game` | string (uuid) | no |  |
| `grants[].site` | string (uuid) | no |  |
| `grants[].role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes |

<h2 id="schema-approvalowner"><code>ApprovalOwner</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `accountId` | string (uuid) | yes |  |
| `name` | string | yes |  |
| `email` | string (email) | yes |  |

<h2 id="schema-approvalgame"><code>ApprovalGame</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `gameId` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `owners` | [ApprovalOwner](/docs/api/reference/schemas/#schema-approvalowner)[] | yes |  |
| `createdAt` | string (date-time) | yes |  |

<h2 id="schema-approvalsite"><code>ApprovalSite</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `owners` | [ApprovalOwner](/docs/api/reference/schemas/#schema-approvalowner)[] | yes |  |
| `pendingDomains` | string[] | yes | The domains the owner proposed. Approving the site does not approve them; they move to the `domains` queue, decided one by one |
| `createdAt` | string (date-time) | yes |  |

<h2 id="schema-approvaldomains"><code>ApprovalDomains</code></h2>

The proposed domains of one approved site

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `siteId` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `domains` | string[] | yes |  |

<h2 id="schema-approvalversion"><code>ApprovalVersion</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `versionId` | string (uuid) | yes |  |
| `gameId` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `version` | string | yes |  |
| `reviewRequestedAt` | string (date-time) | yes |  |
| `createdBy` | string | yes |  |

<h2 id="schema-approvals"><code>Approvals</code></h2>

Every list is oldest first

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `games` | [ApprovalGame](/docs/api/reference/schemas/#schema-approvalgame)[] | yes |  |
| `sites` | [ApprovalSite](/docs/api/reference/schemas/#schema-approvalsite)[] | yes |  |
| `domains` | [ApprovalDomains](/docs/api/reference/schemas/#schema-approvaldomains)[] | yes |  |
| `versions` | [ApprovalVersion](/docs/api/reference/schemas/#schema-approvalversion)[] | yes |  |

<h2 id="schema-rejectinput"><code>RejectInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `reason` | string | yes | Sent to the owners by email; 1 to 1000 characters |

<h2 id="schema-member"><code>Member</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `accountId` | string (uuid) or null | yes | Null while the invitation is not accepted |
| `invitationId` | string (uuid) or null | yes | Set while the invitation is not accepted; `DELETE .../members/invitations/{invitationId}` withdraws it |
| `emailMasked` | string | yes |  |
| `name` | string or null | yes | Null while the invitation is not accepted |
| `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes |
| `invited` | boolean | yes | True for an invitation not accepted yet |
| `createdAt` | string (date-time) | yes |  |

<h2 id="schema-memberinput"><code>MemberInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string (email) | yes | at most 254 characters |
| `role` | [MembershipRole](/docs/api/reference/schemas/#schema-membershiprole) | yes | A member has every right of the owner except membership changes |

<h2 id="schema-sitekeyscope"><code>SiteKeyScope</code></h2>

Each `/v1/server/*` route says which scope it needs

Type: one of `"catalog"`, `"players"`, `"tokens"`, `"leaderboards"`, `"stats"`.

<h2 id="schema-sitekey"><code>SiteKey</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `name` | string | yes |  |
| `prefix` | string | yes | The first 12 characters of the key, to tell keys apart |
| `scopes` | [SiteKeyScope](/docs/api/reference/schemas/#schema-sitekeyscope)[] | yes |  |
| `createdBy` | string or null | yes | Name of the account that created it |
| `createdAt` | string (date-time) | yes |  |
| `lastUsedAt` | string (date-time) or null | yes | Written at most once a minute |
| `revokedAt` | string (date-time) or null | yes |  |

<h2 id="schema-sitekeycreated"><code>SiteKeyCreated</code></h2>

Type: [SiteKey](/docs/api/reference/schemas/#schema-sitekey) + object.

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `name` | string | yes |  |
| `prefix` | string | yes | The first 12 characters of the key, to tell keys apart |
| `scopes` | [SiteKeyScope](/docs/api/reference/schemas/#schema-sitekeyscope)[] | yes |  |
| `createdBy` | string or null | yes | Name of the account that created it |
| `createdAt` | string (date-time) | yes |  |
| `lastUsedAt` | string (date-time) or null | yes | Written at most once a minute |
| `revokedAt` | string (date-time) or null | yes |  |
| `key` | string | yes | `fsk_<site slug>_<32 base62>`. Shown once; the API stores a hash |

<h2 id="schema-sitekeyinput"><code>SiteKeyInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | 1 to 60 characters |
| `scopes` | [SiteKeyScope](/docs/api/reference/schemas/#schema-sitekeyscope)[] | yes | at least 1 item; unique items |

<h2 id="schema-retentionshare"><code>RetentionShare</code></h2>

Share of new players who played again in the window; null below 10 players old enough to be counted

Type: number or null; 0 to 1.

<h2 id="schema-statscohort"><code>StatsCohort</code></h2>

Players by the ISO week of their first play

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `week` | string | yes | ISO week; pattern `^\d{4}-W\d{2}$` |
| `players` | integer | yes | Players whose first session is in that week |
| `d1` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |
| `d7` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |
| `d30` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |

<h2 id="schema-statsfunnel"><code>StatsFunnel</code></h2>

Distinct players of the range at each step

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `loaded` | integer | yes | Sent `frame_loaded` from the hub |
| `started` | integer | yes | Opened a play session |
| `saved` | integer | yes | Wrote a save |
| `scored` | integer | yes | Sent a score |
| `linked` | integer | yes | Linked an email |

<h2 id="schema-consolestatsday"><code>ConsoleStatsDay</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `day` | string (date) | yes |  |
| `dau` | integer | yes | Distinct players with a session that day |
| `wau` | integer | yes | Distinct players with a session in the 7 days ending that day |
| `mau` | integer | yes | Distinct players with a session in the 30 days ending that day |
| `newPlayers` | integer | yes | Players whose first session on this game is that day |
| `returningPlayers` | integer | yes | Players with a session that day and a first session before it |
| `sessions` | integer | yes |  |
| `scores` | integer | yes |  |
| `saves` | integer | yes | Saves written or updated |
| `events` | integer | yes |  |
| `meanSessionSec` | number or null | yes |  |
| `medianSessionSec` | number or null | yes |  |

<h2 id="schema-consolestatstotals"><code>ConsoleStatsTotals</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `players` | integer | yes | Distinct players with a session |
| `newPlayers` | integer | yes |  |
| `returningPlayers` | integer | yes | Players of the range with a session on at least two distinct days |
| `sessions` | integer | yes |  |
| `scores` | integer | yes |  |
| `saves` | integer | yes |  |
| `events` | integer | yes |  |
| `meanSessionSec` | number or null | yes |  |
| `medianSessionSec` | number or null | yes |  |
| `retentionD1` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |
| `retentionD7` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |
| `retentionD30` | [RetentionShare](/docs/api/reference/schemas/#schema-retentionshare) or null | yes | Share of new players who played again in the window; null below 10 players old enough to be counted; 0 to 1 |

<h2 id="schema-consolegamestats"><code>ConsoleGameStats</code></h2>

A game's numbers across every site that lists it (ADR 0015)

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `gameId` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `from` | string (date) | yes |  |
| `to` | string (date) | yes |  |
| `days` | [ConsoleStatsDay](/docs/api/reference/schemas/#schema-consolestatsday)[] | yes |  |
| `totals` | [ConsoleStatsTotals](/docs/api/reference/schemas/#schema-consolestatstotals) | yes |  |
| `previous` | [ConsoleStatsTotals](/docs/api/reference/schemas/#schema-consolestatstotals) | yes |  |
| `cohorts` | [StatsCohort](/docs/api/reference/schemas/#schema-statscohort)[] | yes |  |
| `funnel` | [StatsFunnel](/docs/api/reference/schemas/#schema-statsfunnel) | yes | Distinct players of the range at each step |
| `sites` | object[] | yes | Every site that lists the game, most sessions first |
| `sites[].siteId` | string (uuid) | yes |  |
| `sites[].slug` | string | yes |  |
| `sites[].name` | string | yes |  |
| `sites[].sessions` | integer | yes |  |
| `sites[].players` | integer | yes | Distinct players |
| `hosts` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per host |
| `versions` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per bundle version |
| `locales` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per player locale |
| `devices` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Sessions per device: phone, tablet, desktop, unknown |
| `boards` | [GameStatsBoard](/docs/api/reference/schemas/#schema-gamestatsboard)[] | yes |  |
| `events` | [StatsKeyCount](/docs/api/reference/schemas/#schema-statskeycount)[] | yes | Per event name with distinct players, top 20 |
| `generatedAt` | string (date-time) | yes |  |

<h2 id="schema-serverplayerinput"><code>ServerPlayerInput</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `externalId` | string | yes | Your own user id; 1 to 128 characters |
| `nickname` | string | no | 3 to 20 characters; pattern `^[\p{L}\p{N} _.-]+$` |
| `locale` | string | no | Defaults to the site's first locale |

<h2 id="schema-serverplayer"><code>ServerPlayer</code></h2>

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `externalId` | string | yes |  |
| `nickname` | string | yes |  |
| `locale` | string | yes |  |
| `hasAccount` | boolean | yes | True once the player linked an email on your site |
| `createdAt` | string (date-time) | yes |  |
| `lastSeenAt` | string (date-time) | yes |  |
