openapi: "3.0.0"
info:
    title: neowire Public API
    version: 1.0.0
    description: >
        The client-facing API for neowire. Authenticated with the user bearer token the publisher's backend mints through the Publisher API.

servers:
    - url: https://api.neowire.ai
paths:
    /v1/feed:
        get:
            summary: Get personalized video feed
            description: >
                Returns the next batch of videos for the user. If no `session_id` is provided, a new session is created. Pure read endpoint — events must be posted via `POST /v1/feed/events`. The user is identified by the bearer token; demographics come from the record stored at `POST /s2s/v1/user/{id}/auth`.

                Videos are always served, no matter how much the user has already watched today. Once the daily watchtime-reward limit is reached, `watchtime_rewards_available_after` carries the moment rewards resume.

                | Status | Code                   | Meaning                                | | ------ | ---------------------- | -------------------------------------- | | 410    | session_not_found      | The referenced session does not exist. | | 410    | session_expired        | The session has expired.               |

            operationId: getFeed
            security:
                - UserBearerToken: []
            parameters:
                - name: session_id
                  in: query
                  required: false
                  schema:
                    type: string
                    format: uuid
                - name: earning_mode
                  in: query
                  required: false
                  schema:
                    type: string
                    enum:
                        - low
                        - normal
                        - high
                    default: normal
                  description: >
                    Controls earning intensity for the session: ad cadence, ad lock duration, earning multiplier, and external ad sub-type. Stored on the session; when it changes between calls, the session is updated to the latest value.

            responses:
                "200":
                    description: Feed response with recommendations
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/GetFeedResponse"
                "410":
                    description: Session not found or expired
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/GetFeedSessionError"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/feed/events:
        post:
            summary: Submit video feed interaction events
            description: >
                Processes user interaction events (likes, unlikes, watch time) against an existing session. Returns the updated earnings state. The request that crosses the daily watch-time limit is rewarded only for the watch time that still fits within it. From then on events keep being accepted and recorded, but the watch time in them earns nothing until `watchtime_rewards_available_after`, which every response carries once the limit is reached.

                When the user is not eligible for rewards (as recorded at `POST /s2s/v1/user/{id}/auth`), all `video_watch_time` events in the request body are silently discarded before processing. Likes and unlikes are still applied. No watchtime accumulates and no reward callback fires.

                | Status | Code                   | Meaning                                | | ------ | ---------------------- | -------------------------------------- | | 410    | session_not_found      | The referenced session does not exist. | | 410    | session_expired        | The session has expired.               |

            operationId: postFeedEvents
            security:
                - UserBearerToken: []
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/PostFeedEventsRequest"
            responses:
                "200":
                    description: Events processed
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/PostFeedEventsResponse"
                "410":
                    description: Session not found or expired
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/GetFeedSessionError"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/user:
        get:
            summary: Get the authenticated user's profile
            operationId: getUser
            security:
                - UserBearerToken: []
            responses:
                "200":
                    description: User profile
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/GetUserProfileResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/user/videos:
        get:
            summary: List videos liked by the user
            description: >
                Returns a paginated list of videos the user has liked, ordered by the time of the like (newest first). Cursor is the RFC3339Nano timestamp of the last item's `liked_at`.

            operationId: listUserVideos
            security:
                - UserBearerToken: []
            parameters:
                - name: cursor
                  in: query
                  required: false
                  schema:
                    type: string
                    description: RFC3339Nano timestamp from the previous page's `next_cursor`.
                - name: limit
                  in: query
                  required: false
                  schema:
                    type: integer
                    minimum: 1
                    maximum: 50
                    default: 20
            responses:
                "200":
                    description: Paginated liked videos
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/ListUserVideosResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/user/creators:
        get:
            summary: List creators followed by the user
            description: >
                Returns the creators followed by the user, ordered by most recent follow first.

            operationId: listUserCreators
            security:
                - UserBearerToken: []
            responses:
                "200":
                    description: OK
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/ListUserCreatorsResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/user/games:
        get:
            summary: List games the user has played
            description: >
                Returns the games the user has at least one recorded event for, ordered by most recent event first. The user is identified by the bearer token.

            operationId: listUserGames
            security:
                - UserBearerToken: []
            responses:
                "200":
                    description: Games the user has played
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/ListUserGamesResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/user/comics:
        get:
            summary: List the user's comics
            description: >
                Returns a list of the user's comics, ordered by most recent comic first.

            operationId: listUserComics
            security:
                - UserBearerToken: []
            responses:
                "200":
                    description: User's comics
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/ListUserComicsResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/videos/{uid}:
        get:
            summary: Get a video
            description: >
                Returns a single video by its id, including the requesting user's like state (`liked_at`) and which of their friends liked it.

                | Status | Code            | Meaning                                  | | ------ | --------------- | ---------------------------------------- | | 404    | video_not_found | The video does not exist or was deleted. |

            operationId: getVideo
            security:
                - UserBearerToken: []
            parameters:
                - name: uid
                  in: path
                  required: true
                  schema:
                    type: string
                    format: uuid
            responses:
                "200":
                    description: Video
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/Video"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/videos/{uid}/share:
        post:
            summary: Create or fetch a shareable link for a video
            description: >
                Mints a new globally-unique 8-character share id for the video that the public `GET /shares/{id}` endpoint resolves. A user may share the same video repeatedly; every call returns a fresh share id. An app carrying a share link of its own gets that link back instead (see `share_id`).

                | Status | Code               | Meaning                          | | ------ | ------------------ | -------------------------------- | | 404    | video_not_found    | The video does not exist or was deleted. | | 429    | rate_limited       | The user has created too many shares this hour. |

            operationId: createVideoShare
            security:
                - UserBearerToken: []
            parameters:
                - name: uid
                  in: path
                  required: true
                  schema:
                    type: string
                    format: uuid
            responses:
                "200":
                    description: Share created or already existed
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/CreateShareResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/videos/{uid}/unlike:
        post:
            summary: Remove a user's like from a video
            description: >
                Removes the user's like for a video outside the session-event flow. Idempotent - unliking a video the user has not liked succeeds.

            operationId: unlikeVideo
            security:
                - UserBearerToken: []
            parameters:
                - name: uid
                  in: path
                  required: true
                  schema:
                    type: string
                    format: uuid
            responses:
                "204":
                    description: Like removed (or did not exist)
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/videos/{uid}/report:
        post:
            summary: Report a video
            description: >
                Records a user report for a video. Only one report per user & video is stored.

                | Status | Code               | Meaning                          | | ------ | ------------------ | -------------------------------- | | 404    | video_not_found    | The video does not exist or was deleted. |

            operationId: reportVideo
            security:
                - UserBearerToken: []
            parameters:
                - name: uid
                  in: path
                  required: true
                  schema:
                    type: string
                    format: uuid
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/ReportVideoRequest"
            responses:
                "204":
                    description: Report recorded (or already existed)
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/creators/{creator_tag}:
        get:
            summary: Get a creator by tag
            description: >
                Returns the creator's metadata, including whether the requesting user follows them (`followed_at`).

                | Status | Code              | Meaning                          | | ------ | ----------------- | -------------------------------- | | 404    | creator_not_found | The creator does not exist.      |

            operationId: getCreator
            security:
                - UserBearerToken: []
            parameters:
                - name: creator_tag
                  in: path
                  required: true
                  schema:
                    type: string
                  description: The creator's tag.
            responses:
                "200":
                    description: Creator metadata
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/Creator"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/creators/{creator_tag}/follow:
        patch:
            summary: Follow/Unfollow a creator
            description: >
                Follows or unfollows the creator. Idempotent in both directions.

                | Status | Code              | Meaning                          | | ------ | ----------------- | -------------------------------- | | 404    | creator_not_found | The creator does not exist.      |

            operationId: followCreator
            security:
                - UserBearerToken: []
            parameters:
                - name: creator_tag
                  in: path
                  required: true
                  schema:
                    type: string
                  description: The creator's tag.
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/FollowCreatorRequest"
            responses:
                "204":
                    description: Follow status updated
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/creators/{creator_tag}/videos:
        get:
            summary: List videos by creator
            description: >
                Returns a paginated list of videos uploaded by the specified creator. The cursor is the id (UUID) of the last video from the previous page's `next_cursor`; pass it verbatim to fetch the following page.

                | Status | Code              | Meaning                          | | ------ | ----------------- | -------------------------------- | | 404    | creator_not_found | The creator does not exist.      |

            operationId: listCreatorVideos
            security:
                - UserBearerToken: []
            parameters:
                - name: creator_tag
                  in: path
                  required: true
                  schema:
                    type: string
                  description: The creator's tag.
                - name: cursor
                  in: query
                  required: false
                  schema:
                    type: string
                    description: Video id (UUID) from the previous page's `next_cursor`.
                - name: limit
                  in: query
                  required: false
                  schema:
                    type: integer
                    minimum: 1
                    maximum: 20
                    default: 10
            responses:
                "200":
                    description: List of videos
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/ListCreatorVideosResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/games:
        get:
            summary: List public games
            description: >
                Returns the generally available game catalog without user-specific state. This endpoint does not require authentication.

            operationId: listGames
            responses:
                "200":
                    description: Public game catalog
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/ListGamesResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/games/{game_id}:
        get:
            summary: Get a game and its events for the user
            description: >
                Returns the game's metadata and the events of the version the user is on: the latest version if the user has not completed any event for this game yet, otherwise the version of the user's first completed event (so progress stays stable when new versions are published).

                | Status | Code           | Meaning                                      | | ------ | -------------- | -------------------------------------------- | | 404    | game_not_found | The game does not exist or was deleted.      |

            operationId: getGame
            security:
                - UserBearerToken:
                    - games
            parameters:
                - name: game_id
                  in: path
                  required: true
                  schema:
                    type: string
            responses:
                "200":
                    description: Game with events
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/Game"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/games/{game_id}/play:
        post:
            summary: Track a game play
            description: >
                Records a client game play (one game_end). score is null on game over; for level games it is the level reached, for highscore games the score achieved. playtime_seconds is the whole seconds played since the previous submission. state_change optionally carries per-value operations (see state_change) that are applied to the user's running game state, which the response returns as `state`.

                A round the client continues — after granting a second life, say — is submitted again with `previous_play_id` set to the `play_id` the previous submission returned. Each submission carries only what happened since the one before it, of playtime and state alike, never a running total; `score` is the exception and stays the round's best so far. A round may be continued at most 5 times.

                A submission carrying an `idempotency_key` — a UUID the client stamps on it, fresh per submission and the same across every retry of it — is recorded once however often it arrives: each repeat returns the recorded submission's `play_id` and the user's current `state`, completing and rewarding nothing further. A key is scoped to the caller and the game, so it only has to be unique among that user's own submissions for this game; it is shared with the shop endpoint. Omit the key to have every request recorded on its own.

                | Status | Code                 | Meaning                                      | | ------ | -------------------- | -------------------------------------------- | | 404    | game_not_found       | The game does not exist or was deleted.      | | 400    | invalid_state_change | A state_change value is unknown, carries an operation its type does not support, or is out of range. | | 404    | play_not_found       | previous_play_id does not name a play of this game belonging to the caller. | | 409    | play_superseded      | previous_play_id is no longer the caller's most recent play of the game, so its round is over. | | 409    | play_chain_exhausted | The round has already been continued 5 times. |

            operationId: postGamePlay
            security:
                - UserBearerToken:
                    - games
            parameters:
                - name: game_id
                  in: path
                  required: true
                  schema:
                    type: string
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/PostGamePlayRequest"
            responses:
                "200":
                    description: Play tracked
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/PostGamePlayResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/games/{game_id}/shop:
        post:
            summary: Record an in-game shop transaction
            description: >
                Applies a state change that came from the game's own shop — an upgrade bought, a skin unlocked — rather than from playing. Use this instead of a scoreless play: a transaction counts toward no play count, leaderboard, playtime total or games-played goal, and is not part of a round, so it can neither be continued nor continue one.

                It does contribute the state it grants. An event watching a counter, gauge or set can complete on it, and an ongoing mission is advanced and can complete — but a transaction never starts a mission, and never reaches a goal that measures a round (level, highscore, games_played, or any goal with a playtime cap). Those complete on the user's next play instead.

                A transaction carrying an `idempotency_key` — a UUID the client stamps on it, fresh per transaction and the same across every retry of it — is applied once however often it arrives: each repeat returns the user's current `state`, completing and rewarding nothing further. A key is scoped to the caller and the game, so it only has to be unique among that user's own submissions for this game; it is shared with the play endpoint. Omit the key to have every request applied on its own.

                | Status | Code                 | Meaning                                      | | ------ | -------------------- | -------------------------------------------- | | 404    | game_not_found       | The game does not exist or was deleted.      | | 400    | invalid_state_change | A state_change value is unknown, carries an operation its type does not support, or is out of range. | | 429    | shop_rate_limited    | More than 120 transactions for this game within the hour. Unlike a rate-limited play, this is refused rather than silently dropped. |

            operationId: postGameShop
            security:
                - UserBearerToken:
                    - games
            parameters:
                - name: game_id
                  in: path
                  required: true
                  schema:
                    type: string
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/PostGameShopRequest"
            responses:
                "200":
                    description: Transaction recorded
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/PostGameShopResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/games/{game_id}/rating:
        post:
            summary: Rate a game
            description: >
                Submits the user's 1-5 star rating for a game. A user may only rate a game they have played (at least one recorded play) and each user can rate a game exactly once — the rating is immutable and cannot be changed or resubmitted.

                | Status | Code           | Meaning                                             | | ------ | -------------- | --------------------------------------------------- | | 400    | invalid_rating | rating is outside the 1-5 range.                    | | 403    | not_played     | The user has not played this game yet.              | | 404    | game_not_found | The game does not exist or was deleted.             | | 409    | already_rated  | The user has already rated this game.               |

            operationId: rateGame
            security:
                - UserBearerToken:
                    - games
            parameters:
                - name: game_id
                  in: path
                  required: true
                  schema:
                    type: string
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/RateGameRequest"
            responses:
                "204":
                    description: Rating recorded
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/games/{game_id}/leaderboard:
        get:
            summary: Get a game's leaderboard
            description: >
                Returns the top 50 users for the game ranked by their best score: the highest level reached for level games, the highest score achieved for highscore games. Each entry carries the user's rank, username, picture url and score.

                | Status | Code           | Meaning                                 | | ------ | -------------- | --------------------------------------- | | 404    | game_not_found | The game does not exist or was deleted. |

            operationId: getGameLeaderboard
            security:
                - UserBearerToken:
                    - games
            parameters:
                - name: game_id
                  in: path
                  required: true
                  schema:
                    type: string
            responses:
                "200":
                    description: Game leaderboard
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/GetGameLeaderboardResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/games/{game_id}/share:
        post:
            summary: Create or fetch a shareable link for a game
            description: >
                Mints a new globally-unique share id for the game that the public `GET /shares/{id}` endpoint resolves. A user may share the same game repeatedly; every call returns a fresh share id. An optional score can be attached to the share. An app carrying a share link of its own gets that link back instead (see `share_id`).

                | Status | Code           | Meaning                                 | | ------ | -------------- | --------------------------------------- | | 404    | game_not_found | The game does not exist or was deleted. | | 429    | rate_limited   | The user has created too many shares this hour. | | 400    | invalid_score  | The score exceeds the user's recorded best score for this game. |

            operationId: createGameShare
            security:
                - UserBearerToken:
                    - games
            parameters:
                - name: game_id
                  in: path
                  required: true
                  schema:
                    type: string
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/CreateGameShareRequest"
            responses:
                "200":
                    description: Share created or already existed
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/CreateShareResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/comics/{comic_id}:
        get:
            summary: Get a comic
            description: >
                Retrieves a comic by its ID.

                | Status | Code           | Meaning                                      | | ------ | -------------- | -------------------------------------------- | | 404    | comic_not_found | The comic does not exist or was deleted.    |

            operationId: getComic
            security:
                - UserBearerToken: []
            parameters:
                - name: comic_id
                  in: path
                  required: true
                  schema:
                    type: string
            responses:
                "200":
                    description: Comic
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/Comic"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/comics/{comic_id}/parts/{part_index}:
        get:
            summary: Get a full comic part
            description: >
                Returns a single comic part with all its pages. This is the only comic endpoint that populates pages and creates the part's server-side ad slots (a mid-part native ad, plus a trailing interstitial while the part is unfinished); the list/detail/feed endpoints return part metadata without pages.

                Reading is sequential: only a part the user has already read or the single next readable part can be fetched. Any other part — locked, behind a still-processing gap, or an unknown index — returns part_locked.

                | Status | Code            | Meaning                                        | | ------ | --------------- | ---------------------------------------------- | | 400    | part_locked     | The part is locked or not visible to the user. | | 404    | comic_not_found | The comic does not exist or was deleted.       |

            operationId: getComicPart
            security:
                - UserBearerToken: []
            parameters:
                - name: comic_id
                  in: path
                  required: true
                  schema:
                    type: string
                - name: part_index
                  in: path
                  required: true
                  schema:
                    type: integer
            responses:
                "200":
                    description: Comic part
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/ComicPart"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/comics/{comic_id}/parts/{part_index}/read:
        post:
            summary: Mark a comic part as read
            description: >
                Marks a specific part of a comic as read by the user.

                | Status | Code            | Meaning                                          | | ------ | --------------- | ------------------------------------------------ | | 400    | already_read    | The part is already marked as read.              | | 400    | part_locked     | The part is locked and cannot be marked as read. | | 404    | comic_not_found | The comic does not exist or was deleted.         |

            operationId: markComicPartRead
            security:
                - UserBearerToken: []
            parameters:
                - name: comic_id
                  in: path
                  required: true
                  schema:
                    type: string
                - name: part_index
                  in: path
                  required: true
                  schema:
                    type: integer
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/MarkComicPartReadRequest"
            responses:
                "200":
                    description: Part marked as read
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/MarkComicPartReadResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/comics/{comic_id}/parts/{part_index}/rating:
        post:
            summary: Rate a comic part
            description: >
                Submits the user's 1-5 star rating for a specific part of a comic. A user may only rate a part they have read and each user can rate a part exactly once — the rating is immutable and cannot be changed or resubmitted.

                | Status | Code            | Meaning                                          | | ------ | --------------- | ------------------------------------------------ | | 400    | invalid_rating  | rating is outside the 1-5 range.                 | | 403    | not_read        | The user has not read this part yet.             | | 404    | comic_not_found | The comic does not exist or was deleted.         | | 409    | already_rated   | The user has already rated this part.            |

            operationId: rateComicPart
            security:
                - UserBearerToken: []
            parameters:
                - name: comic_id
                  in: path
                  required: true
                  schema:
                    type: string
                - name: part_index
                  in: path
                  required: true
                  schema:
                    type: integer
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/RateComicPartRequest"
            responses:
                "204":
                    description: Rating recorded
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/comics/{comic_id}/share:
        post:
            summary: Create or fetch a shareable link for a comic
            description: >
                Mints a new globally-unique share id for the comic that the public `GET /shares/{id}` endpoint resolves. A user may share the same comic repeatedly; every call returns a fresh share id. An app carrying a share link of its own gets that link back instead (see `share_id`).

                | Status | Code            | Meaning                                        | | ------ | --------------- | ---------------------------------------------- | | 404    | comic_not_found | The comic does not exist or is not visible.    | | 429    | rate_limited    | The user has created too many shares this hour. |

            operationId: createComicShare
            security:
                - UserBearerToken: []
            parameters:
                - name: comic_id
                  in: path
                  required: true
                  schema:
                    type: string
            responses:
                "200":
                    description: Share created or already existed
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/CreateShareResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/shares/{id}:
        get:
            summary: Resolve a share
            description: >
                Resolves a public share id to its media preview and the sharer's public identity. Resolving a share counts as a click: it is tracked for analytics, deduplicated per client IP for a short window.

                | Status | Code            | Meaning                               | | ------ | --------------- | ------------------------------------- | | 404    | share_not_found | Share id is unknown or media deleted. |

            operationId: getShare
            security:
                - UserBearerToken: []
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                    type: string
            responses:
                "200":
                    description: Share resolved
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/GetShareResponse"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/ads:
        post:
            summary: Create a client-side ad slot
            description: >
                Records a client-created ad slot for the user and returns it, including the `slot_id` used to fill it later via `PATCH /v1/ads/{id}`. `placement` is required; the optional `media_type`/`media_ref` attribute the slot to the game the ad is shown against. Clients create `feed` and `game` placement slots; comic ads are server-managed. Feed slots reference a session rather than a specific media, so they carry no `media_type`/`media_ref`.

                | Status | Code         | Meaning                                   | | ------ | ------------ | ----------------------------------------- | | 429    | rate_limited | Too many ad slots created recently.       |

            operationId: createAd
            security:
                - UserBearerToken:
                    - games
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/CreateAdRequest"
            responses:
                "200":
                    description: Ad slot created
                    content:
                        application/json:
                            schema:
                                $ref: "#/components/schemas/Ad"
                default:
                    $ref: "#/components/responses/GenericError"
    /v1/ads/{id}:
        patch:
            summary: Fill a client-side ad slot
            description: >
                Attaches client attribution data to an ad slot the user owns. A slot can be filled exactly once. Overly long string fields are truncated and the bid is capped server-side.

                | Status | Code           | Meaning                                | | ------ | -------------- | -------------------------------------- | | 404    | ad_not_found   | The ad slot does not exist.            | | 409    | already_filled | The ad slot has already been filled.   |

            operationId: fillClientAd
            security:
                - UserBearerToken: []
            parameters:
                - name: id
                  in: path
                  required: true
                  schema:
                    type: string
                    format: uuid
            requestBody:
                required: true
                content:
                    application/json:
                        schema:
                            $ref: "#/components/schemas/FillClientAdRequest"
            responses:
                "204":
                    description: Ad slot filled
                default:
                    $ref: "#/components/responses/GenericError"
