openapi: "3.0.0"
info:
    title: neowire Publisher API
    version: 1.0.0
    description: >
        The server-to-server API a publisher's backend calls to register its users.

servers:
    - url: https://api.neowire.ai
paths:
    /s2s/v1/user/{id}/auth:
        post:
            summary: Authenticate a user and mint a bearer token
            description: >
                Service-to-service endpoint called by an app backend (e.g. airperks-api) with its app token. Creates the user named by the path id on first call, upserts their demographics and currency-conversion settings, then returns a short-lived JWT the caller passes to the client as a bearer token for the `/v1/*` endpoints.

                | Status | Code                     | Meaning                                                     | | ------ | ------------------------ | ----------------------------------------------------------- | | 400    | invalid_timezone         | timezone is not a valid IANA timezone name.                 | | 400    | too_many_friend_ids      | friend_user_ids resolves to more than 100 friends.          | | 400    | currency_not_configured  | currency is not one of the app's configured currencies.     | | 409    | username_taken           | Another user of this app already uses this username.        |

            operationId: authUser
            security:
                - AppBearerToken: []
            parameters:
                - $ref: "#/components/parameters/UserID"
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/S2SUserAuthRequest"
            responses:
                "200":
                    description: User authenticated; bearer token issued
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/S2SUserAuthResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /s2s/v1/user/{id}:
        patch:
            summary: Partially update a user's stored profile
            description: |
                Service-to-service endpoint called by an app backend with its app
                token. Updates only the provided fields on the user named by the
                path id; omitted fields keep their stored values. When
                `friend_user_ids` is present (even as an empty array) the user's
                friend list is replaced with the given set; ids that match no
                existing user of the calling app are dropped silently. `ad_bid`
                and the reward factor derived from it are frozen at user creation
                and cannot be changed here.

                | Status | Code                     | Meaning                                                 |
                | ------ | ------------------------ | ------------------------------------------------------- |
                | 400    | invalid_timezone         | timezone is not a valid IANA timezone name.             |
                | 400    | too_many_friend_ids      | friend_user_ids resolves to more than 100 friends.      |
                | 400    | currency_not_configured  | currency is not one of the app's configured currencies. |
                | 404    | user_not_found           | No user with this id exists for the calling app.        |
                | 409    | username_taken           | Another user of this app already uses this username.    |
            operationId: patchUser
            security:
                - AppBearerToken: []
            parameters:
                - $ref: "#/components/parameters/UserID"
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/S2SUserFields"
            responses:
                "200":
                    description: User updated
                default:
                    $ref: "#/components/responses/GenericError"
        delete:
            summary: Delete a user
            description: >
                | Code | Status | Meaning | | --- | --- | --- | | `user_not_found` | 404 | No live user of this app matches the path id (unknown or already deleted). |

            operationId: deleteUser
            security:
                - AppBearerToken: []
            parameters:
                - $ref: "#/components/parameters/UserID"
            responses:
                "200":
                    description: User deleted
                default:
                    $ref: "#/components/responses/GenericError"
    /s2s/v1/user/{id}/games:
        get:
            summary: Read a user's game activity
            description: >
                Service-to-service endpoint called by an app backend with its app token. Returns one user's whole minigame history, resolved for that user: display names translated into their language, rewards converted to their currency, and mission goals carrying their progress. A game only visible to test users is included only for a test user, and a deleted game is never included.

                Every collection is always present and may be empty, so a user who has never played reports five empty arrays rather than an error.

                | Status | Code           | Meaning                                             | | ------ | -------------- | --------------------------------------------------- | | 404    | user_not_found | No live user with this id exists for the app.       |

            operationId: getUserGameActivity
            security:
                - AppBearerToken: []
            parameters:
                - $ref: "#/components/parameters/UserID"
            responses:
                "200":
                    description: The user's game activity
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/S2SUserGameActivityResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /s2s/v1/shares/{id}:
        get:
            summary: Resolve a share id to media metadata
            description: >
                Resolves one share id to its media metadata plus the sharer's external user id. Only shares belonging to the calling app resolve.

                | Status | Code            | Meaning                                                       | | ------ | --------------- | ------------------------------------------------------------- | | 404    | share_not_found | Unknown id, another app's share, or the media has been deleted. |

            operationId: getShare
            security:
                - AppBearerToken: []
            parameters:
                - name: id
                  in: path
                  required: true
                  description: The share id to resolve.
                  schema:
                    type: string
                    maxLength: 32
            responses:
                "200":
                    description: Resolved share
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/S2SShare"
                default:
                    $ref: "#/components/responses/GenericError"
        post:
            summary: Resolve share ids to media metadata (batch)
            description: >
                Batch counterpart of `GET /s2s/v1/shares/{id}`, resolving the request body's `ids` in one call. The path id must be the literal `_`.

                Only shares belonging to the calling app are returned; ids that are unknown, belong to another app, or whose media has been deleted are omitted from the response (so the result may be shorter than the request, and in any order).

            operationId: getShares
            security:
                - AppBearerToken: []
            parameters:
                - name: id
                  in: path
                  required: true
                  description: Must be `_`; the ids to resolve come from the request body.
                  schema:
                    type: string
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/S2SGetSharesRequest"
            responses:
                "200":
                    description: Resolved shares
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/S2SGetSharesResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /s2s/v1/ads/{id}:
        patch:
            summary: Fill ad slots with server-reported revenue
            description: >
                Server-to-server counterpart of the client `PATCH /v1/ads/{id}` fill, called by an app backend with its app token. Attaches server-reported revenue (`server_data`, currently currency + bid) to ad slots owned by the calling app.

                When `id` is a slot uuid the body fills that single slot via `server_data`. When `id` is the literal `_` the body's `items` array fills many slots at once; `items` must be non-empty and `server_data` must be omitted in that case. The batch is atomic: any validation error fails the whole request and nothing is written. Slots that don't exist for the app (or were already server-filled) are silently skipped.

            operationId: fillServerAd
            security:
                - AppBearerToken: []
            parameters:
                - name: id
                  in: path
                  required: true
                  description: A slot uuid, or `_` to fill a batch via the request body's `items`.
                  schema:
                    type: string
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/S2SFillServerAdRequest"
            responses:
                "204":
                    description: Slots filled
                default:
                    $ref: "#/components/responses/GenericError"
