Gamers Lab Docs

3.2 Player Profiles

Section 3 — Player Management

3.2 Player Profiles

Scope

Defines who can access player profile endpoints, what credential is required, and what data is returned for each caller type and visibility setting. Covers both self-management endpoints (/api/player-profile) and external lookup endpoints (/api/player-profiles).

Caller Types

CallerCredentialWhere it comes from
Player (self)Authorization: Bearer <player_jwt>Issued by any player auth login flow
Game serverX-Game-Key: <key>Issued per tenant, write-capable, tenant-scoped
Dashboard / third-party appX-API-Key: <key>Issued per tenant, read-only, must have allow_data_api flag
AnonymousNoneNo access to any profile endpoint

Game keys and API keys are fundamentally different — see TYPES_OF_KEYS.md for full details.

Profile Visibility Settings

Players control their own visibility via PATCH /api/player-profile/me.

ValueMeaning
privateOpted out of global external visibility. Game keys still see id only within the player's tenant unless tenant-specific opt-out is active; API keys see nothing (404).
limitedDefault. Display name and avatar are visible to external callers.
fullFully visible. External callers also receive tenant access history.

Visibility only affects external lookup (/api/player-profiles/\{id\}). It has no effect on /api/player-profile/me — players always see their own full profile.

Tenant-specific discovery opt-out is a separate rule stored on game_player_tenant_access. It is narrower than profileVisibility: it hides the player only from tenant-scoped discovery surfaces for one tenant, without affecting other tenants or the player's own account access.

Tenant Opt Out

Tenant opt-out is a per-tenant discovery rule. It is separate from global profileVisibility.

What it does

When tenant opt-out is active for a specific (playerId, tenantId) pair, tenant-scoped discovery surfaces for that tenant should treat the player as not discoverable and return 404.

This is stricter than profileVisibility=private:

  • profileVisibility=private still allows game-key callers in the player's tenant to receive PlayerProfileMinimalDTO with id only.
  • tenant opt-out is intended to suppress even that tenant-scoped existence confirmation.

What it does not affect

Tenant opt-out does not affect:

  • other tenants
  • /api/player-profile/me
  • internal/admin access
  • POST /api/player-auth/players/exists for game-key registration/existence checks
  • merge resolution
  • global non-tenant-scoped visibility rules

How to set it

Players can set or clear tenant opt-out with:

PUT /api/player-profile/me/bus_tenants/\{tenantId\}/opt-out

Request body:

{
  "isOptedOut": true
}

Use true to hide the player from tenant-scoped discovery for that tenant. Use false to re-allow tenant-scoped discovery for that tenant.

The player must already have a tenant access record for that tenant. This endpoint does not create tenant membership on its own.

Current implementation note

Tenant opt-out is enforced on tenant-scoped discovery paths. If a player is hidden for a specific tenant, those endpoints should behave as if the player is not discoverable there.

Endpoint Permission Tables

GET /api/player-profile/me

Self-lookup. Always returns FullPlayerProfileDTO.

CallerAllowedNotes
Player JWTYesMust be a scope=player JWT. Returns own profile only.
Game KeyNo401
API KeyNo401
AnonymousNo401

Returns (FullPlayerProfileDTO):

id, displayName, avatarUrl, email, platformRole, profileVisibility,
createdAt, isActive, mergedIntoId, mergedProfileIds,
authMethods[], tenantAccess[]

Visibility setting has no effect — this is always the full view.


GET /api/player-profile/me/portal

Aggregated self-service landing-page read for the authenticated player.

CallerAllowedNotes
Player JWTYesMust be a scope=player JWT. Returns own portal view only.
Game KeyNo401
API KeyNo401
AnonymousNo401

Returns (PlayerPortalResponseDTO):

profile
  id, displayName, avatarUrl, email, profileVisibility,
  platformRole, createdAt, isActive, authMethods[]

visibilityOptions[]
  value, label, description

tenants[]
  tenantId, tenantName, tenantSlug, tenantRole,
  firstSeenAt, lastSeenAt, loginCount, isOptedOut,
  banStatus
    isCurrentlyBanned
    bannedUntil
    isPermanent
    statusText

Player-facing ban safety: this view exposes only current player-safe ban state. It does not expose bannedByUserId, internal moderation metadata, or raw admin-only ban fields.

Visibility guidance: the response includes backend-driven visibility option labels and descriptions so client apps can explain private, limited, and full consistently.

This is a separate self-service profile view DTO. It is not the login response from POST /api/player-portal-auth/otc/exchange, which returns PlayerPortalLoginResponse.


GET /api/player-profiles/{id}

External lookup. Response shape depends on caller type and the player's visibility setting. Provide exactly one of X-Game-Key or X-API-Key. Requests that send both are rejected with 400.

Game Key path (X-Game-Key)

The game key is tenant-scoped. Only players who have a game_player_tenant_access record for this key's tenant are reachable. A player in a different game entirely returns 404.

