openapi: 3.0.3 info: title: Galaxy User Service Internal REST API version: v1 description: | This specification documents the trusted internal REST contract of `galaxy/user`. The current runtime is implemented as an internal-only HTTP service backed by Redis. Scope: - regular-user state only; system-admin identity belongs to future `Admin Service` - auth-facing user resolution, ensure, existence, and subject blocking - gateway-facing authenticated account reads and self-service mutations - lobby-facing eligibility snapshots - geo-facing declared-country synchronization - admin/internal reads, filtered listing, and explicit mutation commands This specification is internal REST only. It intentionally does not describe public edge transport, gateway gRPC, or the auxiliary async event contracts documented in `README.md` and `docs/flows.md`. The auth-facing paths listed under `AuthIntegration` are already reserved by `Auth / Session Service` and their route shapes must remain stable. Current transport rules: - request bodies are strict JSON only - unknown fields are rejected - trailing JSON input is rejected - error responses use `{ "error": { "code", "message" } }` - stable error codes are `invalid_request`, `conflict`, `subject_not_found`, `internal_error`, and `service_unavailable` servers: - url: http://localhost:8091 description: Default local internal listener for User Service. tags: - name: AuthIntegration description: Trusted auth-facing user ownership and block-policy endpoints with frozen route shapes reserved by `Auth / Session Service`. - name: MyAccount description: Gateway-facing authenticated account queries and self-service mutations. - name: LobbyIntegration description: Trusted lobby-facing synchronous eligibility reads. - name: GeoIntegration description: Trusted geo-facing declared-country synchronization. - name: AdminUsers description: Trusted administrative lookup, listing, and explicit mutation commands. paths: /api/v1/internal/user-resolutions/by-email: post: tags: - AuthIntegration operationId: resolveUserByEmail summary: Resolve one e-mail address without creating a user requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UserResolutionByEmailRequest" responses: "200": description: Current coarse user-resolution state for the e-mail subject. content: application/json: schema: $ref: "#/components/schemas/UserResolutionByEmailResponse" "400": $ref: "#/components/responses/InvalidRequestError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/exists: get: tags: - AuthIntegration operationId: userExistsByID summary: Check whether a stable user identifier exists parameters: - $ref: "#/components/parameters/UserIDPath" responses: "200": description: Existence check result for the supplied `user_id`. content: application/json: schema: $ref: "#/components/schemas/UserExistsResponse" "400": $ref: "#/components/responses/InvalidRequestError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/ensure-by-email: post: tags: - AuthIntegration operationId: ensureUserByEmail summary: Resolve, create, or block one e-mail subject description: | Returns an existing user for `email`, creates a new regular platform user when registration is allowed, or returns a blocked outcome when policy denies the flow. `registration_context` is required on the current auth-to-user call. Its frozen shape is `preferred_language` plus `time_zone`. The registration context is create-only. Implementations must ignore it for existing users and must not overwrite settings of an already existing account. During the current rollout `Auth / Session Service` sends temporary `preferred_language="en"` and forwards the public confirm `time_zone`. Gateway-side geoip language derivation is a later rollout and is not part of the current source-of-truth contract. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/EnsureByEmailRequest" responses: "200": description: Ensure-user outcome for the supplied `email`. content: application/json: schema: $ref: "#/components/schemas/EnsureByEmailResponse" "400": $ref: "#/components/responses/InvalidRequestError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/block: post: tags: - AuthIntegration operationId: blockUserByID summary: Block one user by stable user identifier parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/BlockUserByIDRequest" responses: "200": description: The block mutation applied or the subject was already blocked. content: application/json: schema: $ref: "#/components/schemas/BlockMutationResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/user-blocks/by-email: post: tags: - AuthIntegration operationId: blockUserByEmail summary: Block one e-mail subject even when no user exists yet requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/BlockUserByEmailRequest" responses: "200": description: The block mutation applied or the subject was already blocked. content: application/json: schema: $ref: "#/components/schemas/BlockMutationResponse" "400": $ref: "#/components/responses/InvalidRequestError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/account: get: tags: - MyAccount operationId: getMyAccount summary: Read one authenticated regular-user account aggregate parameters: - $ref: "#/components/parameters/UserIDPath" responses: "200": description: Read-optimized account aggregate for the supplied `user_id`. content: application/json: schema: $ref: "#/components/schemas/GetMyAccountResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/profile: post: tags: - MyAccount operationId: updateMyProfile summary: Update self-service profile fields description: | `race_name` uniqueness is enforced through a canonical reservation policy that is case-insensitive, rejects the frozen anti-fraud confusable pairs, and preserves the original stored casing. parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateMyProfileRequest" responses: "200": description: Updated account aggregate after the profile mutation commits. content: application/json: schema: $ref: "#/components/schemas/GetMyAccountResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/settings: post: tags: - MyAccount operationId: updateMySettings summary: Update self-service settings fields parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateMySettingsRequest" responses: "200": description: Updated account aggregate after the settings mutation commits. content: application/json: schema: $ref: "#/components/schemas/GetMyAccountResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/eligibility: get: tags: - LobbyIntegration operationId: getUserEligibility summary: Read one synchronous lobby-facing eligibility snapshot description: | Returns a read-optimized snapshot for lobby decisions. Unknown users are represented as `exists=false` instead of `404`. parameters: - $ref: "#/components/parameters/UserIDPath" responses: "200": description: Eligibility snapshot for the supplied `user_id`. content: application/json: schema: $ref: "#/components/schemas/UserEligibilityResponse" "400": $ref: "#/components/responses/InvalidRequestError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/declared-country/sync: post: tags: - GeoIntegration operationId: syncDeclaredCountry summary: Synchronize the current effective declared country description: | Applies the latest effective declared country chosen by `Geo Profile Service`. `declared_country` must be a known uppercase ISO 3166-1 alpha-2 country code. When the supplied value is already stored on the user account, the command is a no-op and returns the existing `updated_at` unchanged. parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SyncDeclaredCountryRequest" responses: "200": description: Declared-country synchronization applied successfully. content: application/json: schema: $ref: "#/components/schemas/DeclaredCountrySyncResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}: get: tags: - AdminUsers operationId: getUserByID summary: Read one user by stable user identifier parameters: - $ref: "#/components/parameters/UserIDPath" responses: "200": description: Exact user lookup result for the supplied `user_id`. content: application/json: schema: $ref: "#/components/schemas/UserLookupResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/user-lookups/by-email: post: tags: - AdminUsers operationId: getUserByEmail summary: Read one user by exact-after-trim e-mail requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UserLookupByEmailRequest" responses: "200": description: Exact user lookup result for the supplied `email`. content: application/json: schema: $ref: "#/components/schemas/UserLookupResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/user-lookups/by-race-name: post: tags: - AdminUsers operationId: getUserByRaceName summary: Read one user by exact race name requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UserLookupByRaceNameRequest" responses: "200": description: Exact user lookup result for the supplied `race_name`. content: application/json: schema: $ref: "#/components/schemas/UserLookupResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users: get: tags: - AdminUsers operationId: listUsers summary: List users with deterministic pagination and rich filters description: | Returns full user account aggregates ordered by `created_at desc`, then `user_id desc`. All supplied query filters combine with logical `AND`. `page_token` is opaque and bound to the normalized filter set that produced it. Malformed or filter-mismatched tokens return `400 invalid_request`. parameters: - $ref: "#/components/parameters/PageSize" - $ref: "#/components/parameters/PageToken" - name: paid_state in: query description: Filter by current free or paid state. schema: type: string enum: - free - paid - name: paid_expires_before in: query description: Filter to users whose paid entitlement expires before this RFC 3339 timestamp. schema: type: string format: date-time - name: paid_expires_after in: query description: Filter to users whose paid entitlement expires after this RFC 3339 timestamp. schema: type: string format: date-time - name: declared_country in: query description: Filter by the current effective declared country. schema: $ref: "#/components/schemas/CountryCode" - name: sanction_code in: query description: Filter by one active sanction code. schema: $ref: "#/components/schemas/SanctionCode" - name: limit_code in: query description: Filter by one active limit code. schema: $ref: "#/components/schemas/LimitCode" - name: can_login in: query description: Filter by the derived login eligibility marker. schema: type: boolean - name: can_create_private_game in: query description: Filter by the derived private-game creation eligibility marker. schema: type: boolean - name: can_join_game in: query description: Filter by the derived game-join eligibility marker. schema: type: boolean responses: "200": description: Deterministically ordered page of users. content: application/json: schema: $ref: "#/components/schemas/UserListResponse" "400": $ref: "#/components/responses/InvalidRequestError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/entitlements/grant: post: tags: - AdminUsers operationId: grantEntitlement summary: Grant a new entitlement period description: | Grants a current paid entitlement when the current effective state is `free`. parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/GrantEntitlementRequest" responses: "200": description: Entitlement grant applied successfully. content: application/json: schema: $ref: "#/components/schemas/EntitlementCommandResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/entitlements/extend: post: tags: - AdminUsers operationId: extendEntitlement summary: Extend the current entitlement period description: | Extends the current finite paid entitlement. parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExtendEntitlementRequest" responses: "200": description: Entitlement extension applied successfully. content: application/json: schema: $ref: "#/components/schemas/EntitlementCommandResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/entitlements/revoke: post: tags: - AdminUsers operationId: revokeEntitlement summary: Revoke the effective paid entitlement description: | Revokes the current effective paid entitlement. parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RevokeEntitlementRequest" responses: "200": description: Entitlement revocation applied successfully. content: application/json: schema: $ref: "#/components/schemas/EntitlementCommandResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/sanctions/apply: post: tags: - AdminUsers operationId: applySanction summary: Apply one sanction record description: | Applies one new active sanction record. parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ApplySanctionRequest" responses: "200": description: Sanction application applied successfully. content: application/json: schema: $ref: "#/components/schemas/SanctionCommandResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/sanctions/remove: post: tags: - AdminUsers operationId: removeSanction summary: Remove one active sanction record description: | Removes the current active sanction for one `sanction_code`. parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RemoveSanctionRequest" responses: "200": description: Sanction removal applied successfully. content: application/json: schema: $ref: "#/components/schemas/SanctionCommandResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/limits/set: post: tags: - AdminUsers operationId: setLimit summary: Set one active user-specific limit record description: | Creates one new active limit or replaces the current active record of the same `limit_code`. parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SetLimitRequest" responses: "200": description: User-specific limit set successfully. content: application/json: schema: $ref: "#/components/schemas/LimitCommandResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" /api/v1/internal/users/{user_id}/limits/remove: post: tags: - AdminUsers operationId: removeLimit summary: Remove one active user-specific limit record description: | Removes the current active user-specific limit for one `limit_code`. parameters: - $ref: "#/components/parameters/UserIDPath" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RemoveLimitRequest" responses: "200": description: User-specific limit removal applied successfully. content: application/json: schema: $ref: "#/components/schemas/LimitCommandResponse" "400": $ref: "#/components/responses/InvalidRequestError" "404": $ref: "#/components/responses/SubjectNotFoundError" "409": $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" components: parameters: UserIDPath: name: user_id in: path required: true description: Stable regular-user identifier owned by User Service. schema: $ref: "#/components/schemas/UserID" PageSize: name: page_size in: query description: Maximum number of users returned in one page. schema: type: integer minimum: 1 maximum: 200 default: 50 PageToken: name: page_token in: query description: Opaque deterministic pagination cursor returned by the previous page and bound to the normalized filter set that produced it. Malformed or filter-mismatched tokens return `400 invalid_request`. schema: type: string schemas: UserID: type: string description: Stable regular-user identifier. minLength: 1 Email: type: string format: email description: | Login and contact e-mail address. The service trims surrounding whitespace and validates the value structurally, then treats the trimmed value as the exact stored and lookup value. The service does not lowercase or otherwise canonicalize e-mail before storage or exact lookup. RaceName: type: string description: | Stored race name preserving the user-selected casing after successful uniqueness checks. Uniqueness is enforced against a canonical reservation key rather than exact string equality only. minLength: 1 maxLength: 64 LanguageTag: type: string description: | BCP 47 language tag. User Service validates semantic correctness on auth-driven creation and stores the canonical tag form. minLength: 1 maxLength: 32 TimeZoneName: type: string description: | IANA time zone name. User Service validates semantic correctness on auth-driven creation and stores the trimmed caller value without additional alias canonicalization. minLength: 1 maxLength: 128 CountryCode: type: string description: | ISO 3166-1 alpha-2 country code in uppercase ASCII form. The geo sync command additionally rejects well-formed but unknown region codes. pattern: "^[A-Z]{2}$" UserResolutionKind: type: string enum: - existing - creatable - blocked EnsureUserOutcome: type: string enum: - existing - created - blocked BlockUserOutcome: type: string enum: - blocked - already_blocked PlanCode: type: string enum: - free - paid_monthly - paid_yearly - paid_lifetime SanctionCode: type: string enum: - login_block - private_game_create_block - private_game_manage_block - game_join_block - profile_update_block LimitCode: type: string description: | Current supported user-specific limit codes. Retired legacy codes may still exist in stored history for backward compatibility, but they are not part of this write or read contract. enum: - max_owned_private_games - max_pending_public_applications - max_active_game_memberships ActorRef: type: object additionalProperties: false required: - type properties: type: type: string description: Machine-readable actor type such as `admin`, `service`, or `billing`. id: type: string description: Optional stable actor identifier. RegistrationContext: type: object description: | Frozen create-only initialization context used by the current auth-facing ensure-by-email contract. `preferred_language` is semantically validated as BCP 47 and stored in canonical tag form on create. `time_zone` is semantically validated as an IANA time zone name and stored after trim without additional alias canonicalization. additionalProperties: false required: - preferred_language - time_zone properties: preferred_language: $ref: "#/components/schemas/LanguageTag" description: | Create-only initial preferred language. During the current rollout `Auth / Session Service` sends a temporary `"en"` default and forwards `time_zone`. Gateway-side geoip derivation is not part of the current source-of-truth contract. Future derived values must remain valid BCP 47 tags. time_zone: $ref: "#/components/schemas/TimeZoneName" description: Create-only initial IANA time zone name. UserResolutionByEmailRequest: type: object additionalProperties: false required: - email properties: email: $ref: "#/components/schemas/Email" UserResolutionByEmailResponse: type: object additionalProperties: false required: - kind properties: kind: $ref: "#/components/schemas/UserResolutionKind" user_id: $ref: "#/components/schemas/UserID" block_reason_code: type: string description: Present only for `kind=blocked`. UserExistsResponse: type: object additionalProperties: false required: - exists properties: exists: type: boolean EnsureByEmailRequest: type: object additionalProperties: false required: - email - registration_context properties: email: $ref: "#/components/schemas/Email" registration_context: $ref: "#/components/schemas/RegistrationContext" EnsureByEmailResponse: type: object additionalProperties: false required: - outcome properties: outcome: $ref: "#/components/schemas/EnsureUserOutcome" user_id: $ref: "#/components/schemas/UserID" description: | Present for `existing` and `created`. A `created` outcome returns the durable newly materialized `user_id` created together with an initial generated `player-` race name and free entitlement snapshot. block_reason_code: type: string description: Present only for `outcome=blocked`. BlockUserByIDRequest: type: object additionalProperties: false required: - reason_code properties: reason_code: type: string BlockUserByEmailRequest: type: object additionalProperties: false required: - email - reason_code properties: email: $ref: "#/components/schemas/Email" reason_code: type: string BlockMutationResponse: type: object additionalProperties: false required: - outcome properties: outcome: $ref: "#/components/schemas/BlockUserOutcome" user_id: $ref: "#/components/schemas/UserID" EntitlementSnapshot: type: object description: | Materialized current effective entitlement snapshot. The current snapshot is read-optimized and repaired lazily when a finite paid state has already reached `ends_at`, so callers do not observe stale paid/free state. additionalProperties: false required: - plan_code - is_paid - source - starts_at - updated_at properties: plan_code: $ref: "#/components/schemas/PlanCode" is_paid: type: boolean source: type: string actor: $ref: "#/components/schemas/ActorRef" reason_code: type: string starts_at: type: string format: date-time ends_at: type: string format: date-time updated_at: type: string format: date-time ActiveSanction: type: object additionalProperties: false required: - sanction_code - scope - reason_code - applied_at properties: sanction_code: $ref: "#/components/schemas/SanctionCode" scope: type: string reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" applied_at: type: string format: date-time expires_at: type: string format: date-time ActiveLimit: type: object additionalProperties: false description: | Current supported active user-specific limit override. Retired legacy limit codes are ignored on reads and are not returned. required: - limit_code - value - reason_code - applied_at properties: limit_code: $ref: "#/components/schemas/LimitCode" value: type: integer minimum: 0 reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" applied_at: type: string format: date-time expires_at: type: string format: date-time EffectiveLimit: type: object additionalProperties: false description: | Materialized numeric quota after the frozen `free` or `paid` default catalog is combined with any active user-specific override for the same `limit_code`. `max_owned_private_games` is meaningful only while the current entitlement is paid and is omitted from free effective limits. `max_active_game_memberships` applies only to public games. `max_pending_public_applications` stores the total public-games budget. `Game Lobby` subtracts current active public memberships from this value and clamps at `0` to derive remaining pending-application headroom. required: - limit_code - value properties: limit_code: $ref: "#/components/schemas/LimitCode" value: type: integer minimum: 0 AccountView: type: object additionalProperties: false required: - user_id - email - race_name - preferred_language - time_zone - entitlement - active_sanctions - active_limits - created_at - updated_at properties: user_id: $ref: "#/components/schemas/UserID" email: $ref: "#/components/schemas/Email" race_name: $ref: "#/components/schemas/RaceName" preferred_language: $ref: "#/components/schemas/LanguageTag" time_zone: $ref: "#/components/schemas/TimeZoneName" declared_country: $ref: "#/components/schemas/CountryCode" entitlement: $ref: "#/components/schemas/EntitlementSnapshot" active_sanctions: type: array items: $ref: "#/components/schemas/ActiveSanction" active_limits: type: array items: $ref: "#/components/schemas/ActiveLimit" created_at: type: string format: date-time updated_at: type: string format: date-time GetMyAccountResponse: type: object additionalProperties: false required: - account properties: account: $ref: "#/components/schemas/AccountView" UpdateMyProfileRequest: type: object additionalProperties: false description: | The current implementation accepts only `race_name` here. Attempts to mutate `email` or `declared_country` are rejected as `400 invalid_request` through strict unknown-field handling. required: - race_name properties: race_name: $ref: "#/components/schemas/RaceName" UpdateMySettingsRequest: type: object additionalProperties: false required: - preferred_language - time_zone properties: preferred_language: $ref: "#/components/schemas/LanguageTag" time_zone: $ref: "#/components/schemas/TimeZoneName" EligibilityMarkers: type: object additionalProperties: false required: - can_login - can_create_private_game - can_manage_private_game - can_join_game - can_update_profile properties: can_login: type: boolean can_create_private_game: type: boolean can_manage_private_game: type: boolean can_join_game: type: boolean can_update_profile: type: boolean UserEligibilityResponse: type: object additionalProperties: false required: - exists - user_id - active_sanctions - effective_limits - markers properties: exists: type: boolean user_id: $ref: "#/components/schemas/UserID" entitlement: description: | Current effective entitlement snapshot. Omitted when `exists=false`. $ref: "#/components/schemas/EntitlementSnapshot" active_sanctions: type: array items: $ref: "#/components/schemas/ActiveSanction" effective_limits: description: | Materialized effective quotas for the current supported lobby catalog. Unknown users return an empty array. Free users omit `max_owned_private_games`. type: array items: $ref: "#/components/schemas/EffectiveLimit" markers: $ref: "#/components/schemas/EligibilityMarkers" SyncDeclaredCountryRequest: type: object additionalProperties: false description: | Synchronizes the latest effective declared country selected by `Geo Profile Service`. Repeating the current stored value is accepted as a no-op. required: - declared_country properties: declared_country: $ref: "#/components/schemas/CountryCode" DeclaredCountrySyncResponse: type: object additionalProperties: false required: - user_id - declared_country - updated_at properties: user_id: $ref: "#/components/schemas/UserID" declared_country: $ref: "#/components/schemas/CountryCode" updated_at: type: string format: date-time description: | Effective account mutation timestamp. Same-value no-op syncs return the existing stored timestamp unchanged. UserAdminView: allOf: - $ref: "#/components/schemas/AccountView" UserLookupByEmailRequest: type: object additionalProperties: false required: - email properties: email: $ref: "#/components/schemas/Email" UserLookupByRaceNameRequest: type: object additionalProperties: false required: - race_name properties: race_name: $ref: "#/components/schemas/RaceName" UserLookupResponse: type: object additionalProperties: false required: - user properties: user: $ref: "#/components/schemas/UserAdminView" UserListResponse: type: object additionalProperties: false required: - items properties: items: type: array items: $ref: "#/components/schemas/UserAdminView" next_page_token: type: string GrantEntitlementRequest: type: object additionalProperties: false description: | Grants one current paid entitlement. `plan_code=free` is invalid here. `starts_at` may be current or past, but not future. Finite paid plans require `ends_at`, while `paid_lifetime` forbids it. required: - plan_code - source - reason_code - actor - starts_at properties: plan_code: $ref: "#/components/schemas/PlanCode" source: type: string reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" starts_at: type: string format: date-time ends_at: type: string format: date-time description: Required for `paid_monthly` and `paid_yearly`; omitted for `paid_lifetime`. ExtendEntitlementRequest: type: object additionalProperties: false description: | Extends the current finite paid entitlement by appending one new paid history segment. required: - source - reason_code - actor - ends_at properties: source: type: string reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" ends_at: type: string format: date-time RevokeEntitlementRequest: type: object additionalProperties: false description: | Revokes the current effective paid entitlement and materializes a new `free` snapshot immediately. required: - source - reason_code - actor properties: source: type: string reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" EntitlementCommandResponse: type: object additionalProperties: false description: Resulting current effective entitlement snapshot after one successful trusted entitlement command. required: - user_id - entitlement properties: user_id: $ref: "#/components/schemas/UserID" entitlement: $ref: "#/components/schemas/EntitlementSnapshot" ApplySanctionRequest: type: object additionalProperties: false required: - sanction_code - scope - reason_code - actor - applied_at properties: sanction_code: $ref: "#/components/schemas/SanctionCode" scope: type: string reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" applied_at: type: string format: date-time expires_at: type: string format: date-time RemoveSanctionRequest: type: object additionalProperties: false required: - sanction_code - reason_code - actor properties: sanction_code: $ref: "#/components/schemas/SanctionCode" reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" SanctionCommandResponse: type: object additionalProperties: false required: - user_id - active_sanctions properties: user_id: $ref: "#/components/schemas/UserID" active_sanctions: type: array items: $ref: "#/components/schemas/ActiveSanction" SetLimitRequest: type: object additionalProperties: false required: - limit_code - value - reason_code - actor - applied_at properties: limit_code: $ref: "#/components/schemas/LimitCode" value: type: integer minimum: 0 reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" applied_at: type: string format: date-time expires_at: type: string format: date-time RemoveLimitRequest: type: object additionalProperties: false required: - limit_code - reason_code - actor properties: limit_code: $ref: "#/components/schemas/LimitCode" reason_code: type: string actor: $ref: "#/components/schemas/ActorRef" LimitCommandResponse: type: object additionalProperties: false required: - user_id - active_limits properties: user_id: $ref: "#/components/schemas/UserID" active_limits: type: array items: $ref: "#/components/schemas/ActiveLimit" ErrorResponse: type: object additionalProperties: false required: - error properties: error: $ref: "#/components/schemas/ErrorBody" ErrorBody: type: object additionalProperties: false required: - code - message properties: code: type: string message: type: string responses: InvalidRequestError: description: Request body, path, or query fields are invalid. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: invalidRequest: value: error: code: invalid_request message: request is invalid SubjectNotFoundError: description: The referenced user or lookup subject does not exist. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: subjectNotFound: value: error: code: subject_not_found message: subject not found ConflictError: description: The requested mutation conflicts with current source-of-truth state. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: conflict: value: error: code: conflict message: request conflicts with current state InternalError: description: Internal User Service error. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: internalError: value: error: code: internal_error message: internal server error ServiceUnavailableError: description: User Service is temporarily unable to serve the request safely. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: unavailable: value: error: code: service_unavailable message: service is unavailable