components:
    securitySchemes:
        AppBearerToken:
            type: http
            scheme: bearer
    parameters:
        UserID:
            name: id
            in: path
            required: true
            description: >
                External user id: the app's own identifier for the user, whose first authentication creates them.

            schema:
                type: string
                maxLength: 128
                pattern: "^[a-zA-Z0-9._-]+$"
                example: user1
    schemas:
        S2SUserFields:
            type: object
            description: >
                The user profile an app backend owns, every field optional: the body of `PATCH /s2s/v1/user/{id}`, and the base S2SUserAuthRequest extends with the fields it requires.

            properties:
                country:
                    type: string
                    minLength: 2
                    maxLength: 2
                    example: US
                language:
                    type: string
                    minLength: 2
                    maxLength: 2
                    example: en
                age:
                    type: integer
                    minimum: 0
                    maximum: 150
                gender:
                    type: string
                    enum:
                        - male
                        - female
                        - other
                timezone:
                    type: string
                    maxLength: 64
                    example: America/Chicago
                    description: IANA timezone name (e.g. "America/Chicago").
                should_reward_user:
                    type: boolean
                    description: >
                        Whether the caller has determined this user is eligible to earn rewards. Stored on the user record and consulted by the feed endpoints.

                currency:
                    type: string
                    maxLength: 10
                    example: USD
                    description: >
                        Identifier of the user's local currency for displayed earnings. Any currency identifier is accepted, including custom, non-ISO codes (e.g. an app's own virtual currency) — it is not required to be an ISO 4217 code. Where the app configures currencies, it must be one of them (`currency_not_configured` otherwise); the matching configuration decides how the client renders amounts.

                currency_factor:
                    type: string
                    example: "1.0"
                    description: >
                        Decimal multiplier applied to USD earnings to display them in the user's local currency (`local = usd * currency_factor`).

                username:
                    type: string
                    pattern: "^[a-z0-9-_.]{1,30}$"
                    description: >
                        Unique handle within the calling app. When provided it is stored; when omitted the existing stored value is kept. There is no way to clear it back to empty.

                picture_url:
                    type: string
                    maxLength: 200
                    pattern: '^https://\S+$'
                    description: >
                        Profile picture URL. When provided it is stored; when omitted the existing stored value is kept. There is no way to clear it back to empty.

                friend_user_ids:
                    type: array
                    maxItems: 100
                    items:
                        type: string
                        maxLength: 128
                        pattern: "^[a-zA-Z0-9._-]+$"
                    description: >
                        The full set of the user's friends' external user ids (empty allowed). When present the stored friend list is replaced with this set; when absent it is left untouched. Ids of unknown users, and the user's own id, are dropped.

        S2SUserAuthRequest:
            allOf:
                - $ref: "#/components/schemas/S2SUserFields"
                - type: object
                  required:
                    - country
                    - language
                    - age
                    - gender
                    - timezone
                    - should_reward_user
                    - currency
                    - currency_factor
                  properties:
                    ad_bid:
                        type: number
                        format: double
                        nullable: true
                        description: >
                            Optional client-reported ad bid. Consumed only when the user is first created: it is stored for analytics and resolves the reward factor persisted on the user. Omitted or sent on a later auth for an existing user, it is ignored and the factor stays at its default (1.0).

        S2SUserAuthResponse:
            type: object
            required:
                - token
                - expires_at
            properties:
                token:
                    type: string
                    description: JWT bearer token for the `/v1/*` endpoints.
                expires_at:
                    type: string
                    format: date-time
        S2SGetSharesRequest:
            type: object
            required:
                - ids
            properties:
                ids:
                    type: array
                    minItems: 1
                    maxItems: 50
                    description: The share ids to resolve.
                    items:
                        type: string
                        maxLength: 32
        S2SGetSharesResponse:
            type: object
            required:
                - shares
            properties:
                shares:
                    type: array
                    description: >
                        Resolved shares, one per successfully resolved id. Unknown, wrong-app or deleted-media ids are omitted, so this may be shorter than the requested id list and in any order.

                    items:
                        $ref: "#/components/schemas/S2SShare"
        S2SShare:
            type: object
            required:
                - id
                - sharer_uid
                - media
            properties:
                id:
                    type: string
                    description: The share id.
                sharer_uid:
                    type: string
                    description: External user id of the user who created the share.
                media:
                    $ref: "#/components/schemas/ShareMedia"
        S2SFillServerAdRequest:
            type: object
            description: >
                Body for `PATCH /s2s/v1/ads/{id}`. Set `server_data` to fill the single slot named in the path; set `items` to fill a batch when the path id is `_`.

            properties:
                server_data:
                    $ref: "#/components/schemas/S2SServerAdData"
                items:
                    type: array
                    maxItems: 500
                    description: Per-slot fills, used when the path id is `_`.
                    items:
                        $ref: "#/components/schemas/S2SFillServerAdItem"
        S2SFillServerAdItem:
            type: object
            required:
                - id
                - server_data
            properties:
                id:
                    type: string
                    format: uuid
                    description: The slot id to fill.
                server_data:
                    $ref: "#/components/schemas/S2SServerAdData"
        S2SServerAdData:
            type: object
            description: Server-reported ad revenue for a slot.
            required:
                - currency
                - bid
            properties:
                currency:
                    type: string
                    maxLength: 10
                    description: >
                        Currency identifier for the bid. Any currency identifier is accepted, including custom, non-ISO codes — it is not required to be an ISO 4217 code.

                bid:
                    type: string
                    description: Bid amount as a decimal string (e.g. "1.25").
        S2SUserGameActivityResponse:
            type: object
            required:
                - completed_events
                - progress
                - plays
                - joinable_missions
                - mission_participations
            properties:
                completed_events:
                    type: array
                    description: The events the user has completed, most recent first.
                    items:
                        $ref: "#/components/schemas/S2SGameCompletedEvent"
                progress:
                    type: array
                    description: >
                        Derived per-game state, one entry per game the user has played, most recently played first.

                    items:
                        $ref: "#/components/schemas/S2SGameProgress"
                plays:
                    type: array
                    description: >
                        The user's most recent rounds, newest first, capped at the last 10. One entry per round rather than per submission, so a round continued after a second life appears once.

                    items:
                        $ref: "#/components/schemas/S2SGamePlay"
                joinable_missions:
                    type: array
                    description: >
                        The missions the user could join right now by playing the corresponding game. A mission moves to mission_participations as soon as they join it.

                    items:
                        $ref: "#/components/schemas/S2SGameMissionOffer"
                mission_participations:
                    type: array
                    description: >
                        The user's ongoing mission windows plus every past one whose window ended within the last 14 days, most recent first.

                    items:
                        $ref: "#/components/schemas/S2SGameMissionParticipation"
        S2SGameType:
            type: string
            enum:
                - level
                - highscore
        S2SGameCompletedEvent:
            type: object
            required:
                - game_id
                - game_name
                - game_type
                - event_id
                - event_name
                - reward_user_currency
                - completed_at
            properties:
                game_id:
                    type: string
                    description: The game slug (e.g. "apex-layers").
                game_name:
                    type: string
                    description: The game's display name.
                game_type:
                    $ref: "#/components/schemas/S2SGameType"
                event_id:
                    type: string
                    description: The event's internal name (e.g. "level-50").
                event_name:
                    type: string
                    description: >
                        The event's display name in the user's language, falling back to English and then to its internal name.

                reward_user_currency:
                    type: string
                    description: >
                        The reward the user actually received for this event, as a decimal string in their currency.

                completed_at:
                    type: string
                    format: date-time
        S2SGameProgress:
            type: object
            required:
                - game_id
                - game_name
                - game_type
                - best_score
                - play_count
                - total_playtime_seconds
                - last_played_at
                - events
            properties:
                game_id:
                    type: string
                    description: The game slug (e.g. "apex-layers").
                game_name:
                    type: string
                game_type:
                    $ref: "#/components/schemas/S2SGameType"
                best_score:
                    type: integer
                    description: >
                        The user's best metric value across all their rounds of this game: the highest level for a level game, the highest score for a highscore game.

                play_count:
                    type: integer
                    description: How many rounds the user has played of this game.
                total_playtime_seconds:
                    type: integer
                    description: Total seconds the user has spent in this game.
                last_played_at:
                    type: string
                    format: date-time
                events:
                    type: array
                    description: >
                        Every event of the version the user is on for this game, in the order shown to the client. Completed events carry a non-null completed_at.

                    items:
                        $ref: "#/components/schemas/S2SGameEventState"
        S2SGameEventState:
            type: object
            required:
                - event_id
                - event_name
                - reward_user_currency
                - completed_at
            properties:
                event_id:
                    type: string
                    description: The event's internal name (e.g. "level-50").
                event_name:
                    type: string
                    description: >
                        The event's display name in the user's language, falling back to English and then to its internal name.

                reward_user_currency:
                    type: string
                    description: >
                        The reward completing this event pays, as a decimal string in the user's currency. This is the prospective amount derived from the event's configuration, so it can differ from what a completed event actually paid — read S2SGameCompletedEvent.reward_user_currency for that.

                completed_at:
                    type: string
                    format: date-time
                    nullable: true
                    description: When the user completed this event, or null if they have not.
        S2SGamePlay:
            type: object
            required:
                - game_id
                - game_name
                - game_type
                - score
                - playtime_seconds
                - played_at
            properties:
                game_id:
                    type: string
                    description: The game slug (e.g. "apex-layers").
                game_name:
                    type: string
                game_type:
                    $ref: "#/components/schemas/S2SGameType"
                score:
                    type: integer
                    description: >
                        The metric value reached in this round, 0 when the user did not score.

                playtime_seconds:
                    type: integer
                    description: How many seconds the round lasted, summed over its submissions.
                played_at:
                    type: string
                    format: date-time
                    description: When the round began.
        S2SGameMissionOffer:
            type: object
            required:
                - game_id
                - game_name
                - game_type
                - mission_id
                - mission_name
                - goals
                - reward_user_currency
                - joinable_until
            properties:
                game_id:
                    type: string
                    description: The game slug (e.g. "apex-layers").
                game_name:
                    type: string
                game_type:
                    $ref: "#/components/schemas/S2SGameType"
                mission_id:
                    type: string
                    description: The mission's internal name (e.g. "daily-grind").
                mission_name:
                    type: string
                    description: >
                        The mission's display name in the user's language, falling back to English and then to its internal name.

                goals:
                    type: array
                    description: >
                        The mission's goals in the order shown to the client, all at zero progress since the user has not joined yet.

                    items:
                        $ref: "#/components/schemas/S2SGameMissionGoal"
                reward_user_currency:
                    type: string
                    description: >
                        What completing every goal would pay if the user joined now, as a decimal string in their currency.

                joinable_until:
                    type: string
                    format: date-time
                    nullable: true
                    description: When the mission stops being joinable, or null when open-ended.
        S2SGameMissionStatus:
            type: string
            description: >
                Where a joined mission window stands. There is no `not_started`: a mission the user has not joined is reported in joinable_missions instead.

            enum:
                - ongoing
                - completed
                - failed
        S2SGameMissionParticipation:
            type: object
            required:
                - game_id
                - game_name
                - game_type
                - mission_id
                - mission_name
                - status
                - goals
                - reward_user_currency
                - joined_at
                - resets_at
            properties:
                game_id:
                    type: string
                    description: The game slug (e.g. "apex-layers").
                game_name:
                    type: string
                game_type:
                    $ref: "#/components/schemas/S2SGameType"
                mission_id:
                    type: string
                    description: The mission's internal name (e.g. "daily-grind").
                mission_name:
                    type: string
                    description: >
                        The mission's display name in the user's language, falling back to English and then to its internal name.

                status:
                    $ref: "#/components/schemas/S2SGameMissionStatus"
                goals:
                    type: array
                    description: >
                        The goals of the mission version the user joined, in the order shown to the client, carrying their progress.

                    items:
                        $ref: "#/components/schemas/S2SGameMissionGoal"
                reward_user_currency:
                    type: string
                    description: >
                        The reward received on completion, as a decimal string in the user's currency. Zero unless status is `completed`.

                joined_at:
                    type: string
                    format: date-time
                    description: When the user joined, i.e. their first counting play.
                resets_at:
                    type: string
                    format: date-time
                    description: When the mission window ends, or ended for a past participation.
        S2SGameMissionGoal:
            type: object
            required:
                - goal_id
                - goal_name
                - value
                - threshold
                - completed
            properties:
                goal_id:
                    type: string
                    description: The goal's internal name (e.g. "complete-levels").
                goal_name:
                    type: string
                    description: >
                        The goal's display name in the user's language, falling back to English and then to its internal name.

                value:
                    type: number
                    format: double
                    description: The user's progress toward threshold, 0 while unjoined.
                threshold:
                    type: number
                    format: double
                    description: >
                        The value at which the goal completes. Frozen when the user joins, so it does not move with a later config change.

                completed:
                    type: boolean
        RewardDisplayIcon:
            type: object
            description: Render the amount with the icon at `icon_url` on the given side of it.
            required:
                - type
                - icon_url
                - position
            properties:
                type:
                    type: string
                    const: icon
                icon_url:
                    type: string
                position:
                    $ref: "#/components/schemas/RewardDisplayPosition"
        RewardDisplayPlain:
            type: object
            description: Render the amount with `symbol` on the given side of it, e.g. "$1.50".
            required:
                - type
                - symbol
                - position
            properties:
                type:
                    type: string
                    const: plain
                symbol:
                    type: string
                position:
                    $ref: "#/components/schemas/RewardDisplayPosition"
        ComicPartPage:
            type: object
            required:
                - page_type
                - index
                - video_url
            properties:
                page_type:
                    type: string
                    const: page
                index:
                    type: integer
                    description: The index of the page in the part.
                video_url:
                    type: string
        ComicPartPageAd:
            type: object
            allOf:
                - type: object
                  required:
                    - page_type
                  properties:
                    page_type:
                        type: string
                        const: ad
                - $ref: "#/components/schemas/Ad"
        ShareVideoMedia:
            type: object
            required:
                - type
                - id
                - creator_tag
                - creator_name
                - thumbnail_url
            properties:
                type:
                    type: string
                    enum:
                        - video
                id:
                    type: string
                creator_tag:
                    type: string
                creator_name:
                    type: string
                thumbnail_url:
                    type: string
        ShareComicMedia:
            type: object
            required:
                - type
                - id
                - title
                - description
                - cover_url
            properties:
                type:
                    type: string
                    enum:
                        - comic
                id:
                    type: string
                title:
                    type: string
                description:
                    type: string
                cover_url:
                    type: string
        ShareGameMedia:
            type: object
            required:
                - type
                - id
                - name
                - icon_url
                - banner_image_url
                - score
            properties:
                type:
                    type: string
                    enum:
                        - game
                id:
                    type: string
                name:
                    type: string
                icon_url:
                    type: string
                banner_image_url:
                    type: string
                score:
                    type: integer
                    nullable: true
                    description: Score the sharer attached to the share, or null.
        ShareMedia:
            description: The shared media, discriminated by `type`.
            oneOf:
                - $ref: "#/components/schemas/ShareVideoMedia"
                - $ref: "#/components/schemas/ShareComicMedia"
                - $ref: "#/components/schemas/ShareGameMedia"
            discriminator:
                propertyName: type
                mapping:
                    video: "#/components/schemas/ShareVideoMedia"
                    comic: "#/components/schemas/ShareComicMedia"
                    game: "#/components/schemas/ShareGameMedia"
        Error:
            type: object
            properties:
                error:
                    type: string
                code:
                    type: string
                    description: Machine-readable error code; omitted when the error has none.
        VideoFriendLike:
            type: object
            allOf:
                - $ref: "#/components/schemas/Friend"
                - type: object
                  required:
                    - liked_at
                  properties:
                    liked_at:
                        type: string
                        format: date-time
                        description: When the friend liked the video.
        Friend:
            type: object
            description: A compact projection of a friend user.
            required:
                - uid
                - username
                - picture_url
            properties:
                uid:
                    type: string
                    description: The friend's external user id (app-scoped public identifier).
                username:
                    type: string
                    nullable: true
                    description: The friend's username; null when unset.
                picture_url:
                    type: string
                    nullable: true
                    description: The friend's profile picture URL; null when unset.
        CreatorLink:
            type: object
            required:
                - text
                - url
            properties:
                text:
                    type: string
                url:
                    type: string
        RewardDisplayPosition:
            type: string
            description: Which side of the amount the icon or symbol goes on.
            enum:
                - left
                - right
        GameCategory:
            type: string
            description: Game category slug.
            enum:
                - arcade
                - board-card
                - casual
                - entertainment
                - puzzle
                - role-playing
                - strategy
                - other
        GameType:
            type: string
            enum:
                - level
                - highscore
        GameEvent:
            type: object
            required:
                - id
                - translations
                - sort_order
                - reward
                - threshold_type
                - blocked_by
                - marks_endgame
                - completed_at
            properties:
                id:
                    type: string
                    format: uuid
                translations:
                    type: object
                    additionalProperties:
                        type: string
                sort_order:
                    type: integer
                reward:
                    $ref: "#/components/schemas/Reward"
                threshold_type:
                    type: string
                    enum:
                        - level
                        - highscore
                        - games_played
                        - counter
                        - gauge
                        - set
                blocked_by:
                    type: string
                    format: uuid
                    nullable: true
                    description: public_id of the event that must be completed first, or null.
                marks_endgame:
                    type: boolean
                    description: >
                        Whether completing this event marks the user as having reached the endgame. At most one event per version sets this.

                completed_at:
                    type: string
                    format: date-time
                    nullable: true
        GameMission:
            type: object
            required:
                - id
                - translations
                - reward
                - status
                - joinable_until
                - minutes_to_complete
                - goals
            properties:
                id:
                    type: string
                    format: uuid
                translations:
                    type: object
                    additionalProperties:
                        type: string
                reward:
                    $ref: "#/components/schemas/Reward"
                status:
                    type: string
                    enum:
                        - not_started
                        - ongoing
                        - completed
                joinable_until:
                    type: string
                    format: date-time
                    nullable: true
                    description: Until when the mission can be started, or null when unbounded.
                minutes_to_complete:
                    type: integer
                    description: >
                        Minutes the user has to complete the mission: the full cadence window (1440 for daily) before starting, the remainder until the mission resets once started.

                goals:
                    type: array
                    items:
                        $ref: "#/components/schemas/GameMissionGoal"
        GameState:
            type: object
            additionalProperties:
                $ref: "#/components/schemas/GameStateValue"
            description: >
                The user's current game state keyed by value name: the latest snapshot, or every configured value at its zero state when the user has not played yet. Which field of each value is populated follows the value's configured type.

        GameFriendHighscore:
            type: object
            allOf:
                - $ref: "#/components/schemas/Friend"
                - type: object
                  required:
                    - highscore
                  properties:
                    highscore:
                        type: integer
                        description: The friend's best result for this game (highest level for level games, highest score for highscore games).
        GameSection:
            type: string
            description: >
                Which block of the games tab a game belongs to, for rendering the section headers, in the order the sections appear: `test` holds the test-user-only games and reaches test users only, `continue_playing` the games the user is part-way through, `discover` the rest. Responses that rank nothing for a specific user — a single game, the public catalog, a game injected into the feed — report `discover`.

            enum:
                - test
                - continue_playing
                - discover
        Reward:
            type: object
            required:
                - total
                - bonus
                - base
                - display
            properties:
                total:
                    type: string
                    description: total = bonus + base
                bonus:
                    type: string
                base:
                    type: string
                display:
                    description: >
                        How the client should render the amounts, as configured for the user's currency. Falls back to a leading "$" where the app configures no rendering for that currency, and wherever there is no user to read a currency from, such as the public games catalog.

                    oneOf:
                        - $ref: "#/components/schemas/RewardDisplayIcon"
                        - $ref: "#/components/schemas/RewardDisplayPlain"
                    discriminator:
                        propertyName: type
                        mapping:
                            icon: "#/components/schemas/RewardDisplayIcon"
                            plain: "#/components/schemas/RewardDisplayPlain"
        GameMissionGoal:
            type: object
            required:
                - translations
                - value
                - threshold
                - completed
            properties:
                translations:
                    type: object
                    additionalProperties:
                        type: string
                value:
                    type: number
                    format: double
                    description: The user's current progress toward the goal.
                threshold:
                    type: number
                    format: double
                    description: The value at which the goal completes.
                completed:
                    type: boolean
        GameStateValue:
            type: object
            description: >
                One state value. Exactly one field carries the value, chosen by the type it is configured as: `total` for counter and gauge values, `number` for number values, `string`/`boolean`/`set`/`list` for the others. The fields that do not apply are omitted.

            properties:
                total:
                    type: integer
                    format: int64
                    description: >
                        A counter's or gauge's current total. Both only ever move by whole increments, so this is a whole number - exact across the full int64 range, of which a client reading it into a double can represent up to 2^53.

                number:
                    type: number
                    format: double
                    description: A number value's current value.
                string:
                    type: string
                    description: A string value's current value.
                boolean:
                    type: boolean
                    description: A boolean value's current value.
                set:
                    type: array
                    description: >
                        A set value's current elements, sorted. Always present (and empty when nothing is held) for set values.

                    items:
                        type: string
                list:
                    type: array
                    description: >
                        A list value's current items in order, each shaped by the item schema configured for the value. Always present (and empty when nothing is held) for list values.

                    items: {}
        ComicPart:
            type: object
            required:
                - index
                - version
                - title
                - description
                - unlocked
                - finished_reading_at
                - reward
                - cover_url
                - music_url
                - pages
                - rating_count
                - rating_avg
                - read_time_avg
            properties:
                index:
                    type: integer
                    description: The index of the part in the comic.
                version:
                    type: string
                    description: The version of the part.
                title:
                    type: string
                description:
                    type: string
                unlocked:
                    type: boolean
                    description: >
                        Whether the part is unlocked (already read, or the single next readable part). When false, music_url and pages are null. Pages are only ever populated by GET /v1/comics/{comic_id}/parts/{part_index}, so an unlocked part still has null pages in the list/detail/feed views.

                finished_reading_at:
                    type: string
                    format: date-time
                    nullable: true
                    description: The timestamp the user finished reading the part.
                reward:
                    $ref: "#/components/schemas/Reward"
                cover_url:
                    type: string
                music_url:
                    type: string
                    nullable: true
                    description: The URL of the music for the part. Null if the part is locked.
                pages:
                    type: array
                    nullable: true
                    description: >
                        The pages and ads of the part. Only populated by GET /v1/comics/{comic_id}/parts/{part_index} (the full part view); null in the list/detail/feed views and for locked parts.

                    items:
                        $ref: "#/components/schemas/ComicPartPagesItem"
                rating_count:
                    type: integer
                    format: int64
                    description: >
                        Number of 1-5 star ratings submitted for this part. Reported as 0 (together with rating_avg) until the part has collected at least 5 ratings, so early, unrepresentative scores are hidden.

                rating_avg:
                    type: number
                    format: double
                    description: >
                        Average star rating (1.0-5.0), truncated to one decimal place. Reported as 0 until the part has collected at least 5 ratings (see rating_count).

                read_time_avg:
                    type: number
                    format: double
                    description: >
                        Median time, in seconds, users spent reading this part (reads over one hour are excluded as outliers). 0 when the part has not been read yet.

        ComicFriendReadingProgress:
            type: object
            allOf:
                - $ref: "#/components/schemas/Friend"
                - type: object
                  required:
                    - last_read_part_index
                  properties:
                    last_read_part_index:
                        type: integer
                        description: The highest part index the friend has read in this comic.
        ComicPartPagesItem:
            oneOf:
                - $ref: "#/components/schemas/ComicPartPage"
                - $ref: "#/components/schemas/ComicPartPageAd"
            discriminator:
                propertyName: page_type
                mapping:
                    page: "#/components/schemas/ComicPartPage"
                    ad: "#/components/schemas/ComicPartPageAd"
        Ad:
            type: object
            required:
                - slot_id
                - type
                - lock_seconds
                - external_sub_type
            properties:
                slot_id:
                    type: string
                    format: uuid
                    description: The ad slot's id. Use it to fill the slot via `PATCH /v1/ads/{id}`.
                type:
                    type: string
                    enum:
                        - offer
                        - external
                lock_seconds:
                    type: integer
                    description: How long the ad is locked before it can be skipped.
                external_sub_type:
                    type: string
                    enum:
                        - native
                        - interstitial
                        - rewarded
                    nullable: true
                    description: Sub-type of an external ad; null for non-external ads.
    responses:
        GenericError:
            description: Generic Error
            content:
                application/json:
                    schema:
                        $ref: "#/components/schemas/Error"