Lookup resolves merged profile IDs to the current active profile before visibility is applied. If an old merged ID is used, the response id will be the current canonical profile ID, not necessarily the requested ID.

Player visibilityResponseShape
private200PlayerProfileMinimalDTOid, profileVisibility only, unless tenant-specific opt-out is active
limited200PlayerProfileSummaryDTOid, displayName, avatarUrl, profileVisibility
full200TenantScopedPlayerProfilePublicDTO — summary + this tenant's access record only
Not in tenant404Player not found in this tenant (regardless of visibility)
Tenant-specific opt-out active404Player is hidden from tenant-scoped discovery for this tenant
Player does not exist404

Why private still returns id for game keys: the game server enrolled this player through its own login flow. It already knows the player exists. Returning id lets servers correlate records without leaking profile data.

Tenant-specific opt-out boundary: when tenant-specific opt-out is active for this tenant, game-key callers should also receive 404. This is stricter than global profileVisibility and is intended to remove the player from all tenant-scoped discovery surfaces for that tenant.

Why full only shows this tenant's access record: game servers have context for their own game only. All-tenant access history from other games is not their concern.

Game-key tenant access shape: the tenant access record returned on the game-key path omits isOptedOut. Tenant opt-out is an internal privacy control, not game-client profile data.

API Key path (X-API-Key, requires allow_data_api)

API keys are tenant-scoped for player profile lookup. They can look up a player by ID only when that player has a visible game_player_tenant_access record for the API key's tenant. Profile visibility is also enforced strictly.

Lookup resolves merged profile IDs to the current active profile before visibility is applied.

Player visibilityResponseShape
private404Player has opted out — existence is not revealed
limited200PlayerProfileSummaryDTOid, displayName, avatarUrl, profileVisibility
full200PlayerProfilePublicDTO — summary + this API key tenant's access record only
Not in API key tenant404Player not found for this tenant (regardless of visibility)
Hidden from tenant discovery404Player is hidden from tenant-scoped discovery for this tenant
Player does not exist404
API key missing allow_data_api403Key does not permit data access

POST /api/player-profiles/bulk

Bulk public player profile lookup using an API key.

POST /api/player-profiles/bulk
X-API-Key: <api_key_with_allow_data_api>
Content-Type: application/json

Request body:

{
  "playerIds": ["<uuid>", "<uuid>"]
}
CallerAllowed
API key with allow_data_apiYes
Game keyNo — 400
Missing/invalid keyNo — 401
API key missing allow_data_apiNo — 403

Accepts up to 100 player IDs. Duplicate IDs are de-duplicated before processing. Requests can return 429 when the profile lookup rate limit is exceeded.

Private profiles and profiles without visible access in the API key tenant are returned in notFound, intentionally indistinguishable from missing profiles. Limited profiles return basic identity fields; full profiles include only the API key tenant's visible access record.

Returns (BulkPlayerProfileLookupResponse):

items[]             PlayerProfileBulkLookupItemDTO[]
notFound            Guid[]
requestedCount      int
processedCount      int
returnedCount       int

Each returned item uses the same external shape as API-key single-profile lookup: id, displayName, avatarUrl, profileVisibility, and tenantAccess[].


GET /api/player-profile/{id} — DISABLED

This endpoint has been commented out. It accepted any valid JWT (player or SaaS) and returned FullPlayerProfileDTO with no ownership check or visibility gating.

Players looking up their own profile should use /api/player-profile/me. External lookups should use /api/player-profiles/\{id\} with a game key or API key.


PATCH /api/player-profile/me

Update own profile fields.

CallerAllowed
Player JWTYes — own profile only
Any otherNo — 401

Updatable fields: displayName, avatarUrl, email, profileVisibility

Visibility values: private, limited, full

Separate tenant-scoped opt-out: per-tenant discovery opt-out is not controlled by profileVisibility. It is tracked separately on game_player_tenant_access and is meant to hide the player from discovery within one tenant only.


GET /api/player-profile/me/bus_tenants

List all games/tenants the authenticated player has logged into.

CallerAllowed
Player JWTYes — own records only
Any otherNo — 401

Returns: TenantAccessDTO[]tenantId, tenantRole, firstSeenAt, lastSeenAt, loginCount, isOptedOut Includes records from any merged profiles.


PUT /api/player-profile/me/bus_tenants/{tenantId}/opt-out

Set or clear tenant discovery opt-out for the authenticated player within one tenant.

CallerAllowed
Player JWTYes — own tenant access only
Any otherNo — 401

Request body: SetTenantOptOutRequest — required isOptedOut

Returns: TenantOptOutStatusDTOtenantId, isOptedOut

Not Found: returned when the player does not have a tenant access record for that tenant.


POST /api/player-profile/me/auth-methods

Link an additional authentication provider to the player's profile.

CallerAllowed
Player JWTYes
Any otherNo — 401