components:
    securitySchemes:
        UserBearerToken:
            type: http
            scheme: bearer
            bearerFormat: JWT
    schemas:
        GetFeedResponse:
            type: object
            required:
                - session_id
                - total_earnings_local_currency
                - next_earning_progress
                - watchtime_rewards_available_after
                - items
            properties:
                session_id:
                    type: string
                    format: uuid
                total_earnings_local_currency:
                    type: string
                    description: >
                        The session's total earnings so far, with base and bonus summed into a single amount. Together with its PostFeedEventsResponse counterpart this is deliberately the only earnings field that is a plain string rather than a Reward object: the session total is rendered as one running counter, and the base/bonus breakdown is only meaningful on individual rewards, where it is exposed.

                next_earning_progress:
                    type: number
                    description: Progress towards the next earning (0-1)
                watchtime_rewards_available_after:
                    type: string
                    format: date-time
                    nullable: true
                    description: >
                        When watch time starts earning again, or null while it still earns. Set once the user has reached the daily watchtime-reward limit: videos keep being served and events keep being accepted, but the watch time in them is no longer rewarded until this moment.

                items:
                    type: array
                    items:
                        $ref: "#/components/schemas/GetFeedItem"
        GetFeedSessionError:
            type: object
            required:
                - error
                - code
            properties:
                error:
                    type: string
                code:
                    type: string
                    enum:
                        - session_not_found
                        - session_expired
        GetFeedItem:
            oneOf:
                - $ref: "#/components/schemas/GetFeedVideoItem"
                - $ref: "#/components/schemas/GetFeedAdItem"
                - $ref: "#/components/schemas/GetFeedGameItem"
                - $ref: "#/components/schemas/GetFeedComicItem"
            discriminator:
                propertyName: item_type
                mapping:
                    video: "#/components/schemas/GetFeedVideoItem"
                    ad: "#/components/schemas/GetFeedAdItem"
                    game: "#/components/schemas/GetFeedGameItem"
                    comic: "#/components/schemas/GetFeedComicItem"
        GetFeedVideoItem:
            type: object
            allOf:
                - type: object
                  required:
                    - item_type
                  properties:
                    item_type:
                        type: string
                        const: video
                - $ref: "#/components/schemas/Video"
                - type: object
                  required:
                    - user_liked
                  properties:
                    user_liked:
                        type: boolean
                        deprecated: true
                        description: Use `liked_at` instead.
        GetFeedAdItem:
            type: object
            allOf:
                - type: object
                  required:
                    - item_type
                  properties:
                    item_type:
                        type: string
                        const: ad
                - $ref: "#/components/schemas/Ad"
        GetFeedGameItem:
            type: object
            allOf:
                - type: object
                  required:
                    - item_type
                  properties:
                    item_type:
                        type: string
                        const: game
                - $ref: "#/components/schemas/Game"
        GetFeedComicItem:
            type: object
            allOf:
                - type: object
                  required:
                    - item_type
                  properties:
                    item_type:
                        type: string
                        const: comic
                - type: object
                  description: General comic information. Note that only the next part the user has not read yet is included.
                  allOf:
                    - $ref: "#/components/schemas/Comic"
        PostFeedEventsRequest:
            type: object
            required:
                - session_id
                - events
            properties:
                session_id:
                    type: string
                    format: uuid
                events:
                    type: array
                    items:
                        $ref: "#/components/schemas/PostFeedEvent"
        PostFeedEvent:
            type: object
            required:
                - type
                - video_id
                - timestamp
            properties:
                type:
                    type: string
                    enum:
                        - video_like
                        - video_unlike
                        - video_watch_time
                video_id:
                    type: string
                    format: uuid
                watch_seconds:
                    type: number
                    format: double
                timestamp:
                    type: string
                    format: date-time
        PostFeedEventsResponse:
            type: object
            required:
                - session_id
                - total_earnings_local_currency
                - earned_local_currency
                - next_earning_progress
                - watchtime_rewards_available_after
            properties:
                session_id:
                    type: string
                    format: uuid
                total_earnings_local_currency:
                    type: string
                    description: >
                        The session's total earnings so far, with base and bonus summed into a single amount. Plain string instead of a Reward object — see GetFeedResponse for the rationale.

                earned_local_currency:
                    description: Local currency earned this request, or null if nothing was earned.
                    nullable: true
                    allOf:
                        - $ref: "#/components/schemas/Reward"
                next_earning_progress:
                    type: number
                    description: Progress towards the next earning (0-1)
                watchtime_rewards_available_after:
                    type: string
                    format: date-time
                    nullable: true
                    description: >
                        When watch time starts earning again, or null while it still earns. The request that reaches the daily watchtime-reward limit is the first to carry it, so no further earning is expected until this moment.

        GetUserProfileResponse:
            type: object
            required:
                - watchtime_streak
            properties:
                watchtime_streak:
                    $ref: "#/components/schemas/GetUserProfileWatchtimeStreak"
        GetUserProfileWatchtimeStreak:
            type: object
            required:
                - day_count
                - bonus_percentage
                - day_count_threshold
                - required_daily_minutes
                - completed_today
                - minutes_until_complete
                - fails_in_minutes
            properties:
                day_count:
                    type: integer
                    description: Current streak length in completed days (0 when there is no active streak).
                bonus_percentage:
                    type: integer
                    description: >
                        Reward bonus percent applied once the streak reaches the threshold.

                day_count_threshold:
                    type: integer
                    description: >
                        Completed days required before the bonus activates.

                required_daily_minutes:
                    type: integer
                    description: >
                        Minutes the user must watch each day to complete the day and keep the streak alive. Taken from the active streak's snapshot, or the live config default when no active streak exists.

                completed_today:
                    type: boolean
                    description: Whether the user has already completed today's watch requirement.
                minutes_until_complete:
                    type: integer
                    nullable: true
                    description: >
                        Minutes left to watch today to advance the streak. Null if completed today or no active streak exists.

                fails_in_minutes:
                    type: integer
                    nullable: true
                    description: >
                        Minutes until the streak fails if no further day is completed. Null if completed today or no active streak exists.

        ListUserVideosResponse:
            type: object
            required:
                - items
                - next_cursor
            properties:
                items:
                    type: array
                    items:
                        $ref: "#/components/schemas/Video"
                next_cursor:
                    type: string
                    nullable: true
                    description: Cursor for the next page (RFC3339Nano timestamp), null if no more items.
        ListUserCreatorsResponse:
            type: object
            required:
                - items
            properties:
                items:
                    type: array
                    items:
                        $ref: "#/components/schemas/Creator"
        FollowCreatorRequest:
            type: object
            required:
                - action
            properties:
                action:
                    type: string
                    enum:
                        - follow
                        - unfollow
        ListUserGamesResponse:
            type: object
            required:
                - games
            properties:
                games:
                    type: array
                    items:
                        $ref: "#/components/schemas/Game"
        ListUserComicsResponse:
            type: object
            required:
                - items
            properties:
                items:
                    type: array
                    description: Each item only contains the next part the user has not ready yet or the last part of the comic.
                    items:
                        $ref: "#/components/schemas/Comic"
        CreateGameShareRequest:
            type: object
            required:
                - score
            properties:
                score:
                    type: integer
                    nullable: true
                    description: If set, the score to associate with the share.
        CreateShareResponse:
            type: object
            required:
                - share_id
                - share_url
                - message
            properties:
                share_id:
                    type: string
                    nullable: true
                    description: >
                        Id of the created share, which `GET /v1/shares/{id}` resolves. Null when the app carries a share link of its own: share_url is then that link and no share was created, so there is nothing to resolve.

                share_url:
                    type: string
                    format: uri
                message:
                    type: string
                    description: Translated share message to use for external sharing. Does not include the link yet.
        GetShareResponse:
            type: object
            required:
                - share_id
                - sharer_username
                - sharer_picture_url
                - media
            properties:
                share_id:
                    type: string
                sharer_username:
                    type: string
                    description: Public display name of the user who created the share.
                sharer_picture_url:
                    type: string
                    description: Public avatar url of the user who created the share.
                media:
                    $ref: "#/components/schemas/ShareMedia"
        ReportVideoRequest:
            type: object
            required:
                - reason
            properties:
                reason:
                    type: string
                    enum:
                        - sexual_content
                        - violent_or_repulsive_content
                        - hateful_or_abusive_content
                        - harassment_or_bullying
                        - harmful_or_dangerous_acts
                        - suicide_self_harm_or_eating_disorders
                        - misinformation
                        - child_abuse
                        - promotes_terrorism
                        - spam_or_misleading
                        - legal_issue
        ListCreatorVideosResponse:
            type: object
            required:
                - items
                - next_cursor
            properties:
                items:
                    type: array
                    items:
                        $ref: "#/components/schemas/Video"
                next_cursor:
                    type: string
                    nullable: true
                    description: The last video's id (UUID), used as the next page's cursor; null if no more items.
        ListGamesResponse:
            type: object
            required:
                - items
            properties:
                items:
                    type: array
                    items:
                        $ref: "#/components/schemas/Game"
        PostGamePlayRequest:
            type: object
            required:
                - score
                - playtime_seconds
            properties:
                score:
                    type: integer
                    nullable: true
                    description: >
                        Level reached (level games) or score achieved (highscore games). Null on game over.

                playtime_seconds:
                    type: integer
                    description: >
                        Whole seconds the client played since the previous submission, so a continued round's submissions each carry their own slice rather than the round's total.

                idempotency_key:
                    type: string
                    format: uuid
                previous_play_id:
                    type: string
                    format: uuid
                    description: >
                        The play_id of the submission this one continues, when the client is resuming a round rather than starting one. It must be the caller's most recent play of this game — a shop transaction in between does not end a round — and a round may be continued at most 5 times. Omit to start a new round.

                state_change:
                    type: object
                    additionalProperties:
                        $ref: "#/components/schemas/PostGamePlayStateDelta"
                    description: >
                        Per-value state changes keyed by value name (may be empty or omitted). Each name must be configured on the game and each operation must be one the value's type supports; anything else returns 400 invalid_state_change. The changes are applied to the user's running state snapshot for this game, which the response returns as `state`.

                placement:
                    type: string
                    enum:
                        - feed
                        - games_tab
                    description: >
                        Where the play occurred: inside the video feed or the dedicated games tab. Defaults to feed when omitted.

        PostGamePlayStateDelta:
            type: object
            description: >
                The operations one play applies to a single state value. Every field is optional and each one belongs to a single value type, so only the operations of the value's configured type may be set - anything else returns 400 invalid_state_change:

                | type    | operations                                             | | ------- | ------------------------------------------------------ | | counter | `increment` (non-negative)                             | | gauge   | `increment` (either sign)                              | | string  | `change_string` (must match the configured pattern)     | | boolean | `change_boolean`                                       | | number  | `change_number`                                        | | set     | `set_add`, `set_remove` (configured elements)           | | list    | `list_add`, `list_remove` (indices), `list_replace`     |

                A value may carry several operations at once. List operations are applied in a fixed order - `list_replace`, then `list_remove`, then `list_add` - and every index refers to the list as it was before this play, so indices do not shift underneath each other.

                A play that breaks a bound is rejected whole: taking a list past its `max_items`, taking the game's whole state for this user past its size limit, or overflowing a counter's or gauge's total - a signed 64-bit integer - returns 400 invalid_state_change rather than applying the part that fits. Short of that overflow a counter's total and a gauge's value are unbounded. The state as a whole is bounded even though each value is bounded on its own, since nothing about a single value's limits bounds their sum, and every play stores the whole state.

            properties:
                increment:
                    type: integer
                    format: int64
                    description: >
                        Amount to add to a counter (non-negative) or gauge value this play. A gauge takes either sign, so a negative increment is how it moves down.

                change_string:
                    type: string
                    description: >
                        The new value of a string value, overriding whatever was stored. Must match the value's configured pattern.

                change_boolean:
                    type: boolean
                    description: >
                        The new value of a boolean value, overriding whatever was stored.

                change_number:
                    type: number
                    format: double
                    description: >
                        The new value of a number value, overriding whatever was stored.

                set_add:
                    type: array
                    description: >
                        Elements to add to a set value. Each must be one of the value's configured elements.

                    items:
                        type: string
                set_remove:
                    type: array
                    description: >
                        Elements to drop from a set value. Each must be one of the value's configured elements and may not also be added.

                    items:
                        type: string
                list_add:
                    type: array
                    description: >
                        Items to append to a list value, each shaped by the value's configured item schema.

                    items: {}
                list_remove:
                    type: array
                    description: >
                        Positions to drop from a list value, as indices into the list as it was before this play.

                    items:
                        type: integer
                        minimum: 0
                list_replace:
                    type: array
                    description: Items to overwrite in a list value.
                    items:
                        $ref: "#/components/schemas/PostGamePlayStateReplace"
        PostGamePlayStateReplace:
            type: object
            required:
                - index
                - value
            properties:
                index:
                    type: integer
                    minimum: 0
                    description: >
                        Position in the list as it was before this play. Must exist.

                value:
                    description: >
                        The item to store at that position, shaped by the value's configured item schema.

                    nullable: true
        PostGamePlayResponse:
            type: object
            required:
                - earned_local_currency
                - completed_events
                - completed_missions
                - ad
                - state
                - play_id
            properties:
                play_id:
                    type: string
                    format: uuid
                    nullable: true
                    description: >
                        Handle of the submission just recorded, to send back as `previous_play_id` when continuing this round. Null when the play was dropped (duplicate submission, rate limit): nothing was recorded, so there is nothing to continue.

                state:
                    description: >
                        The user's game state after this play, with every configured value present. Returned unchanged when the play was dropped (duplicate submission, rate limit), so the client can always reconcile against it.

                    allOf:
                        - $ref: "#/components/schemas/GameState"
                earned_local_currency:
                    description: Local currency earned this request, or null if nothing was earned.
                    nullable: true
                    allOf:
                        - $ref: "#/components/schemas/Reward"
                completed_events:
                    type: array
                    description: Events completed this request.
                    items:
                        $ref: "#/components/schemas/GameEvent"
                completed_missions:
                    type: array
                    description: Missions completed this request.
                    items:
                        $ref: "#/components/schemas/GameMission"
                ad:
                    nullable: true
                    allOf:
                        - $ref: "#/components/schemas/Ad"
                    description: An ad to show, served on a fixed cadence; null otherwise.
        PostGameShopRequest:
            type: object
            required:
                - state_change
            properties:
                state_change:
                    type: object
                    additionalProperties:
                        $ref: "#/components/schemas/PostGamePlayStateDelta"
                    description: >
                        Per-value state changes keyed by value name, in the same form the play endpoint takes and held to the same per-value bounds. These are the values the purchase moved.

                idempotency_key:
                    type: string
                    format: uuid
                placement:
                    type: string
                    enum:
                        - feed
                        - games_tab
                    description: >
                        Where the game was running when the transaction happened: inside the video feed or the dedicated games tab. Defaults to feed when omitted.

        PostGameShopResponse:
            type: object
            required:
                - earned_local_currency
                - completed_events
                - completed_missions
                - state
            properties:
                state:
                    description: >
                        The user's game state after the transaction, with every configured value present.

                    allOf:
                        - $ref: "#/components/schemas/GameState"
                earned_local_currency:
                    description: Local currency earned this request, or null if nothing was earned.
                    nullable: true
                    allOf:
                        - $ref: "#/components/schemas/Reward"
                completed_events:
                    type: array
                    description: Events the transaction completed.
                    items:
                        $ref: "#/components/schemas/GameEvent"
                completed_missions:
                    type: array
                    description: Missions the transaction completed.
                    items:
                        $ref: "#/components/schemas/GameMission"
        GetGameLeaderboardResponse:
            type: object
            required:
                - ranks
                - user_rank
            properties:
                ranks:
                    type: array
                    description: >
                        The top 50 users for the game, ordered by rank ascending. Users without a username are left out - except the requesting user themselves, who is always listed at their own position when they have one, so an unnamed user sees their rank without appearing on anyone else's board.

                    items:
                        $ref: "#/components/schemas/GameLeaderboardRank"
                user_rank:
                    $ref: "#/components/schemas/GameLeaderboardRank"
        GameLeaderboardRank:
            type: object
            required:
                - rank
                - username
                - picture_url
                - score
            properties:
                rank:
                    type: integer
                    nullable: true
                    description: 1-based position in the leaderboard; null in user_rank when the user is not in the top 50.
                username:
                    type: string
                picture_url:
                    type: string
                score:
                    type: integer
                    description: Best level (level games) or best score (highscore games); 0 if never played.
        MarkComicPartReadRequest:
            type: object
            required:
                - pages
            properties:
                pages:
                    type: array
                    description: Per page user metrics.
                    items:
                        type: object
                        required:
                            - index
                            - read_seconds
                        properties:
                            index:
                                type: integer
                                description: The index of the page.
                            read_seconds:
                                type: number
                                format: double
                                minimum: 0
                                description: The number of seconds the user read the page.
        MarkComicPartReadResponse:
            type: object
            required:
                - next_part
                - earned_local_currency
            properties:
                next_part:
                    type: object
                    nullable: true
                    description: >
                        The next part of the comic to read, or null if there is no next part. Carries part metadata only — its pages are null; fetch them from GET /v1/comics/{comic_id}/parts/{part_index}.

                    allOf:
                        - $ref: "#/components/schemas/ComicPart"
                earned_local_currency:
                    description: Local currency earned this request, or null if nothing was earned.
                    nullable: true
                    allOf:
                        - $ref: "#/components/schemas/Reward"
        RateGameRequest:
            type: object
            required:
                - rating
            properties:
                rating:
                    type: integer
                    minimum: 1
                    maximum: 5
                    description: The rating in whole stars, from 1 to 5. Values outside this range are rejected with invalid_rating.
        RateComicPartRequest:
            type: object
            required:
                - rating
            properties:
                rating:
                    type: integer
                    minimum: 1
                    maximum: 5
                    description: The rating in whole stars, from 1 to 5. Values outside this range are rejected with invalid_rating.
        CreateAdRequest:
            type: object
            required:
                - type
                - lock_seconds
                - placement
            properties:
                type:
                    type: string
                    enum:
                        - offer
                        - external
                external_sub_type:
                    type: string
                    enum:
                        - native
                        - interstitial
                        - rewarded
                    nullable: true
                    description: Sub-type for external ads; null or omitted otherwise.
                lock_seconds:
                    type: integer
                    minimum: 0
                    description: How long the ad is locked before it can be skipped.
                placement:
                    type: string
                    enum:
                        - feed
                        - game
                    description: The placement this ad belongs to.
                media_type:
                    type: string
                    enum:
                        - game
                    nullable: true
                    description: >
                        The type of media the slot references. Only game media is client-referenceable (feed and comic ads are server-managed). Null or omitted when no specific media is referenced.

                media_ref:
                    type: string
                    nullable: true
                    maxLength: 50
                    description: >
                        The referenced media's public id: the game's id/slug (e.g. `arrow-escape`). Stored as-is. Null or omitted when no specific media is referenced.

        FillClientAdRequest:
            type: object
            required:
                - client_data
            properties:
                client_data:
                    $ref: "#/components/schemas/AdClientData"
        AdClientData:
            type: object
            description: >
                Client-reported ad attribution. All fields are optional; string fields are truncated and the bid is capped server-side.

            properties:
                ad_platform:
                    type: string
                    maxLength: 50
                ad_source:
                    type: string
                    maxLength: 50
                ad_format:
                    type: string
                    maxLength: 50
                ad_unit_name:
                    type: string
                    maxLength: 50
                currency:
                    type: string
                    maxLength: 3
                    description: ISO 4217 currency code for the bid.
                bid:
                    type: string
                    maxLength: 12
                    description: Bid amount as a decimal string (e.g. "1.25").
        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.
        Video:
            type: object
            required:
                - id
                - creator
                - creator_tag
                - creator_name
                - caption
                - hls_url
                - uploaded_at
                - liked_at
                - like_count
                - view_count
                - cta
                - friend_likes
            properties:
                id:
                    type: string
                creator:
                    type: string
                    deprecated: true
                    description: Deprecated; use `creator_name`. Holds the same value as `creator_name`.
                creator_tag:
                    type: string
                    description: The creator's tag (unique username / public identifier).
                creator_name:
                    type: string
                    description: The creator's display name; falls back to the tag when unset.
                caption:
                    type: string
                hls_url:
                    type: string
                uploaded_at:
                    type: string
                    format: date-time
                liked_at:
                    type: string
                    format: date-time
                    nullable: true
                like_count:
                    type: integer
                    format: int64
                view_count:
                    type: integer
                    format: int64
                    description: Number of times the video has been watched.
                cta:
                    type: object
                    nullable: true
                    required:
                        - text
                        - url
                    properties:
                        text:
                            type: string
                        url:
                            type: string
                friend_likes:
                    type: array
                    description: The requesting user's friends who liked this video, most recently liked first.
                    items:
                        $ref: "#/components/schemas/VideoFriendLike"
        Creator:
            type: object
            required:
                - tag
                - name
                - bio
                - picture_url
                - picture_style
                - followed_at
                - video_count
                - followers_count
                - links
            properties:
                tag:
                    type: string
                    description: The creator's unique tag (username / public identifier).
                name:
                    type: string
                    description: The creator's display name; falls back to the tag when unset.
                bio:
                    type: string
                picture_url:
                    type: string
                    nullable: true
                    description: Profile picture URL; null when the creator has none set.
                picture_style:
                    type: string
                    enum:
                        - circle
                        - cover
                    description: |
                        How the client should render the profile picture — "circle"
                        (round avatar mask) or "cover" (full-bleed image, no mask).
                followed_at:
                    type: string
                    format: date-time
                    nullable: true
                    description: Timestamp when the requesting user followed this creator; null when not followed.
                video_count:
                    type: integer
                    format: int64
                followers_count:
                    type: integer
                    format: int64
                links:
                    type: array
                    items:
                        $ref: "#/components/schemas/CreatorLink"
        Game:
            type: object
            required:
                - id
                - name
                - icon_url
                - banner_image_url
                - card_image_url
                - category
                - game_url
                - type
                - version
                - play_count
                - is_in_endgame
                - events
                - missions
                - highscore
                - last_played_at
                - discovered_at
                - state
                - friend_highscores
                - rating_count
                - rating_avg
                - user_rating
                - section
            properties:
                id:
                    type: string
                    description: The game slug (e.g. "arrow-escape").
                name:
                    type: string
                icon_url:
                    type: string
                banner_image_url:
                    type: string
                card_image_url:
                    type: string
                category:
                    $ref: "#/components/schemas/GameCategory"
                game_url:
                    type: string
                type:
                    $ref: "#/components/schemas/GameType"
                version:
                    type: integer
                    description: >
                        The event-set version the user is on: the version of the user's completed events, or the newest version if they have not completed any event for this game yet.

                play_count:
                    type: integer
                    format: int64
                    description: Total number of recorded plays of this game across all users.
                is_in_endgame:
                    type: boolean
                    description: >
                        Whether the user has reached the endgame for this game: completed the event marked as the endgame in the version they are on. False when that version has no endgame event. Missions can only be joined once this is true.

                events:
                    type: array
                    items:
                        $ref: "#/components/schemas/GameEvent"
                missions:
                    type: array
                    description: >
                        The user's started missions (ongoing, and completed until they reset) plus the missions they could start by playing now.

                    items:
                        $ref: "#/components/schemas/GameMission"
                highscore:
                    type: integer
                    nullable: true
                    description: >
                        The user's best result for this game (highest level for level games, highest score for highscore games). Null when the user has not played this game yet or has no score yet.

                last_played_at:
                    type: string
                    format: date-time
                    nullable: true
                    description: Time of the user's most recent play, or null if never played.
                discovered_at:
                    type: string
                    format: date-time
                    nullable: true
                    description: >
                        Time of the user's first play of this game, or null if never played.

                state:
                    $ref: "#/components/schemas/GameState"
                friend_highscores:
                    type: array
                    description: The requesting user's friends who have played this game, with each friend's highscore, best first.
                    items:
                        $ref: "#/components/schemas/GameFriendHighscore"
                rating_count:
                    type: integer
                    format: int64
                    description: >
                        Number of 1-5 star ratings submitted for this game. Reported as 0 (together with rating_avg) until the game 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 game has collected at least 5 ratings (see rating_count).

                user_rating:
                    type: integer
                    nullable: true
                    description: >
                        The 1-5 star rating the requesting user submitted for this game, or null if they have not rated it. Unlike rating_count and rating_avg this is never hidden, and it never changes once set — a rating is immutable. Always null where there is no authenticated user, such as the public games catalog.

                section:
                    $ref: "#/components/schemas/GameSection"
        Comic:
            type: object
            required:
                - id
                - title
                - description
                - cover_url
                - parts_count
                - read_count
                - rating_count
                - rating_avg
                - read_time_avg
                - parts
                - friend_reading_progress
            properties:
                id:
                    type: string
                title:
                    type: string
                description:
                    type: string
                cover_url:
                    type: string
                parts_count:
                    type: integer
                    description: The total number of parts in the comic.
                read_count:
                    type: integer
                    format: int64
                    description: Total number of finished part-reads of this comic across all users.
                rating_count:
                    type: integer
                    format: int64
                    description: >
                        Total number of 1-5 star ratings across all of this comic's parts. Reported as 0 (together with rating_avg) until the comic has collected at least 5 part ratings in total.

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

                read_time_avg:
                    type: number
                    format: double
                    description: >
                        Sum of the per-part median read times, in seconds (reads over one hour are excluded as outliers). 0 when none of the comic's parts have been read.

                parts:
                    type: array
                    description: Depending on the request, this may contain all parts or only the next part the user has not read yet.
                    items:
                        $ref: "#/components/schemas/ComicPart"
                friend_reading_progress:
                    type: array
                    description: The requesting user's friends who have read at least one part of this comic, furthest read first.
                    items:
                        $ref: "#/components/schemas/ComicFriendReadingProgress"
        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.

        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.
        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"
        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"
        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.

        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"
        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
        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
        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: {}
        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"
    responses:
        GenericError:
            description: Generic Error
            content:
                application/json:
                    schema:
                        $ref: "#/components/schemas/Error"