Validates the provider token live. If the provider user ID is already linked to a different profile, the link is rejected — use the merge flow instead.


DELETE /api/player-profile/me/auth-methods/{authMethodId}

Unlink an authentication provider from the player's profile.

CallerAllowed
Player JWTYes — own auth methods only
Any otherNo — 401

Cannot remove the last remaining auth method. If the removed method was the primary, another method is automatically promoted to primary.


POST /api/player-profile/me/auth-methods/{authMethodId}/set-primary

Set a linked provider as the primary authentication method.

CallerAllowed
Player JWTYes — own auth methods only
Any otherNo — 401

POST /api/player-profile/me/merge

Merge two profiles into one. The authenticated player's profile is the target (survives). The source profile becomes inactive.

CallerAllowed
Player JWTYes
Any otherNo — 401

Requires: sourceProfileId, sourceProvider, sourceAuthToken — a live provider token proving ownership of the source profile. The token is validated and matched against the source profile's auth methods before the merge proceeds.

What is transferred from source to target:

  • All linked auth methods
  • All tenant access records

Source profile after merge: isActive=false, isBackMerged=true, mergedIntoId set to target. Target profile after merge: mergedProfileIds updated to include source and any profiles source had previously absorbed.


Response DTO Reference

FullPlayerProfileDTO — player self-view only

id                  Guid
displayName         string?
avatarUrl           string?
email               string?
platformRole        string
profileVisibility   string
createdAt           DateTime
isActive            bool
mergedIntoId        Guid?
mergedProfileIds    Guid[]
authMethods[]
  id                Guid
  authProvider      string
  providerUserId    string
  email             string?
  username          string?
  displayName       string?
  avatarUrl         string?
  isPrimary         bool
  linkedAt          DateTime
  lastUsedAt        DateTime?
tenantAccess[]
  tenantId          Guid
  tenantRole        string
  firstSeenAt       DateTime
  lastSeenAt        DateTime
  loginCount        int
  isOptedOut        bool

TenantScopedPlayerProfilePublicDTO — full visibility, game key callers

id                  Guid
displayName         string?
avatarUrl           string?
profileVisibility   string
tenantAccess[]
  tenantId          Guid
  tenantRole        string
  firstSeenAt       DateTime
  lastSeenAt        DateTime
  loginCount        int

PlayerProfilePublicDTO — full visibility, API key callers

id                  Guid
displayName         string?
avatarUrl           string?
profileVisibility   string
tenantAccess[]      (API key tenant only)
  tenantId          Guid
  tenantRole        string
  firstSeenAt       DateTime
  lastSeenAt        DateTime
  loginCount        int
  isOptedOut        bool

PlayerProfileSummaryDTO — limited visibility, external callers

id                  Guid
displayName         string?
avatarUrl           string?
profileVisibility   string

PlayerProfileMinimalDTO — private + game key only

id                  Guid
profileVisibility   string

Fields Never Exposed to External Callers

Regardless of visibility setting, these fields are only returned via /api/player-profile/me:

  • email — PII; game owners should not have access to player emails without explicit consent
  • authMethods — linked provider accounts and provider user IDs
  • platformRole — internal platform classification
  • createdAt — account creation timestamp
  • isActive — merge/active status
  • mergedIntoId / mergedProfileIds — internal merge chain pointers

Note: external callers do not receive merge pointer fields, but a lookup by an old merged profile ID currently resolves to the active profile and returns that active profile's id.

Error Codes

CodeMeaning
400Invalid request payload, invalid visibility value, link/merge failure, auth method not found, or unlink blocked
401Missing or invalid credential (JWT, game key, or API key)
403API key exists but lacks allow_data_api permission
404Player not found, not in this tenant, tenant-scoped opt-out active, or private + API key caller

Known Limitations

Atomic Merge Operation (TODO): Current merge behavior spans multiple independent write operations (transfer auth methods, transfer tenant access, mark source inactive, update target mergedProfileIds) without a single transaction boundary. A mid-operation failure can leave the merge partially applied.

Atomic Primary Auth Method Mutations (TODO): Auth method mutations (link with setAsPrimary=true, unlink primary, set-primary) are multi-step operations without a single transaction and can leave a profile with no primary or transiently inconsistent primary state.

Code References

  • Lookup controller: GamersLabRestAPI/PlayerAuth/Controllers/PlayerProfileLookupController.cs
  • Self-management controller: GamersLabRestAPI/PlayerAuth/Controllers/PlayerProfileController.cs
  • Profile service: GamersLabRestAPI/PlayerAuth/Services/PlayerProfileService.cs
  • Profile DTOs: GamersLabRestAPI/PlayerAuth/DTOs/PlayerProfileDTOs.cs
  • Visibility constants: GamersLabRestAPI/PlayerAuth/Models/PlayerProfile.cs (PlayerProfileVisibilityValues)
  • Tenant scoped lookup: IPlayerProfileService.GetTenantScopedProfileAsync

On this page