openapi: 3.1.0
info:
  title: Vstrechi API
  version: 0.1.0
  description: API contract for the Vstrechi MVP backend.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://meets.cc/api/backend
    description: Staging HTTPS REST API.
  - url: https://meets.cc
    description: Staging web and Yandex OAuth origin.
tags:
  - name: assistant
    description: Experimental authenticated read-only AI Chat.
  - name: health
    description: Runtime health checks.
  - name: auth
    description: M2 dev authentication and session endpoints.
  - name: users
    description: Current user and profile endpoints.
  - name: events
    description: Event creation, details, lifecycle and discovery endpoints.
  - name: attendance
    description: M3 event participation, invitations and RSVP endpoints.
  - name: feed
    description: M4 feed endpoint. Until graph modules exist, feed uses published public events.
  - name: event_posts
    description: M11 event announcements, posts and comments.
  - name: coordination
    description: M5 date coordination endpoints for event planning.
  - name: reviews
    description: M7 event reviews after completed meetings.
  - name: moderation
    description: M7 user reports and future admin-lite moderation endpoints.
  - name: blocks
    description: M7 user-to-user blocking endpoints.
  - name: calendar
    description: M8 social calendar entries, invites, reminders and event links.
  - name: notifications
    description: Persistent user notifications and unread state.
  - name: chats
    description: Authenticated one-to-one conversations and unread state.
  - name: payments
    description: M6 payment profile and future payment flow endpoints.
  - name: images
    description: Controlled external image proxy and immutable uploaded event media.
paths:
  /voice/uploads:
    post:
      tags: [chats]
      operationId: uploadVoiceMessage
      summary: Upload a private mono Opus CAF voice draft.
      description: >-
        Requires current direct-chat membership or organizer/going discussion access.
        A stable upload UUID is an exact retry key over owner, target, waveform and audio bytes.
        Maximum 2 MiB and 300 seconds; server derives duration from validated CAF packet tables.
        Up to 60 new uploads per hour and 256 MiB retained audio per owner. Unattached drafts
        expire after 24 hours. Content remains private and is never returned by a public URL.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: X-Meets-Voice-Upload-ID
          in: header
          required: true
          schema:
            type: string
            format: uuid
        - name: X-Meets-Voice-Target-Kind
          in: header
          required: true
          schema:
            type: string
            enum:
              - direct
              - discussion
        - name: X-Meets-Voice-Target-ID
          in: header
          required: true
          schema:
            type: string
            format: uuid
        - name: X-Meets-Voice-Waveform
          in: header
          required: true
          description: Standard base64 encoding of 1 to 64 amplitude bytes (0 to 255).
          schema:
            type: string
            maxLength: 88
      requestBody:
        required: true
        content:
          audio/x-caf:
            schema:
              type: string
              format: binary
              maxLength: 2097152
      responses:
        "201":
          description: Uploaded private audio metadata, also for exact retries.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VoiceMessage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: Upload UUID was already used with different audio or target.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          description: Voice file exceeds two MiB.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Account retained-audio storage limit reached; preserve the local recording and let the user retry after freeing space.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Voice upload quota or concurrent capacity exceeded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          description: Authentication temporarily unavailable; keep the current session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /voice/uploads/{voice_id}:
    get:
      tags: [chats]
      operationId: getVoiceMessageContent
      summary: Read private voice bytes after a current membership check.
      description: >-
        Unsent drafts are readable only by their owner for 24 hours. Published audio requires
        a nondeleted referencing message and current conversation membership. Tombstone audio
        is inaccessible even to its author. Uses private no-store responses and supports ranges.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: voice_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Private mono Opus CAF audio bytes.
          content:
            audio/x-caf:
              schema:
                type: string
                format: binary
        "206":
          description: Requested audio byte range.
          content:
            audio/x-caf:
              schema:
                type: string
                format: binary
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "416":
          description: Requested byte range is outside the audio file.
          content:
            text/plain:
              schema:
                type: string
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          description: Authentication temporarily unavailable; keep the current session.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /healthz:
    get:
      tags:
        - health
      operationId: getHealthz
      summary: Check API, PostgreSQL and Redis availability.
      security: []
      responses:
        "200":
          description: API dependencies are available.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "503":
          description: One or more API dependencies are unavailable.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
  /images/proxy:
    get:
      tags:
        - images
      operationId: proxyImage
      summary: Proxy an external HTTPS image through the controlled image proxy.
      description: Fetches only HTTPS image URLs, rejects private or local destinations and returns only image content.
      security: []
      parameters:
        - name: url
          in: query
          required: true
          schema:
            type: string
            format: uri
      responses:
        "200":
          description: Proxied image bytes.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "400":
          $ref: "#/components/responses/BadRequest"
        "413":
          description: Image is larger than the proxy limit.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "415":
          description: Upstream response is not an image.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "502":
          description: Image is unavailable.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /images/search:
    get:
      tags:
        - images
      operationId: searchImages
      summary: Search licensed Pexels photos for an event.
      description: Returns provider-neutral landscape photo results. Selected photos must be copied through the immutable Meets upload endpoint before being attached to an event.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: query
          in: query
          required: true
          schema:
            type: string
            minLength: 2
            maxLength: 80
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 1
      responses:
        "200":
          description: Licensed image search results.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImageSearchResponse"
        "400":
          description: Invalid image search query or page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "502":
          description: The configured image provider is unavailable.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Image search is not configured.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /images/uploads:
    post:
      tags:
        - images
      operationId: uploadImage
      summary: Upload an immutable image for the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: X-Meets-Image-Optimization
          in: header
          required: false
          description: Opt in to bounded lossy WebP conversion. Omit to preserve the legacy exact-byte upload contract.
          schema:
            type: string
            enum: [webp]
      requestBody:
        required: true
        content:
          image/jpeg:
            schema:
              type: string
              format: binary
          image/png:
            schema:
              type: string
              format: binary
          image/webp:
            schema:
              type: string
              format: binary
      responses:
        "201":
          description: Immutable image stored; content_type and byte_size describe the stored bytes, which may differ from the request when optimization is requested.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UploadedImageResponse"
        "400":
          description: Empty or malformed image upload.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "413":
          description: Image exceeds 1000000 bytes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "415":
          description: Unsupported image type or declared type does not match the bytes.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /images/uploads/{image_id}:
    get:
      tags:
        - images
      operationId: getUploadedImage
      summary: Read immutable uploaded image bytes.
      security: []
      parameters:
        - name: image_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Stored image bytes with immutable public caching.
          headers:
            Content-Length:
              schema:
                type: integer
                minimum: 1
                maximum: 1000000
            Cache-Control:
              schema:
                type: string
                const: public, max-age=31536000, immutable
            X-Content-Type-Options:
              schema:
                type: string
                const: nosniff
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
            image/webp:
              schema:
                type: string
                format: binary
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          description: Uploaded image does not exist.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/dev/start:
    post:
      tags:
        - auth
      operationId: startDevAuthChallenge
      summary: Start a dev email authentication challenge.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StartDevAuthChallengeRequest"
      responses:
        "200":
          description: Dev authentication challenge created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StartDevAuthChallengeResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/dev/verify:
    post:
      tags:
        - auth
      operationId: verifyDevAuthChallenge
      summary: Verify a dev email challenge and create a session.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyDevAuthChallengeRequest"
      responses:
        "200":
          description: Challenge verified and session created.
          headers:
            Set-Cookie:
              description: HttpOnly refresh token cookie.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/phone/call/start:
    post:
      tags:
        - auth
      operationId: startPhoneCallAuth
      summary: Start phone authentication through SMS.RU Call Auth.
      description: Creates a five-minute phone call challenge and returns the number the user must call from the submitted phone. Clients may send X-Request-ID for server-log correlation; the backend echoes it in the response.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StartPhoneCallAuthRequest"
      responses:
        "200":
          description: Phone call challenge created.
          headers:
            X-Request-ID:
              $ref: "#/components/headers/RequestID"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StartPhoneCallAuthResponse"
        "400":
          $ref: "#/components/responses/PhoneStartBadRequest"
        "429":
          $ref: "#/components/responses/PhoneRateLimited"
        "503":
          $ref: "#/components/responses/PhoneProviderUnavailable"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/phone/call/status:
    post:
      tags:
        - auth
      operationId: completePhoneCallAuth
      summary: Check phone call authentication status.
      description: Polls the provider status. Pending and verified states return 200. An expired challenge returns 410 with phone_challenge_expired. Clients may send X-Request-ID for server-log correlation; the backend echoes it in the response.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CompletePhoneCallAuthRequest"
      responses:
        "200":
          description: Pending or verified phone call auth state. Verified responses include an auth session.
          headers:
            X-Request-ID:
              $ref: "#/components/headers/RequestID"
            Set-Cookie:
              description: HttpOnly refresh token cookie when status is verified.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PhoneCallAuthStatusResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/PhoneChallengeInvalid"
        "410":
          $ref: "#/components/responses/PhoneChallengeExpired"
        "429":
          $ref: "#/components/responses/PhoneRateLimited"
        "503":
          $ref: "#/components/responses/PhoneProviderUnavailable"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/yandex/start:
    get:
      tags:
        - auth
      operationId: startYandexOAuth
      summary: Start the Yandex OAuth login flow.
      security: []
      parameters:
        - name: return_to
          in: query
          required: false
          schema:
            type: string
            default: /profile
          description: Local path to redirect to after successful login.
      responses:
        "303":
          description: Redirect to Yandex OAuth authorization page.
          headers:
            Location:
              description: Yandex OAuth authorization URL.
              schema:
                type: string
                format: uri
        "429":
          $ref: "#/components/responses/RateLimited"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/yandex/callback:
    get:
      tags:
        - auth
      operationId: completeYandexOAuth
      summary: Complete the Yandex OAuth callback and create a session.
      security: []
      parameters:
        - name: code
          in: query
          required: false
          schema:
            type: string
        - name: state
          in: query
          required: false
          schema:
            type: string
        - name: error
          in: query
          required: false
          schema:
            type: string
      responses:
        "303":
          description: Redirect to the app after success or back to login with an OAuth error.
          headers:
            Set-Cookie:
              description: HttpOnly refresh token cookie on success.
              schema:
                type: string
            Location:
              description: Local redirect target.
              schema:
                type: string
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/yandex/mobile:
    post:
      tags:
        - auth
      operationId: completeYandexMobileOAuth
      summary: Exchange a native Yandex LoginSDK token for a Meets session.
      description: The OAuth token is verified through Yandex user info using the Authorization header. It is never accepted in the URL or persisted by Meets.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/YandexMobileAuthRequest"
      responses:
        "200":
          description: Yandex token verified and standard Meets session created.
          headers:
            X-Request-ID:
              $ref: "#/components/headers/RequestID"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthResponse"
        "400":
          description: Invalid JSON or an empty oauth_token. Error code is invalid_request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: invalid_request
        "401":
          description: Yandex rejected the token, the user id is missing, or the token belongs to a client outside the allowlist. Error code is yandex_token_invalid or yandex_client_not_allowed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: The Yandex profile has no default phone. Error code is yandex_profile_incomplete.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: yandex_profile_incomplete
        "429":
          description: Local or Yandex rate limit. Error code is yandex_rate_limited.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: yandex_rate_limited
        "503":
          description: Yandex timed out, returned a malformed response, or is unavailable. Error code is yandex_provider_unavailable.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: yandex_provider_unavailable
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/refresh:
    post:
      tags:
        - auth
      operationId: refreshAuthSession
      summary: Rotate a refresh session.
      description: >-
        Accepts the HttpOnly refresh cookie. Native mobile clients may send
        `refresh_token` in the JSON body when cookie storage is unavailable.
        Without X-Refresh-Operation-ID, the existing one-use rotation behavior is
        unchanged. With the header, persist the operation UUID with the original
        refresh token before sending and reuse both after a lost response. The
        same operation returns the committed credentials only while its child
        session remains active and its fixed replay window has not expired. The
        window is at most 24 hours, bounded by the child refresh expiry; replay
        does not extend it. An access token recovered by replay may already be
        expired: persist the recovered refresh token and clear the pending
        operation atomically, then rotate with a new operation before using it.
        Preserve the original token and operation on transport failures or 503;
        401 and refresh_replay_expired are terminal for that operation.
      security:
        - cookieAuth: []
      parameters:
        - $ref: "#/components/parameters/RefreshOperationID"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefreshSessionRequest"
      responses:
        "200":
          description: Session rotated, or the same committed credentials recovered. Responses are not cacheable.
          headers:
            Cache-Control:
              description: Prevents storage of the credentials response.
              schema:
                type: string
                const: no-store
            Set-Cookie:
              description: HttpOnly refresh token cookie.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RefreshSessionResponse"
        "400":
          $ref: "#/components/responses/RefreshOperationBadRequest"
        "401":
          description: Missing or invalid refresh token, mismatched operation, or a revoked, expired or subsequently rotated child session. Error code is unauthorized.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          $ref: "#/components/responses/RefreshReplayExpired"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/RefreshReplayUnavailable"
  /auth/sessions:
    get:
      tags:
        - auth
      operationId: listAuthSessions
      summary: List active refresh sessions for the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Active sessions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthSessionsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/sessions/{session_id}:
    delete:
      tags:
        - auth
      operationId: revokeAuthSession
      summary: Revoke one active refresh session owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: session_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Session revoked.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/sessions/revoke-others:
    post:
      tags:
        - auth
      operationId: revokeOtherAuthSessions
      summary: Revoke every active refresh session except the current one.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Other sessions revoked.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /auth/logout:
    post:
      tags:
        - auth
      operationId: logoutAuthSession
      summary: Revoke the current refresh session.
      description: >-
        Accepts the same refresh cookie or JSON refresh_token as refresh. Without
        X-Refresh-Operation-ID, existing logout behavior is unchanged. When a
        refresh response was lost, send the original refresh token and its
        persisted operation UUID to revoke the child created by that operation.
        This remains possible after the replay window expires and its ciphertext
        is cleared, while the session linkage exists. Logout is idempotent for
        missing, unknown or already revoked tokens; a mismatched operation does
        not revoke a previously rotated child. It never returns recovered
        credentials. On 503 the server-side revocation is not confirmed; retry
        with the same token and operation.
      security:
        - cookieAuth: []
      parameters:
        - $ref: "#/components/parameters/RefreshOperationID"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefreshSessionRequest"
      responses:
        "204":
          description: Session revoked or already absent.
          headers:
            Set-Cookie:
              description: Expired refresh token cookie.
              schema:
                type: string
        "400":
          $ref: "#/components/responses/RefreshOperationBadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/RefreshReplayUnavailable"
  /me:
    get:
      tags:
        - users
      operationId: getCurrentUser
      summary: Get the current user and profile.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Current user and profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentUser"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/contact-matches:
    post:
      tags:
        - users
      operationId: matchCurrentUserContacts
      summary: Match normalized phone contacts to registered Meets profiles.
      description: The submitted phone list is used only for this request and is not stored.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContactMatchesRequest"
      responses:
        "200":
          description: Registered contacts, preserving submitted phone order.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContactMatchesResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/saved-events:
    get:
      tags:
        - events
      operationId: getMySavedEventLibrary
      summary: Get the current user's private saved events and collections.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Private event save library.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavedEventLibrary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/saved-events/{event_id}:
    parameters:
      - $ref: "#/components/parameters/EventID"
    put:
      tags:
        - events
      operationId: saveEvent
      summary: Save a visible event and replace its private collection membership.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SaveEventRequest"
      responses:
        "200":
          description: Current saved event state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavedEvent"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - events
      operationId: deleteSavedEvent
      summary: Remove an event from all private saved collections.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Event is no longer saved.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/event-collections:
    post:
      tags:
        - events
      operationId: createEventCollection
      summary: Create a private named event collection.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventCollectionNameRequest"
      responses:
        "201":
          description: Event collection created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventCollection"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/event-collections/{collection_id}:
    parameters:
      - name: collection_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - events
      operationId: renameEventCollection
      summary: Rename a private event collection.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EventCollectionNameRequest"
      responses:
        "200":
          description: Event collection renamed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventCollection"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - events
      operationId: deleteEventCollection
      summary: Delete a private collection without unsaving its events.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Collection deleted; event saves remain.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/reposts:
    get:
      tags:
        - event_posts
      operationId: listMyEventPostReposts
      summary: List the current user's event post repost history.
      description: Returns currently visible reposted posts from newest to oldest using opaque keyset pagination.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          description: Opaque cursor returned by the previous page.
          schema:
            type: string
      responses:
        "200":
          description: Current user's visible repost history.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostRepostHistoryResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/profile:
    patch:
      tags:
        - users
      operationId: updateCurrentUserProfile
      summary: Update the current user's profile.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateProfileRequest"
      responses:
        "200":
          description: Updated current user and profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentUser"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /users/profiles:
    get:
      tags:
        - users
      operationId: listPublicUserProfiles
      summary: Search public user profiles.
      security: []
      parameters:
        - name: q
          in: query
          schema:
            type: string
            maxLength: 120
        - name: city
          in: query
          schema:
            type: string
            maxLength: 120
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          description: Opaque cursor returned by the previous page.
          schema:
            type: string
      responses:
        "200":
          description: Public profiles.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicProfilesResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "500":
          $ref: "#/components/responses/InternalError"
  /users/{user_id}/profile:
    get:
      tags:
        - users
      operationId: getPublicUserProfile
      summary: Get a public user profile.
      security: []
      parameters:
        - $ref: "#/components/parameters/UserID"
      responses:
        "200":
          description: Public profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicProfile"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/friends:
    get:
      tags:
        - users
      operationId: listCurrentUserFriends
      summary: List mutual follows for the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          description: Opaque cursor returned by the previous page.
          schema:
            type: string
      responses:
        "200":
          description: Mutual friends.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FriendsResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /users/{user_id}/social:
    get:
      tags:
        - users
      operationId: getPublicProfileSocialCounts
      summary: Get public friends, followers and following counts for a profile.
      security: []
      parameters:
        - $ref: "#/components/parameters/UserID"
      responses:
        "200":
          description: Public social counts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProfileSocialCounts"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /users/{user_id}/friends:
    get:
      tags:
        - users
      operationId: listPublicProfileFriends
      summary: List public mutual follows for a profile.
      security: []
      parameters:
        - $ref: "#/components/parameters/UserID"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          description: Opaque cursor returned by the previous page.
          schema:
            type: string
      responses:
        "200":
          description: Public mutual follows.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicRelationshipPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /users/{user_id}/followers:
    get:
      tags:
        - users
      operationId: listPublicProfileFollowers
      summary: List public followers of a profile.
      security: []
      parameters:
        - $ref: "#/components/parameters/UserID"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          description: Opaque cursor returned by the previous page.
          schema:
            type: string
      responses:
        "200":
          description: Public followers.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicRelationshipPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /users/{user_id}/following:
    get:
      tags:
        - users
      operationId: listPublicProfileFollowing
      summary: List public profiles followed by a profile.
      security: []
      parameters:
        - $ref: "#/components/parameters/UserID"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          description: Opaque cursor returned by the previous page.
          schema:
            type: string
      responses:
        "200":
          description: Public followed profiles.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublicRelationshipPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /users/{user_id}/follow:
    parameters:
      - $ref: "#/components/parameters/UserID"
    get:
      tags:
        - users
      operationId: getUserFollowStatus
      summary: Get follow status between the current user and a public profile.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Follow status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FollowStatus"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - users
      operationId: followUser
      summary: Follow a public profile.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Follow status after following.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FollowStatus"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - users
      operationId: unfollowUser
      summary: Stop following a public profile.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Follow status after unfollowing.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FollowStatus"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/payment-profile:
    get:
      tags:
        - payments
      operationId: getCurrentUserPaymentProfile
      summary: Get the current user's organizer payment profile.
      description: M6 placeholder profile for paid event organizer verification; no production payout account is created.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Organizer payment profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizerPaymentProfile"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    patch:
      tags:
        - payments
      operationId: updateCurrentUserPaymentProfile
      summary: Update the current user's organizer payment profile placeholder.
      description: Allows self-setting the MVP placeholder status to unverified or verified; blocked is reserved for provider/admin state.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateOrganizerPaymentProfileRequest"
      responses:
        "200":
          description: Updated organizer payment profile.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrganizerPaymentProfile"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/events:
    get:
      tags:
        - events
      operationId: listCurrentUserEvents
      summary: List the current user's published meetings.
      description: Returns private and public meetings organized by the authenticated user, split into upcoming and past periods.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: period
          in: query
          schema:
            type: string
            enum:
              - upcoming
              - past
            default: upcoming
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Meetings organized by the current user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventListResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /events:
    get:
      tags:
        - events
      operationId: listEvents
      summary: List published public events for discovery.
      security: []
      parameters:
        - name: q
          in: query
          schema:
            type: string
            maxLength: 120
        - name: category
          in: query
          schema:
            type: string
            maxLength: 80
        - name: city
          in: query
          schema:
            type: string
            maxLength: 200
        - name: price
          in: query
          description: Price filter. Omit or use `any` to include both free and paid public events.
          schema:
            type: string
            enum:
              - any
              - free
              - paid
        - name: lat
          in: query
          description: Latitude for nearby event filtering. Must be combined with `lng` and `radius_km`.
          schema:
            type: number
            format: double
            minimum: -90
            maximum: 90
        - name: lng
          in: query
          description: Longitude for nearby event filtering. Must be combined with `lat` and `radius_km`.
          schema:
            type: number
            format: double
            minimum: -180
            maximum: 180
        - name: radius_km
          in: query
          description: Radius in kilometers for nearby event filtering. Must be combined with `lat` and `lng`.
          schema:
            type: number
            format: double
            exclusiveMinimum: 0
            maximum: 500
        - name: organizer_id
          in: query
          schema:
            type: string
            format: uuid
        - name: participant_user_id
          in: query
          description: Public profile activity filter. Returns public, published events organized by this user or where this user has a public `going` participant row; private, draft, cancelled, pending, left and incognito attendance is excluded.
          schema:
            type: string
            format: uuid
        - name: starts_from
          in: query
          schema:
            type: string
            format: date-time
        - name: starts_to
          in: query
          schema:
            type: string
            format: date-time
        - name: child_age_from_months
          in: query
          description: Lower bound of the requested child age range. Must be combined with `child_age_to_months`; events with overlapping declared ranges are returned.
          schema:
            type: integer
            minimum: 0
            maximum: 216
        - name: child_age_to_months
          in: query
          description: Upper bound of the requested child age range. Must be combined with `child_age_from_months`.
          schema:
            type: integer
            minimum: 0
            maximum: 216
        - name: sort
          in: query
          description: "`soon` orders scheduled events by start time; `distance` requires `lat`, `lng` and `radius_km`."
          schema:
            type: string
            enum:
              - soon
              - distance
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Published public events.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventListResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - events
      operationId: createEvent
      summary: Create a draft event for the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventRequest"
      responses:
        "201":
          description: Draft event created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDetails"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/map:
    get:
      tags:
        - events
      operationId: listEventMapItems
      summary: List published public events with coordinates for map discovery.
      security: []
      parameters:
        - name: bbox
          in: query
          description: Bounding box as min_lng,min_lat,max_lng,max_lat.
          schema:
            type: string
            example: "13.0,52.0,14.0,53.0"
        - name: category
          in: query
          schema:
            type: string
            maxLength: 80
        - name: price
          in: query
          description: Price filter. Omit or use `any` to include both free and paid public map events.
          schema:
            type: string
            enum:
              - any
              - free
              - paid
        - name: lat
          in: query
          description: Latitude for nearby map filtering. Must be combined with `lng` and `radius_km`.
          schema:
            type: number
            format: double
            minimum: -90
            maximum: 90
        - name: lng
          in: query
          description: Longitude for nearby map filtering. Must be combined with `lat` and `radius_km`.
          schema:
            type: number
            format: double
            minimum: -180
            maximum: 180
        - name: radius_km
          in: query
          description: Radius in kilometers for nearby map filtering. Must be combined with `lat` and `lng`.
          schema:
            type: number
            format: double
            exclusiveMinimum: 0
            maximum: 500
        - name: starts_from
          in: query
          schema:
            type: string
            format: date-time
        - name: starts_to
          in: query
          schema:
            type: string
            format: date-time
        - name: child_age_from_months
          in: query
          description: Lower bound of the requested child age range. Must be combined with `child_age_to_months`.
          schema:
            type: integer
            minimum: 0
            maximum: 216
        - name: child_age_to_months
          in: query
          description: Upper bound of the requested child age range. Must be combined with `child_age_from_months`.
          schema:
            type: integer
            minimum: 0
            maximum: 216
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        "200":
          description: Published public map events.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventMapResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}:
    parameters:
      - $ref: "#/components/parameters/EventID"
    get:
      tags:
        - events
      operationId: getEvent
      summary: Get event details visible to the current viewer.
      security: []
      responses:
        "200":
          description: Event details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          description: Event missing or hidden by visibility policy.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
    patch:
      tags:
        - events
      operationId: updateEvent
      summary: Update an event owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateEventRequest"
      responses:
        "200":
          description: Updated event details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDetails"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/publish:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - events
      operationId: publishEvent
      summary: Publish a draft event owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Published event details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Paid event organizer payment profile is not verified.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/cancel:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - events
      operationId: cancelEvent
      summary: Cancel an event owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Canceled event details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/participants:
    parameters:
      - $ref: "#/components/parameters/EventID"
    get:
      tags:
        - attendance
      operationId: listEventParticipants
      summary: List visible participants of an event.
      description: >-
        Public event participant identities are anonymously readable. Private event
        identities require organizer or existing attendance access. The total
        participant_count includes public and incognito going participants. Regular
        viewers receive the organizer and public participants only; the authenticated
        event organizer also receives incognito participant identities in items and
        visible_participant_count.
      security:
        - {}
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 30
        - name: cursor
          in: query
          description: Opaque cursor returned by the previous participant page.
          schema:
            type: string
      responses:
        "200":
          description: Privacy-safe participant page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventParticipantsResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/join:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - attendance
      operationId: joinEvent
      summary: Join a published public event.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/JoinEventRequest"
      responses:
        "200":
          description: Current user's participant row.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventParticipant"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - attendance
      operationId: leaveEvent
      summary: Leave a public event joined by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Updated participant row.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventParticipant"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/payment-intents:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - payments
      operationId: createEventPaymentIntent
      summary: Start a checkout payment intent for a paid public event.
      description: Creates an MVP dev-provider payment intent only; successful attendance confirmation is handled by future webhook processing.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "201":
          description: Payment intent created with checkout URL placeholder.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentIntent"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Event is not available for paid checkout by the current user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /payments/intents/{payment_intent_id}:
    parameters:
      - $ref: "#/components/parameters/PaymentIntentID"
    get:
      tags:
        - payments
      operationId: getPaymentIntent
      summary: Get the current user's payment intent.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Payment intent visible to the current user.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentIntent"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /payments/webhooks/{provider}:
    parameters:
      - $ref: "#/components/parameters/PaymentProvider"
    post:
      tags:
        - payments
      operationId: processPaymentWebhook
      summary: Process a payment provider webhook.
      description: M6 dev-provider webhook endpoint with signature verification placeholder and idempotency on provider event id.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: Provider-specific webhook payload. Dev provider expects id, type and provider_payment_id.
      responses:
        "200":
          description: Webhook processed or ignored as a duplicate.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaymentWebhookResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          description: Webhook signature is invalid.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/posts:
    get:
      tags:
        - event_posts
      operationId: listEventPostsBatch
      summary: List event post threads for multiple events.
      description: Batch helper for the unified feed. Public published events are readable anonymously; private events require current-user event access.
      security:
        - {}
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: event_id
          in: query
          required: false
          schema:
            type: array
            maxItems: 50
            items:
              type: string
              format: uuid
          style: form
          explode: true
      responses:
        "200":
          description: Event post threads grouped by event id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostThreadsBatchResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/posts:
    parameters:
      - $ref: "#/components/parameters/EventID"
    get:
      tags:
        - event_posts
      operationId: listEventPosts
      summary: List event announcements, posts and comments.
      description: Public published events are readable anonymously. Private events require current-user event access. Deleted comments remain as tombstones with `deleted_at` so replies stay connected.
      security:
        - {}
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - in: query
          name: scope
          schema:
            type: string
            enum: [publication]
          description: Explicit public publication scope. Omit only for legacy combined member history; outsiders never receive private discussion messages.
      responses:
        "200":
          description: Event post threads.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostThreadsResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - event_posts
      operationId: createEventPost
      summary: Create an event announcement, important organizer message or post.
      description: Only the event organizer can create event posts in the M11 slice.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventPostRequest"
      responses:
        "201":
          description: Created event post.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPost"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/posts/{post_id}/comments:
    parameters:
      - $ref: "#/components/parameters/EventID"
      - name: post_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - event_posts
      operationId: createEventPostComment
      summary: Comment on an event post.
      description: Authenticated users who can view the event can comment. `parent_comment_id` creates a threaded reply and must reference an active comment in the same event post.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventPostCommentRequest"
      responses:
        "201":
          description: Created event post comment.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostComment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/discussion:
    parameters:
      - $ref: "#/components/parameters/EventID"
    get:
      tags: [event_posts]
      operationId: listEventDiscussionMessages
      summary: Read the private meeting discussion as organizer or going participant.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Discussion threads only; ordinary publication comments are excluded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostThreadsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags:
        - event_posts
      operationId: ensureEventDiscussion
      summary: Ensure the meeting's default discussion exists.
      description: Only the organizer or a going participant may create or access the private discussion root.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Existing or newly created default discussion thread.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostThread"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/comments/thread:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - event_posts
      operationId: ensureEventPublicationComments
      summary: Ensure the meeting's public comment thread exists.
      description: Authenticated viewers can idempotently create the public comment root, also used for event reactions.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Existing or newly created public comment thread thread.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostThread"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/discussions:
    get:
      tags: [event_posts]
      operationId: listMyDiscussionInbox
      summary: List the viewer's meeting discussions by latest activity.
      description: Owned or going published/canceled meetings with at least one nondeleted discussion message. Publication comments and organizer posts are excluded. This read never marks messages as read.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - in: query
          name: cursor
          schema:
            type: string
      responses:
        "200":
          description: Discussions ordered by activity timestamp and event UUID descending.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussionInboxResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/discussion/mute:
    parameters:
      - $ref: "#/components/parameters/EventID"
    put:
      tags: [event_posts]
      operationId: setDiscussionMuted
      summary: Mute or unmute discussion pushes for the current organizer or going participant.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetDiscussionMutedRequest"
      responses:
        "204":
          description: Saved. Discussion messages and discussion mentions are suppressed while muted; old queued alerts are not replayed after unmute. Messages and unread counts remain.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/discussions/{event_id}:
    parameters:
      - $ref: "#/components/parameters/EventID"
    delete:
      tags: [event_posts]
      operationId: hideDiscussion
      summary: Hide a discussion only from the current member's inbox, preserving participation and history.
      description: Existing comments are marked read. Only new discussion messages restore the inbox entry. Does not change mute or other members.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Hidden from the viewer inbox.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /me/discussions/unread-counts:
    get:
      tags:
        - event_posts
      operationId: listMyDiscussionUnreadCounts
      summary: List unread incoming comment counts for owned and going meetings.
      description: Only nonzero counts are returned. Own and deleted comments are excluded. Read markers are persistent per account and independent of the notifications inbox. Events no longer owned or attended are omitted.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Current unread counts.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiscussionUnreadCountsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/discussion/read:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - event_posts
      operationId: markEventDiscussionRead
      summary: Mark only this conversation read through a known message.
      description: The boundary is server-owned and monotonic. Discussion reads require organizer/going membership; publication reads require view access and only affect the comment's publication. A boundary from the other conversation is rejected.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarkEventDiscussionReadRequest"
      responses:
        "204":
          description: Read boundary recorded.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/comments/read:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - event_posts
      operationId: markEventPublicationCommentsRead
      summary: Mark only this conversation read through a known message.
      description: The boundary is server-owned and monotonic. Discussion reads require organizer/going membership; publication reads require view access and only affect the comment's publication. A boundary from the other conversation is rejected.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarkEventDiscussionReadRequest"
      responses:
        "204":
          description: Read boundary recorded.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/discussion/comments:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - event_posts
      operationId: createEventDiscussionComment
      summary: Add a comment to the meeting's default discussion.
      description: Only the organizer or going participants may send. The private discussion root is created atomically on the first message.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventDiscussionCommentRequest"
      responses:
        "201":
          description: Default discussion thread containing the created comment.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostThread"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Client message ID conflicts with an existing payload, or voice_upload_missing means an expired unsent upload must be uploaded again with its original upload ID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/comments:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - event_posts
      operationId: createEventPublicationComment
      summary: Add a comment to the meeting's public comments.
      description: Authenticated viewers of the publication may comment. Comments never enter the private discussion.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventPublicationCommentRequest"
      responses:
        "201":
          description: Default discussion thread containing the created comment.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostThread"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/posts/{post_id}:
    parameters:
      - $ref: "#/components/parameters/EventID"
      - name: post_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    delete:
      tags:
        - event_posts
      operationId: deleteEventPost
      summary: Delete the current user's event publication.
      description: Only the publication author can delete it. Dependent comments, likes and reposts are removed with the publication.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Publication deleted; response body is empty.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/posts/{post_id}/comments/{comment_id}:
    parameters:
      - $ref: "#/components/parameters/EventID"
      - name: post_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: comment_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - event_posts
      operationId: updateEventPostComment
      summary: Update the current user's event post comment.
      description: Only the comment author can update an active comment on a published event they can view.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateEventPostCommentRequest"
      responses:
        "200":
          description: Updated event post comment.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostComment"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - event_posts
      operationId: deleteEventPostComment
      summary: Delete the current user's event post comment.
      description: Only the comment author can delete an active comment. The server keeps a tombstone so existing replies remain in the thread.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Comment replaced with a tombstone; response body is empty.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/posts/{post_id}/like:
    parameters:
      - $ref: "#/components/parameters/EventID"
      - name: post_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - event_posts
      operationId: likeEventPost
      summary: Like an event post or announcement.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Updated like state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostLikeState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - event_posts
      operationId: unlikeEventPost
      summary: Remove the current user's like from an event post or announcement.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Updated like state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostLikeState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/posts/{post_id}/repost:
    parameters:
      - $ref: "#/components/parameters/EventID"
      - name: post_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      tags:
        - event_posts
      operationId: repostEventPost
      summary: Repost an event post or announcement.
      description: Idempotently records the current user's repost without creating a duplicate post.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Updated repost state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostRepostState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - event_posts
      operationId: unrepostEventPost
      summary: Remove the current user's repost from an event post or announcement.
      description: Idempotently removes the repost relation while preserving the original post.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Updated repost state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventPostRepostState"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/reviews:
    parameters:
      - $ref: "#/components/parameters/EventID"
    get:
      tags:
        - reviews
      operationId: listEventReviews
      summary: List reviews visible to the current user.
      description: Public published event reviews are visible to authenticated users; private event reviews require event access.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Event reviews.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventReviewsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - reviews
      operationId: upsertEventReview
      summary: Create or update the current user's review for a completed event.
      description: Allowed only for authenticated non-organizer participants after the event has completed.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpsertEventReviewRequest"
      responses:
        "201":
          description: Review created or updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventReview"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /reports:
    post:
      tags:
        - moderation
      operationId: createReport
      summary: Report an event, user or review.
      description: Creates an open moderation report. Reporter identity comes from the current auth session; status and moderation action cannot be supplied by the reporter.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateReportRequest"
      responses:
        "201":
          description: Report created with open status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Report"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /admin/reports:
    get:
      tags:
        - moderation
      operationId: listAdminReports
      summary: List moderation reports for admin-lite.
      description: Requires current user to be present in the moderation admin allowlist.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/ReportStatus"
      responses:
        "200":
          description: Moderation reports.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReportsResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /admin/reports/{report_id}:
    parameters:
      - $ref: "#/components/parameters/ReportID"
    patch:
      tags:
        - moderation
      operationId: updateAdminReportStatus
      summary: Update a moderation report status and create an audit action.
      description: Requires current user to be present in the moderation admin allowlist. Every successful status update creates one moderation action row.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateReportStatusRequest"
      responses:
        "200":
          description: Report status updated with audit action.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UpdateReportStatusResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /users/{user_id}/block:
    parameters:
      - $ref: "#/components/parameters/UserID"
    get:
      tags:
        - blocks
      operationId: getUserBlockStatus
      summary: Read current user's block status for another user.
      description: Returns whether the authenticated user currently blocks the target user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Current block status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserBlockStatus"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - blocks
      operationId: blockUser
      summary: Block another user.
      description: Creates or keeps an existing user-to-user block. Self-block is invalid.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: User block created or already present.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserBlock"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - blocks
      operationId: unblockUser
      summary: Unblock another user.
      description: Removes a user-to-user block if present.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: User is unblocked or was not blocked.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/invitations:
    parameters:
      - $ref: "#/components/parameters/EventID"
    get:
      tags:
        - attendance
      operationId: listEventInvitations
      summary: List invitations created by the event organizer.
      description: Owner-only projection containing every invitation status and the invited user's contact summary.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Event invitations ordered from oldest to newest.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventInvitationsResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - attendance
      operationId: inviteEventUser
      summary: Explicitly invite a user to a published event.
      description: >-
        Organizers may invite to public and private events; going participants may invite only
        to public events. Pending access is insufficient. Self, organizer and blocked targets
        are forbidden. Ended events cannot receive new invitations. Pending invitations and
        going participation cannot be duplicated; declined or left users may be explicitly
        re-invited with a new notification generation. Sharing a chat card never calls this action.
        Use `invited_user_id` for a mutual Meets friend. `invited_phone` supports Yandex phone-only
        accounts, while `email` remains accepted for legacy/dev users. Do not combine targets.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventInvitationRequest"
      responses:
        "201":
          description: Invitation created or reset to pending.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventInvitation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Already pending or going (`invitation_already_active`), or the event deadline passed (`invitation_expired`). No new invitation or notification is created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/invitations/{invitation_id}:
    parameters:
      - $ref: "#/components/parameters/EventID"
      - $ref: "#/components/parameters/InvitationID"
    delete:
      tags:
        - attendance
      operationId: removeEventInvitation
      summary: Remove an invitation created by the event organizer.
      description: Owner-only and idempotent. An already accepted participant remains a participant.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Invitation removed or already absent.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/invite-links:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - attendance
      operationId: createEventInviteLink
      summary: Create or reproduce a phone-bound invite link for a private event.
      description: Owner-only. Repeated creation for the same event and normalized phone returns the same active URL. The URL is returned only by this operation and is never returned by list.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventInviteLinkRequest"
      responses:
        "201":
          description: Invite link created or the existing active link reproduced.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
            Referrer-Policy:
              schema:
                type: string
                const: no-referrer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventInviteLinkCreation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: The event is public. The stable error code is `invite_link_event_public`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
    get:
      tags:
        - attendance
      operationId: listEventInviteLinks
      summary: List invite-link metadata for the event organizer.
      description: Owner-only metadata projection. It never contains an invite URL, raw capability or full phone number.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Invite-link metadata ordered by creation time.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventInviteLinksResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/invite-links/{invite_link_id}:
    parameters:
      - $ref: "#/components/parameters/EventID"
      - $ref: "#/components/parameters/InviteLinkID"
    delete:
      tags:
        - attendance
      operationId: revokeEventInviteLink
      summary: Revoke a phone-bound invite link.
      description: Owner-only and idempotent. A revoked or accepted link cannot issue new access.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Invite link is revoked or was already revoked.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /event-invites/{token}:
    parameters:
      - $ref: "#/components/parameters/EventInviteToken"
    get:
      tags:
        - attendance
      operationId: resolveEventInviteLink
      summary: Resolve safe landing metadata for an invite capability.
      description: Public and no-store. It never exposes event ID, event title, phone or full private event details.
      security: []
      responses:
        "200":
          description: Safe invite landing metadata.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
            Referrer-Policy:
              schema:
                type: string
                const: no-referrer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventInviteLinkResolution"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Invite link was already accepted. The stable error code is `invite_link_accepted`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "410":
          description: Invite link expired. The stable error code is `invite_link_expired`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /event-invites/{token}/accept:
    parameters:
      - $ref: "#/components/parameters/EventInviteToken"
    post:
      tags:
        - attendance
      operationId: acceptEventInviteLink
      summary: Accept a phone-bound private event invite.
      description: Requires an authenticated user whose confirmed phone matches the target phone. The response is no-store.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Invite accepted idempotently for the matching user.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
            Referrer-Policy:
              schema:
                type: string
                const: no-referrer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventInviteLinkAcceptance"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Confirmed phone does not match. The stable error code is `invite_link_wrong_phone`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Event is public or the link is accepted by another user. Stable codes are `invite_link_event_public` and `invite_link_accepted`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "410":
          description: Invite link expired. The stable error code is `invite_link_expired`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/rsvp:
    parameters:
      - $ref: "#/components/parameters/EventID"
    patch:
      tags:
        - attendance
      operationId: respondEventInvitation
      summary: Respond to the current user's event invitation.
      description: An unanswered pending invitation remains pending and readable after the event ends (starts_at when ends_at is absent). Both answers then return 409 invitation_expired without side effects. Previously answered invitations retain their existing policy.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RSVPEventInvitationRequest"
      responses:
        "200":
          description: Updated invitation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventInvitation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Pending invitation response deadline passed; error is invitation_expired and no state is changed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/date-mode:
    parameters:
      - $ref: "#/components/parameters/EventID"
    patch:
      tags:
        - coordination
      operationId: setEventDateMode
      summary: Set event date mode to fixed or vote.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetEventDateModeRequest"
      responses:
        "204":
          description: Date mode updated.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/date-options:
    parameters:
      - $ref: "#/components/parameters/EventID"
    get:
      tags:
        - coordination
      operationId: listEventDateOptions
      summary: List date options visible to the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Event date options.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDateOptionsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - coordination
      operationId: createEventDateOption
      summary: Add a date option for an event owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventDateOptionRequest"
      responses:
        "201":
          description: Date option created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDateOption"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/date-options/{option_id}/votes:
    parameters:
      - $ref: "#/components/parameters/EventID"
      - $ref: "#/components/parameters/DateOptionID"
    post:
      tags:
        - coordination
      operationId: voteEventDateOption
      summary: Vote for one date option visible to the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Current user's date vote.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDateVote"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - coordination
      operationId: removeEventDateOptionVote
      summary: Remove the current user's vote from one date option.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Vote removed.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/date-options/{option_id}/select:
    parameters:
      - $ref: "#/components/parameters/EventID"
      - $ref: "#/components/parameters/DateOptionID"
    post:
      tags:
        - coordination
      operationId: selectEventDateOption
      summary: Select the final event date option as organizer.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Selected date option.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDateOption"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /chats:
    get:
      tags:
        - chats
      operationId: listDirectChats
      summary: List the current user's direct conversations.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          description: Opaque keyset cursor returned by the previous page.
          schema:
            type: string
      responses:
        "200":
          description: Direct conversations ordered by latest activity.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatThreadPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /chats/direct:
    post:
      tags:
        - chats
      operationId: createDirectChat
      summary: Create or return the canonical direct conversation with one user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateDirectChatRequest"
      responses:
        "200":
          description: Existing canonical direct conversation.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatThread"
        "201":
          description: Direct conversation created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatThread"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /chats/unread-count:
    get:
      tags:
        - chats
      operationId: getDirectChatUnreadCount
      summary: Get the current user's unread direct-message count.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Unread direct-message count.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatUnreadCountResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /chats/{chat_id}:
    parameters:
      - $ref: "#/components/parameters/ChatID"
    delete:
      tags:
        - chats
      operationId: hideDirectChat
      summary: Hide this conversation from the current member inbox.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Hidden for this member only; history is retained. A new message or explicitly opening the direct conversation restores it.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /chats/{chat_id}/inbox-preferences:
    parameters:
      - $ref: "#/components/parameters/ChatID"
    patch:
      tags: [chats]
      operationId: updateDirectChatInboxPreferences
      summary: Update the viewer's account-wide pin and personal unread reminder.
      description: Partial, idempotent update. Pin order uses pinned_at and is independent of message activity. Does not change read receipts or message unread counts.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChatInboxPreferencesPatch"
      responses:
        "200":
          description: Saved preferences for the authenticated viewer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatInboxPreferences"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/discussion/inbox-preferences:
    parameters:
      - $ref: "#/components/parameters/EventID"
    patch:
      tags: [event_posts]
      operationId: updateDiscussionInboxPreferences
      summary: Update the viewer's account-wide pin and personal unread reminder.
      description: Partial, idempotent update. Pin order uses pinned_at and is independent of message activity. Does not change read receipts or message unread counts.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChatInboxPreferencesPatch"
      responses:
        "200":
          description: Saved preferences for the authenticated viewer.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatInboxPreferences"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /chats/{chat_id}/mute:
    parameters:
      - $ref: "#/components/parameters/ChatID"
    put:
      tags:
        - chats
      operationId: setDirectChatMuted
      summary: Enable or disable direct message push notifications for the current member.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetDirectChatMutedRequest"
      responses:
        "204":
          description: Preference saved. Muting suppresses queued and future chat pushes until unmuted, without changing messages or unread counts.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /chats/{chat_id}/messages:
    parameters:
      - $ref: "#/components/parameters/ChatID"
    get:
      tags:
        - chats
      operationId: listDirectChatMessages
      summary: List a direct conversation's messages chronologically.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          description: Opaque keyset cursor for the next older message page.
          schema:
            type: string
      responses:
        "200":
          description: Chronological direct-message page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatMessagePage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - chats
      operationId: sendDirectChatMessage
      summary: Idempotently send text or an event card to a direct conversation.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendDirectChatMessageRequest"
      responses:
        "200":
          description: Exact idempotent resend returned the existing message.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatMessage"
        "201":
          description: Direct message created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatMessage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"
  /chats/{chat_id}/messages/{message_id}:
    parameters:
      - $ref: "#/components/parameters/ChatID"
      - $ref: "#/components/parameters/ChatMessageID"
    delete:
      tags:
        - chats
      operationId: deleteDirectChatMessage
      summary: Delete the current sender's text direct message, retaining a tombstone for conversation history.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Deleted direct message tombstone.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatMessage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /chats/{chat_id}/messages/{message_id}/reactions/{emoji}:
    parameters:
      - $ref: "#/components/parameters/ChatID"
      - $ref: "#/components/parameters/ChatMessageID"
      - $ref: "#/components/parameters/ChatReactionEmoji"
    put:
      tags:
        - chats
      operationId: addDirectChatMessageReaction
      summary: Add one of the supported reactions to a direct message.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Direct message with current reaction aggregates.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatMessage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - chats
      operationId: removeDirectChatMessageReaction
      summary: Remove the current user's reaction from a direct message.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Direct message with current reaction aggregates.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ChatMessage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /chats/{chat_id}/read:
    parameters:
      - $ref: "#/components/parameters/ChatID"
    post:
      tags:
        - chats
      operationId: markDirectChatRead
      summary: Advance the current user's read position through one message.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarkDirectChatReadRequest"
      responses:
        "204":
          description: Read position advanced or already at a newer message.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/messages:
    parameters:
      - $ref: "#/components/parameters/EventID"
    get:
      tags:
        - coordination
      operationId: listEventChatMessages
      summary: List chat messages visible to the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: before
          in: query
          description: Return messages created before this timestamp.
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Event chat messages.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventChatMessagesResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - coordination
      operationId: sendEventChatMessage
      summary: Send a chat message to an event visible to the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SendEventChatMessageRequest"
      responses:
        "201":
          description: Chat message sent.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventChatMessage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/ws:
    parameters:
      - $ref: "#/components/parameters/EventID"
    get:
      tags:
        - coordination
      operationId: openEventChatWebSocket
      summary: Authenticate event chat WebSocket access.
      description: M5 placeholder endpoint authenticates and checks event chat access, but realtime WebSocket broadcast is not implemented yet.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Chat WebSocket access is authorized; realtime WebSocket transport is not implemented yet.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventChatWebSocketAuthResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/reminders:
    parameters:
      - $ref: "#/components/parameters/EventID"
    get:
      tags:
        - coordination
      operationId: getEventReminderSettings
      summary: Get reminder settings visible to the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Event reminder settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventReminderSettings"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    patch:
      tags:
        - coordination
      operationId: setEventReminderSettings
      summary: Update reminder settings as event organizer.
      description: Stores reminder settings only; external email, SMS or push delivery is outside M5.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetEventReminderSettingsRequest"
      responses:
        "200":
          description: Updated event reminder settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventReminderSettings"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks:
    get:
      tags: [calendar]
      operationId: listPrivateTasks
      summary: Page through all active private tasks visible to the current viewer.
      description: >-
        Includes owned, accepted, and pending tasks, with or without a date, including
        completed tasks. Returns at most 100 items in stable UUID order. Follow
        next_cursor using after until null; every page rechecks viewer membership.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: after
          in: query
          required: false
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Viewer-scoped tasks page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrivateTasksPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - calendar
      operationId: createPrivateTask
      summary: Create a private task for the current user.
      description: >-
        A private task is visible only to its accepted participants in their calendars.
        It is never published to feed, discovery, profiles, or event listings. A new task
        may invite at most one other user, so its maximum active membership is two people.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePrivateTaskRequest"
      responses:
        "201":
          description: Private task created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrivateTask"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Blocked invite target (invitation_conflict), participant_limit_reached, or invited task already past its schedule deadline (invitation_expired). No task is created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/unscheduled:
    get:
      tags: [calendar]
      operationId: listUnscheduledPrivateTasks
      summary: List active private tasks without a date for the current viewer.
      description: Returns owned, accepted, and pending tasks for the fixed unscheduled agenda section.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Viewer-scoped unscheduled tasks.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UnscheduledPrivateTasksResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/subtasks:
    get:
      tags: [calendar]
      operationId: listPrivateTaskSubtasks
      summary: Read visible child collections in stable parent UUID pages of 100.
      description: Children inherit their active parent's pending/accepted membership. Old task/calendar lists remain unchanged. An empty collection is retained with its revision.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: after
          in: query
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Child collections; follow next_cursor until null.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSubtaskPage"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/{task_id}/subtasks:
    parameters:
      - $ref: "#/components/parameters/TaskID"
    get:
      tags: [calendar]
      operationId: readPrivateTaskSubtasks
      summary: Read one parent's child collection; revision zero means no children have been saved.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Authorized child collection.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSubtaskCollection"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    put:
      tags: [calendar]
      operationId: updatePrivateTaskSubtasks
      summary: Atomically save the child collection against its known revision.
      description: >-
        Only the owner changes content/order/membership of this collection. Accepted
        participants may change only completed flags. Pending viewers cannot write.
        The parent must be open without a pending completion review. A repeated last
        operation_id returns the current collection without another write. Stale revision
        returns subtasks_conflict. Completing the parent applies all child completions
        in the same transaction after any required parent confirmation. Reopening retains
        completed children. Children never create separate invitations or confirmations.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTaskSubtasksRequest"
      responses:
        "200":
          description: Saved collection with canonical revision.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskSubtaskCollection"
        "409":
          description: subtasks_conflict or completion_request_pending; client must reconcile rather than overwrite.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/order:
    put:
      tags: [calendar]
      operationId: setPrivateTaskOrder
      summary: Replace the current viewer's personal order for accepted private tasks.
      description: >-
        Stores a relative order for the supplied active, accepted tasks. The client sends
        one visible day and completion section at a time. Pending invitations and another
        participant's order are never changed. The operation is atomic; an unavailable task
        makes the whole request return 404 without saving a partial order.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetPrivateTaskOrderRequest"
      responses:
        "204":
          description: Personal task order saved.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/{task_id}:
    parameters:
      - $ref: "#/components/parameters/TaskID"
    get:
      tags: [calendar]
      operationId: getPrivateTask
      summary: Read an active private task as its owner or pending/accepted invitee.
      description: Uninvited, declined and removed users receive 404 without task metadata.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Task and this viewer's individual participation state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrivateTaskDetails"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    patch:
      tags: [calendar]
      operationId: updatePrivateTask
      summary: Update an active private task as its owner.
      description: Replaces the task title, description and schedule. Existing participants and their personal state are preserved.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdatePrivateTaskRequest"
      responses:
        "200":
          description: Updated task.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrivateTask"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags: [calendar]
      operationId: cancelPrivateTask
      summary: Cancel a private task as its owner.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "204":
          description: Task canceled.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/{task_id}/invitations:
    parameters:
      - $ref: "#/components/parameters/TaskID"
    post:
      tags: [calendar]
      operationId: invitePrivateTaskParticipant
      summary: Invite a user to an active task as its owner.
      description: >-
        Creates pending participation and an invitation notification. Repeating pending or accepted
        invitations is idempotent and never resets completion. A new explicit invitation may reset
        declined or removed participation to pending with a new invited_at/notification generation;
        personal checklist and completion data are preserved. Self and blocked targets are rejected.
        Tasks allow up to 20 pending or accepted non-owner participants. Declined and removed
        participants do not occupy a slot; retries for active members remain idempotent. At/after starts_at new invitations return 409 invitation_expired.
        Non-owners and canceled tasks return 404.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvitePrivateTaskParticipantRequest"
      responses:
        "204":
          description: Invitation saved, or participation already exists.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Self/blocked invitation conflict, participant_limit_reached, or invitation_expired when starts_at has passed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/{task_id}/description:
    parameters:
      - $ref: "#/components/parameters/TaskID"
    patch:
      tags: [calendar]
      operationId: updatePrivateTaskDescription
      summary: Replace or clear an active task description as its owner.
      description: >-
        Previous plain and rich content must match the last read snapshot; otherwise returns 409.
        Personal checks on retained checklist block IDs are preserved for every participant.
        Removed or converted blocks lose their checks. Invitation and completion state are unchanged.
        Non-owners and canceled tasks return 404. Null description_rich clears the description.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdatePrivateTaskDescriptionRequest"
      responses:
        "204":
          description: Description saved.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Description changed since it was read; reload before editing again.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/{task_id}/invitation:
    parameters:
      - $ref: "#/components/parameters/TaskID"
    patch:
      tags:
        - calendar
      operationId: respondPrivateTaskInvitation
      summary: Accept or decline the caller's private task invitation.
      description: Answers are allowed only before starts_at. After that time the invitation stays pending and readable, but both answers return 409 invitation_expired without changing participation or personal completion.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RespondPrivateTaskInvitationRequest"
      responses:
        "204":
          description: Invitation response saved.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Pending invitation response deadline passed; error is invitation_expired and no state is changed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/{task_id}/completion:
    parameters:
      - $ref: "#/components/parameters/TaskID"
    patch:
      tags:
        - calendar
      operationId: setPrivateTaskCompletion
      summary: Request a shared completion-state change or apply a personal one immediately.
      description: >-
        Personal tasks change immediately. Shared tasks keep their current state and return a
        pending request until every accepted participant approves. One rejection preserves the
        previous state. Only one request may be pending for a task.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetPrivateTaskCompletionRequest"
      responses:
        "200":
          description: Applied personal change or persistent shared review request.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrivateTaskCompletionMutation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Another completion request is already pending.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/{task_id}/completion-requests/{request_id}:
    parameters:
      - $ref: "#/components/parameters/TaskID"
      - name: request_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags: [calendar]
      operationId: respondPrivateTaskCompletionRequest
      summary: Confirm or decline a pending shared completion-state change.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RespondPrivateTaskCompletionRequest"
      responses:
        "200":
          description: Response saved; the mutation says whether consensus applied the state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrivateTaskCompletionMutation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: The request is already resolved or the viewer already answered.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags: [calendar]
      operationId: cancelPrivateTaskCompletionRequest
      summary: Cancel the caller's own pending completion-state request.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Request canceled and the previous task state retained.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrivateTaskCompletionMutation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: The request was already accepted or declined.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /tasks/{task_id}/checklist/{block_id}:
    parameters:
      - $ref: "#/components/parameters/TaskID"
      - name: block_id
        in: path
        required: true
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - calendar
      operationId: setPrivateTaskChecklistItem
      summary: Set the caller's personal checklist mark.
      description: >-
        Requires an active task, an accepted participant (including the owner), and
        an existing checklist block. Pending, declined, removed, unknown and canceled
        targets return not_found. Idempotent; never changes task completion or another
        participant's marks. The caller cannot supply a user_id.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetPrivateTaskChecklistItemRequest"
      responses:
        "204":
          description: Personal checklist mark saved.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /assistant/messages:
    post:
      tags:
        - assistant
      operationId: sendAssistantMessage
      summary: Ask about the authenticated viewer's schedule.
      description: >-
        Experimental, disabled unless ASSISTANT_ENABLED is true. This endpoint never mutates schedule data.
        Clients opting in with Assistant-Actions-Version 1 may receive a durable proposal
        for one owned event/task creation or edit. Version 2 additionally enables task deletion/completion,
        event cancellation and sending/answering invitations. Each proposal is one action; only explicit confirm applies it.
        Without the header, behavior remains read-only.
        Server owns identity, history and tools. Reads entries starting within up to 31
        local calendar days, at most 40 sources. Undated tasks and events starting before
        that range are excluded; this is not a complete availability check.
        In-process memory retains the last two turns per viewer/conversation until 30
        minutes of inactivity, checked on the next request. Same request_id and payload
        replays a retained result; changed payload returns 409. Process restarts clear
        memory and limits. Requests are limited to 2000 UTF-8 bytes of text and 8 KiB body.
        At most four outbound model calls per turn including bounded retries, 95 seconds total; no streaming in this slice.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - in: header
          name: Assistant-Actions-Version
          required: false
          description: Opt in to server-owned proposals; omitted means read-only.
          schema:
            type: string
            enum: ['1', '2']
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AssistantRequest'
      responses:
        '200':
          description: Answer with server-owned calendar source cards. Cache-Control no-store.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          description: Viewer/global limit or busy assistant; Retry-After in seconds.
          headers:
            Retry-After:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Assistant disabled, provider unavailable or schedule read failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '504':
          description: Assistant deadline exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /assistant/proposals/{proposal_id}/confirm:
    post:
      tags: [assistant]
      operationId: confirmAssistantProposal
      summary: Confirm a server-owned proposal belonging to the authenticated viewer.
      description: >-
        No model call. Only the stored proposal may be applied. Empty JSON object required.
        Proposal ID is the durable idempotency identity. Applied results replay, including
        after expiry; canceling an applied proposal returns its applied result.
        Pending proposals expire after 15 minutes. Domain data, calendar projections,
        notification outbox and saved result commit atomically. Stale edited records
        return 409 and require a fresh proposal. Cache-Control no-store.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: proposal_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResolveAssistantProposalRequest'
      responses:
        '200':
          description: Durable applied or canceled result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantProposalResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          description: Proposal expired; prepare a new one.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Assistant unavailable or mutation could not be completed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /assistant/proposals/{proposal_id}/cancel:
    post:
      tags: [assistant]
      operationId: cancelAssistantProposal
      summary: Cancel a server-owned proposal belonging to the authenticated viewer.
      description: >-
        No model call. Only the stored proposal may be applied. Empty JSON object required.
        Proposal ID is the durable idempotency identity. Applied results replay, including
        after expiry; canceling an applied proposal returns its applied result.
        Pending proposals expire after 15 minutes. Domain data, calendar projections,
        notification outbox and saved result commit atomically. Stale edited records
        return 409 and require a fresh proposal. Cache-Control no-store.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: proposal_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResolveAssistantProposalRequest'
      responses:
        '200':
          description: Durable applied or canceled result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AssistantProposalResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          description: Proposal expired; prepare a new one.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Assistant unavailable or mutation could not be completed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /calendar/entries:
    get:
      tags:
        - calendar
      operationId: listCalendarEntries
      summary: List the current user's calendar entries.
      description: >-
        Native clients authenticate with `Authorization: Bearer <access_token>`;
        browser clients may use the refresh-token cookie.
        Accepted private tasks completed after their scheduled time are also returned when
        the viewer's completion timestamp falls in the requested range. Their original
        starts_at is unchanged; calendar clients group late-completed tasks by the local
        completion day using viewer_completed_at.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: cursor
          in: query
          description: Opaque next_cursor from the previous page. Keep all other filters unchanged; entries are ordered by starts_at then id.
          schema:
            type: string
            maxLength: 160
        - name: event_id
          in: query
          schema:
            type: string
            format: uuid
        - name: starts_from
          in: query
          schema:
            type: string
            format: date-time
        - name: starts_to
          in: query
          schema:
            type: string
            format: date-time
        - name: include_overdue_tasks
          in: query
          description: Include the current user's incomplete private tasks scheduled before `starts_from`.
          schema:
            type: boolean
            default: false
        - name: include_pending_invitations
          in: query
          description: Include the current user's pending event and task invitations scheduled within the range. These read-only rows do not grant accepted participation and use existing event RSVP or task invitation response endpoints.
          schema:
            type: boolean
            default: false
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/CalendarEntryStatus"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: Calendar entries.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarEntriesResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - calendar
      operationId: createCalendarEntry
      summary: Create a manual calendar entry for the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCalendarEntryRequest"
      responses:
        "201":
          description: Calendar entry created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarEntry"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /calendar/entries/{entry_id}:
    parameters:
      - $ref: "#/components/parameters/CalendarEntryID"
    get:
      tags:
        - calendar
      operationId: getCalendarEntry
      summary: Get a calendar entry owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Calendar entry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarEntry"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    patch:
      tags:
        - calendar
      operationId: updateCalendarEntry
      summary: Update a calendar entry owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateCalendarEntryRequest"
      responses:
        "200":
          description: Calendar entry updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarEntry"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - calendar
      operationId: cancelCalendarEntry
      summary: Cancel a calendar entry owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Calendar entry canceled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarEntry"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /calendar/entries/{entry_id}/invites:
    parameters:
      - $ref: "#/components/parameters/CalendarEntryID"
    post:
      tags:
        - calendar
      operationId: createCalendarInvite
      summary: Invite a user to a calendar entry owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCalendarInviteRequest"
      responses:
        "201":
          description: Calendar invite created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarInvite"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /calendar/invites:
    get:
      tags:
        - calendar
      operationId: listCalendarInvites
      summary: List calendar invites visible to the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - in: query
          name: role
          schema:
            type: string
            enum:
              - all
              - incoming
              - outgoing
            default: all
        - in: query
          name: status
          schema:
            $ref: "#/components/schemas/CalendarInviteStatus"
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        "200":
          description: Calendar invites.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarInvitesResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /calendar/invites/{invite_id}:
    parameters:
      - $ref: "#/components/parameters/CalendarInviteID"
    get:
      tags:
        - calendar
      operationId: getCalendarInvite
      summary: Get a calendar invite visible to the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Calendar invite with entry, inviter and visible participant details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarInviteDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    patch:
      tags:
        - calendar
      operationId: respondCalendarInvite
      summary: Respond to a calendar invite as the invited user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RespondCalendarInviteRequest"
      responses:
        "200":
          description: Calendar invite response saved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarInvite"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /calendar/entries/{entry_id}/reminders:
    parameters:
      - $ref: "#/components/parameters/CalendarEntryID"
    get:
      tags:
        - calendar
      operationId: getCalendarReminderSettings
      summary: Get calendar reminder settings visible to the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Calendar reminder settings.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarReminderSettings"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    patch:
      tags:
        - calendar
      operationId: setCalendarReminderSettings
      summary: Update reminder settings for a calendar entry owned by the current user.
      description: Stores reminder settings only; external email, SMS or push delivery is outside M8.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetCalendarReminderSettingsRequest"
      responses:
        "200":
          description: Calendar reminder settings updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarReminderSettings"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /events/{event_id}/calendar-entry:
    parameters:
      - $ref: "#/components/parameters/EventID"
    post:
      tags:
        - calendar
      operationId: linkEventAttendanceToCalendar
      summary: Create or refresh the current user's event-linked calendar entry.
      description: The backend links only published events with start time where the current user is the organizer or a going participant.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Event-linked calendar entry created or refreshed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarEntry"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
    delete:
      tags:
        - calendar
      operationId: cancelEventAttendanceCalendarEntry
      summary: Cancel the current user's event-linked calendar entry after leaving.
      description: The backend cancels the linked calendar entry only when the current user no longer has a going participant record for the event.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Event-linked calendar entry canceled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarEntry"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /feed:
    get:
      tags:
        - feed
      operationId: listFeed
      summary: List feed events.
      description: Returns feed events. `scope=all` ranks bounded chronological candidate windows using engagement, freshness, event timing and optional viewer signals. Opaque continuation cursors retain unreturned candidates and a creation-time/ID boundary. New publications are included on refresh. Hidden/not-interesting announcement preferences are excluded on every public feed page, including pending candidates. `scope=going`, `scope=invited`, `scope=friends`, and `scope=following` are personal and require the current user.
      security:
        - {}
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: scope
          in: query
          description: Feed filter scope. `friends` uses mutual follows, `following` uses profiles followed by the current user, `going` includes public and incognito attendance, and `invited` includes pending invitations in newest-first order.
          schema:
            type: string
            default: all
            enum:
              - all
              - friends
              - groups
              - following
              - going
              - invited
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          description: Copy next_cursor unchanged. Public scopes return an opaque f1 token bound to viewer and scope; legacy RFC3339 timestamp cursors remain accepted. Invited retains timestamp cursors.
          schema:
            type: string
            maxLength: 8192
      responses:
        "200":
          description: Feed items.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /feed/preferences:
    get:
      tags:
        - feed
      operationId: listFeedPreferences
      summary: List current user's persisted feed preferences.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: item_key
          in: query
          description: Feed item key such as `announcement:{event_id}` or `post:{post_id}`. May be repeated. Omit to return up to 300 most recently updated preferences for the current user. Exact item_key lookups are not limited by that history window; feed exclusion checks all hidden announcement preferences.
          schema:
            type: array
            items:
              type: string
          style: form
          explode: true
      responses:
        "200":
          description: Feed preferences.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedPreferencesResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
    post:
      tags:
        - feed
      operationId: updateFeedPreference
      summary: Persist one feed preference for the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateFeedPreferenceRequest"
      responses:
        "200":
          description: Updated feed preference.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeedPreference"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /notifications:
    get:
      tags:
        - notifications
      operationId: listNotifications
      summary: List persistent notifications for the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: unread
          in: query
          schema:
            type: boolean
            default: false
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 40
        - name: order
          in: query
          schema:
            type: string
            enum:
              - unread_first
              - recent
            default: unread_first
        - name: cursor
          in: query
          description: Opaque keyset cursor returned by the previous page.
          schema:
            type: string
      responses:
        "200":
          description: Notification list and unread count.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationsResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /notifications/device-token:
    put:
      tags:
        - notifications
      operationId: registerNotificationDeviceToken
      summary: Register the current iOS device for APNs delivery.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterNotificationDeviceTokenRequest"
      responses:
        "204":
          description: Device token registered for the current session.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /notifications/unread-count:
    get:
      tags:
        - notifications
      operationId: getNotificationUnreadCount
      summary: Get the current user's unread notification count.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Unread notification count.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationUnreadCountResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /notifications/read-all:
    post:
      tags:
        - notifications
      operationId: markAllNotificationsRead
      summary: Mark all current user's notifications as read.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Number of notifications marked read.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MarkAllNotificationsReadResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
  /notifications/{notification_id}/read:
    post:
      tags:
        - notifications
      operationId: markNotificationRead
      summary: Mark one notification as read.
      security:
        - bearerAuth: []
        - cookieAuth: []
      parameters:
        - name: notification_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Updated notification.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Notification"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /offline/events:
    description: >-
      Durable offline replay of the canonical mutation. A successful response, domain
      changes and notification inbox/delivery rows commit atomically. Retrying the same
      user-scoped key and exact body returns the original response, even after the resource
      changes. A different body with that key returns 409 idempotency_conflict.
      No query parameters are accepted. Do not fall back to the legacy POST on failure.
    parameters:
      - $ref: "#/components/parameters/OfflineOperationID"
    post:
      tags:
        - events
      operationId: offlineCreateEvent
      summary: Create a draft event for the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventRequest"
      responses:
        "201":
          description: Draft event created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDetails"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"
        "409":
          description: The operation key was already committed with different content (idempotency_conflict).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Temporary synchronization failure. Retry the identical body with the same key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /offline/tasks:
    description: >-
      Durable offline replay of the canonical mutation. A successful response, domain
      changes and notification inbox/delivery rows commit atomically. Retrying the same
      user-scoped key and exact body returns the original response, even after the resource
      changes. A different body with that key returns 409 idempotency_conflict.
      No query parameters are accepted. Do not fall back to the legacy POST on failure.
    parameters:
      - $ref: "#/components/parameters/OfflineOperationID"
    post:
      tags:
        - calendar
      operationId: offlineCreatePrivateTask
      summary: Create a private task for the current user.
      description: >-
        A private task is visible only to its accepted participants in their calendars.
        It is never published to feed, discovery, profiles, or event listings. A new task
        may invite at most one other user, so its maximum active membership is two people.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePrivateTaskRequest"
      responses:
        "201":
          description: Private task created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PrivateTask"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Blocked invite target (invitation_conflict), participant_limit_reached, or invited task already past its schedule deadline (invitation_expired). No task is created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          description: Temporary synchronization failure. Retry the identical body with the same key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /offline/events/{event_id}:
    description: >-
      Durable offline replay of the canonical mutation. A successful response, domain
      changes and notification inbox/delivery rows commit atomically. Retrying the same
      user-scoped key and exact body returns the original response, even after the resource
      changes. A different body with that key returns 409 idempotency_conflict.
      No query parameters are accepted. Do not fall back to the legacy POST on failure.
    parameters:
      - $ref: "#/components/parameters/EventID"
      - $ref: "#/components/parameters/OfflineOperationID"
    patch:
      tags:
        - events
      operationId: offlineUpdateEvent
      summary: Update an event owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateEventRequest"
      responses:
        "200":
          description: Updated event details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDetails"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
        "409":
          description: The operation key was already committed with different content (idempotency_conflict).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Temporary synchronization failure. Retry the identical body with the same key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /offline/events/{event_id}/publish:
    description: >-
      Durable offline replay of the canonical mutation. A successful response, domain
      changes and notification inbox/delivery rows commit atomically. Retrying the same
      user-scoped key and exact body returns the original response, even after the resource
      changes. A different body with that key returns 409 idempotency_conflict.
      No query parameters are accepted. Do not fall back to the legacy POST on failure.
    parameters:
      - $ref: "#/components/parameters/EventID"
      - $ref: "#/components/parameters/OfflineOperationID"
    post:
      tags:
        - events
      operationId: offlinePublishEvent
      summary: Publish a draft event owned by the current user.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Published event details.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Paid event organizer payment profile is not verified.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
        "409":
          description: The operation key was already committed with different content (idempotency_conflict).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Temporary synchronization failure. Retry the identical body with the same key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /offline/events/{event_id}/calendar-entry:
    description: >-
      Durable offline replay of the canonical mutation. A successful response, domain
      changes and notification inbox/delivery rows commit atomically. Retrying the same
      user-scoped key and exact body returns the original response, even after the resource
      changes. A different body with that key returns 409 idempotency_conflict.
      No query parameters are accepted. Do not fall back to the legacy POST on failure.
    parameters:
      - $ref: "#/components/parameters/EventID"
      - $ref: "#/components/parameters/OfflineOperationID"
    post:
      tags:
        - calendar
      operationId: offlineLinkEventAttendanceToCalendar
      summary: Create or refresh the current user's event-linked calendar entry.
      description: The backend links only published events with start time where the current user is the organizer or a going participant.
      security:
        - bearerAuth: []
        - cookieAuth: []
      responses:
        "200":
          description: Event-linked calendar entry created or refreshed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalendarEntry"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
        "409":
          description: The operation key was already committed with different content (idempotency_conflict).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          description: Temporary synchronization failure. Retry the identical body with the same key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /offline/events/{event_id}/invitations:
    description: >-
      Durable offline replay of the canonical mutation. A successful response, domain
      changes and notification inbox/delivery rows commit atomically. Retrying the same
      user-scoped key and exact body returns the original response, even after the resource
      changes. A different body with that key returns 409 idempotency_conflict.
      No query parameters are accepted. Do not fall back to the legacy POST on failure.
    parameters:
      - $ref: "#/components/parameters/EventID"
      - $ref: "#/components/parameters/OfflineOperationID"
    post:
      tags:
        - attendance
      operationId: offlineInviteEventUser
      summary: Explicitly invite a user to a published event.
      description: >-
        Organizers may invite to public and private events; going participants may invite only
        to public events. Pending access is insufficient. Self, organizer and blocked targets
        are forbidden. Ended events cannot receive new invitations. Pending invitations and
        going participation cannot be duplicated; declined or left users may be explicitly
        re-invited with a new notification generation. Sharing a chat card never calls this action.
        Use `invited_user_id` for a mutual Meets friend. `invited_phone` supports Yandex phone-only
        accounts, while `email` remains accepted for legacy/dev users. Do not combine targets.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEventInvitationRequest"
      responses:
        "201":
          description: Invitation created or reset to pending.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EventInvitation"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Already pending or going (`invitation_already_active`), or the event deadline passed (`invitation_expired`). No new invitation or notification is created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          description: Temporary synchronization failure. Retry the identical body with the same key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /offline/tasks/{task_id}/invitations:
    description: >-
      Durable offline replay of the canonical mutation. A successful response, domain
      changes and notification inbox/delivery rows commit atomically. Retrying the same
      user-scoped key and exact body returns the original response, even after the resource
      changes. A different body with that key returns 409 idempotency_conflict.
      No query parameters are accepted. Do not fall back to the legacy POST on failure.
    parameters:
      - $ref: "#/components/parameters/TaskID"
      - $ref: "#/components/parameters/OfflineOperationID"
    post:
      tags: [calendar]
      operationId: offlineInvitePrivateTaskParticipant
      summary: Invite a user to an active task as its owner.
      description: >-
        Creates pending participation and an invitation notification. Repeating pending or accepted
        invitations is idempotent and never resets completion. A new explicit invitation may reset
        declined or removed participation to pending with a new invited_at/notification generation;
        personal checklist and completion data are preserved. Self and blocked targets are rejected.
        Tasks allow up to 20 pending or accepted non-owner participants. Declined and removed
        participants do not occupy a slot; retries for active members remain idempotent. At/after starts_at new invitations return 409 invitation_expired.
        Non-owners and canceled tasks return 404.
      security:
        - bearerAuth: []
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvitePrivateTaskParticipantRequest"
      responses:
        "204":
          description: Invitation saved, or participation already exists.
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Self/blocked invitation conflict, participant_limit_reached, or invitation_expired when starts_at has passed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          description: Temporary synchronization failure. Retry the identical body with the same key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
components:
  parameters:
    RefreshOperationID:
      name: X-Refresh-Operation-ID
      in: header
      required: false
      description: >-
        Random UUID stored durably with the original refresh token before the
        first refresh request. Reuse the pair for retries and pending-operation
        logout; use a new UUID for the next rotation. Exactly one nonzero UUID in
        canonical hyphenated form is accepted; hexadecimal case is immaterial.
        Empty, repeated, combined or malformed values return 400
        invalid_refresh_operation. Omitting the header retains legacy behavior.
      schema:
        type: string
        format: uuid
        minLength: 36
        maxLength: 36
        pattern: '^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$'
        not:
          const: "00000000-0000-0000-0000-000000000000"
    OfflineOperationID:
      name: Idempotency-Key
      in: header
      required: true
      description: Stable UUID stored on the client before sending; reuse for every retry of the exact operation.
      schema:
        type: string
        format: uuid
    EventID:
      name: event_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ChatID:
      name: chat_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ChatMessageID:
      name: message_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ChatReactionEmoji:
      name: emoji
      in: path
      required: true
      schema:
        type: string
        enum:
          - "❤️"
          - "👍"
          - "😂"
          - "🔥"
    InvitationID:
      name: invitation_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    InviteLinkID:
      name: invite_link_id
      in: path
      required: true
      schema:
        type: string
        minLength: 22
        maxLength: 22
        pattern: "^[A-Za-z0-9_-]{22}$"
    EventInviteToken:
      name: token
      in: path
      required: true
      description: Bounded opaque capability. It must not be logged, cached or sent to analytics.
      schema:
        type: string
        minLength: 66
        maxLength: 66
        pattern: "^[A-Za-z0-9_-]{22}\\.[A-Za-z0-9_-]{43}$"
    UserID:
      name: user_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ReportID:
      name: report_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    DateOptionID:
      name: option_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    CalendarEntryID:
      name: entry_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    TaskID:
      name: task_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    CalendarInviteID:
      name: invite_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    PaymentIntentID:
      name: payment_intent_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    PaymentProvider:
      name: provider
      in: path
      required: true
      schema:
        type: string
  headers:
    RequestID:
      description: Correlation ID supplied by the client or generated by the backend.
      schema:
        type: string
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque
      description: Short-lived opaque access token returned by sign-in and refresh endpoints.
    cookieAuth:
      type: apiKey
      in: cookie
      name: vstrechi_refresh_token
  responses:
    RefreshOperationBadRequest:
      description: Invalid operation header (invalid_refresh_operation), or invalid JSON/request shape (invalid_request). No rotation or revocation is performed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            operation:
              value:
                error: invalid_refresh_operation
            request:
              value:
                error: invalid_request
    RefreshReplayExpired:
      description: The matching operation's recovery window expired while its child remains active. Terminal for this operation; do not retry it or replace its UUID to reuse the old token. The client must authenticate again.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: refresh_replay_expired
    RefreshReplayUnavailable:
      description: The replay-enabled refresh or logout could not be completed or verified. Preserve the original refresh token and operation UUID and retry the same operation with bounded backoff. Do not treat this as proof of session revocation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: refresh_replay_unavailable
    PhoneStartBadRequest:
      description: Invalid JSON/request shape or phone number. The error code is invalid_request or phone_invalid.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestID"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    PhoneChallengeInvalid:
      description: The phone call challenge does not exist or cannot be used.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestID"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: phone_challenge_invalid
    PhoneChallengeExpired:
      description: The phone call challenge expired before verification.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestID"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: phone_challenge_expired
    PhoneRateLimited:
      description: Too many phone authentication attempts.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestID"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: phone_rate_limited
    PhoneProviderUnavailable:
      description: SMS.RU Call Auth is temporarily unavailable or disabled.
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestID"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: phone_provider_unavailable
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unauthorized:
      description: Missing or invalid authentication.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: Action is not allowed for the current user or event state.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Conflict:
      description: The requested change conflicts with an existing resource or an account limit.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    RateLimited:
      description: Too many authentication attempts.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ServiceUnavailable:
      description: Service is temporarily unavailable or disabled.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    InternalError:
      description: Internal server error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    TaskSubtask:
      type: object
      additionalProperties: false
      required:
        - id
        - title
        - schedule_kind
        - timezone
        - completed
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type:
            - string
            - "null"
          maxLength: 2000
        starts_at:
          type:
            - string
            - "null"
          format: date-time
        schedule_kind:
          type: string
          enum:
            - none
            - date
            - date_time
        timezone:
          type: string
          minLength: 1
          maxLength: 100
        completed:
          type: boolean
    TaskSubtaskCollection:
      type: object
      additionalProperties: false
      required:
        - task_id
        - revision
        - items
      properties:
        task_id:
          type: string
          format: uuid
        revision:
          type: integer
          format: int64
          minimum: 0
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TaskSubtask"
    TaskSubtaskPage:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TaskSubtaskCollection"
        next_cursor:
          type:
            - string
            - "null"
          format: uuid
    UpdateTaskSubtasksRequest:
      type: object
      additionalProperties: false
      required:
        - expected_revision
        - operation_id
        - items
      properties:
        expected_revision:
          type: integer
          format: int64
          minimum: 0
        operation_id:
          type: string
          format: uuid
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/TaskSubtask"
    AssistantRequest:
      type: object
      additionalProperties: false
      required:
        - conversation_id
        - request_id
        - text
        - timezone
      properties:
        conversation_id:
          type: string
          format: uuid
        request_id:
          type: string
          format: uuid
        text:
          type: string
          minLength: 1
          maxLength: 2000
        timezone:
          type: string
          minLength: 1
          maxLength: 100
          example: Asia/Almaty
    AssistantResponse:
      type: object
      additionalProperties: false
      required:
        - request_id
        - text
        - sources
      properties:
        request_id:
          type: string
          format: uuid
        text:
          type: string
        sources:
          type: array
          maxItems: 40
          items:
            $ref: '#/components/schemas/AssistantSource'
        from_date:
          type: string
          format: date
        to_date:
          type: string
          format: date
        notice:
          type: string
        proposal:
          $ref: '#/components/schemas/AssistantProposal'
    ResolveAssistantProposalRequest:
      type: object
      additionalProperties: false
      properties: {}
    AssistantProposalFields:
      type: object
      additionalProperties: false
      required: [title, description, starts_at, ends_at, schedule_kind, timezone]
      properties:
        title:
          type: string
        description:
          type:
            - string
            - "null"
        starts_at:
          type:
            - string
            - "null"
          format: date-time
        ends_at:
          type:
            - string
            - "null"
          format: date-time
        schedule_kind:
          type: string
          enum: [none, date, date_time]
        timezone:
          type: string
    AssistantProposal:
      type: object
      additionalProperties: false
      required: [id, action, after, notice, expires_at, status]
      properties:
        id:
          type: string
          format: uuid
        action:
          type: string
          enum: [create_task, update_task, create_event, update_event, delete_task, complete_task, cancel_event, invite_task, invite_event, accept_task_invitation, decline_task_invitation, accept_event_invitation, decline_event_invitation]
        target_id:
          type: string
          format: uuid
        before:
          $ref: '#/components/schemas/AssistantProposalFields'
        after:
          $ref: '#/components/schemas/AssistantProposalFields'
        notice:
          type: string
        expires_at:
          type: string
          format: date-time
        status:
          type: string
          enum: [pending, applied, canceled]
    AssistantProposalResult:
      type: object
      additionalProperties: false
      required: [proposal]
      properties:
        proposal:
          $ref: '#/components/schemas/AssistantProposal'
        source:
          $ref: '#/components/schemas/AssistantSource'
    AssistantSource:
      type: object
      additionalProperties: false
      required:
        - id
        - kind
        - title
        - pending
        - completed
      properties:
        id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - event
            - task
            - manual
        title:
          type: string
        starts_at:
          type: string
          format: date-time
        ends_at:
          type: string
          format: date-time
        schedule_kind:
          type: string
          enum:
            - none
            - date
            - date_time
        pending:
          type: boolean
        completed:
          type: boolean
    HealthResponse:
      type: object
      additionalProperties: false
      required:
        - status
        - postgres
        - redis
      properties:
        status:
          type: string
          enum:
            - ok
            - error
        postgres:
          type: string
          enum:
            - ok
            - error
        redis:
          type: string
          enum:
            - ok
            - error
    StartDevAuthChallengeRequest:
      type: object
      additionalProperties: false
      required:
        - email
      properties:
        email:
          type: string
          format: email
    StartDevAuthChallengeResponse:
      type: object
      additionalProperties: false
      required:
        - challenge_id
        - email
        - dev_code
        - expires_at
      properties:
        challenge_id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        dev_code:
          type: string
          description: Dev-only verification code for local/staging.
          minLength: 6
          maxLength: 6
        expires_at:
          type: string
          format: date-time
    VerifyDevAuthChallengeRequest:
      type: object
      additionalProperties: false
      required:
        - email
        - code
      properties:
        email:
          type: string
          format: email
        code:
          type: string
          minLength: 6
          maxLength: 6
    StartPhoneCallAuthRequest:
      type: object
      additionalProperties: false
      required:
        - phone
      properties:
        phone:
          type: string
          example: "+79991234567"
    StartPhoneCallAuthResponse:
      type: object
      additionalProperties: false
      required:
        - challenge_id
        - phone
        - call_phone
        - call_phone_pretty
        - expires_at
      properties:
        challenge_id:
          type: string
          format: uuid
        phone:
          type: string
          example: "+79991234567"
        call_phone:
          type: string
          example: "78005008275"
        call_phone_pretty:
          type: string
          example: "+7 (800) 500-8275"
        expires_at:
          type: string
          format: date-time
    CompletePhoneCallAuthRequest:
      type: object
      additionalProperties: false
      required:
        - challenge_id
      properties:
        challenge_id:
          type: string
          format: uuid
    YandexMobileAuthRequest:
      type: object
      additionalProperties: false
      required:
        - oauth_token
      properties:
        oauth_token:
          type: string
          description: OAuth token returned by the official Yandex LoginSDK. Send only in this JSON body over HTTPS; Meets forwards it to Yandex only through the Authorization header and does not persist it.
    PhoneCallAuthStatusResponse:
      type: object
      additionalProperties: false
      required:
        - status
        - expires_at
      properties:
        status:
          type: string
          enum:
            - pending
            - verified
        expires_at:
          type: string
          format: date-time
        auth:
          $ref: "#/components/schemas/AuthResponse"
    AuthResponse:
      type: object
      additionalProperties: false
      required:
        - current_user
        - session_id
        - refresh_token
        - access_token
        - access_token_expires_at
        - token_type
        - expires_at
      properties:
        current_user:
          $ref: "#/components/schemas/CurrentUser"
        session_id:
          type: string
          format: uuid
        refresh_token:
          type: string
          description: Returned for dev and native mobile clients; also set as HttpOnly cookie for browser/WebView clients.
        access_token:
          type: string
          description: "Short-lived opaque token for `Authorization: Bearer <token>` on protected API requests."
        access_token_expires_at:
          type: string
          format: date-time
        token_type:
          type: string
          const: Bearer
        expires_at:
          type: string
          format: date-time
    RefreshSessionRequest:
      type: object
      additionalProperties: false
      properties:
        refresh_token:
          type: string
          description: Optional native mobile fallback when the refresh cookie is unavailable.
    RefreshSessionResponse:
      type: object
      additionalProperties: false
      required:
        - session_id
        - refresh_token
        - access_token
        - access_token_expires_at
        - token_type
        - expires_at
      properties:
        session_id:
          type: string
          format: uuid
        refresh_token:
          type: string
        access_token:
          type: string
          description: Rotated short-lived opaque access token.
        access_token_expires_at:
          type: string
          format: date-time
        token_type:
          type: string
          const: Bearer
        expires_at:
          type: string
          format: date-time
    AuthSessionsResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/AuthSession"
    AuthSession:
      type: object
      additionalProperties: false
      required:
        - id
        - current
        - device_label
        - expires_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        current:
          type: boolean
        device_label:
          type: string
          description: Coarse device/browser label derived from request headers without exposing raw user-agent or IP.
        last_seen_at:
          type:
            - string
            - "null"
          format: date-time
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CurrentUser:
      type: object
      additionalProperties: false
      required:
        - user
        - profile
      properties:
        user:
          $ref: "#/components/schemas/User"
        profile:
          $ref: "#/components/schemas/Profile"
    User:
      type: object
      additionalProperties: false
      required:
        - id
      properties:
        id:
          type: string
          format: uuid
        email:
          type:
            - string
            - "null"
          format: email
        phone:
          type:
            - string
            - "null"
    Profile:
      type: object
      additionalProperties: false
      required:
        - user_id
        - username
        - display_name
        - avatar_url
        - city
        - bio
        - interests
      properties:
        user_id:
          type: string
          format: uuid
        username:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 30
          pattern: "^[a-z0-9_]+$"
        display_name:
          type:
            - string
            - "null"
          maxLength: 80
        avatar_url:
          type:
            - string
            - "null"
          format: uri
        city:
          type:
            - string
            - "null"
          maxLength: 120
        bio:
          type:
            - string
            - "null"
          maxLength: 160
        interests:
          type: array
          maxItems: 20
          items:
            type: string
            maxLength: 80
    ContactMatchesRequest:
      type: object
      additionalProperties: false
      required:
        - phones
      properties:
        phones:
          type: array
          maxItems: 200
          items:
            type: string
            pattern: "^\\+7[0-9]{10}$"
    ContactMatch:
      type: object
      additionalProperties: false
      required:
        - phone
        - user_id
        - username
        - display_name
        - avatar_url
      properties:
        phone:
          type: string
          pattern: "^\\+7[0-9]{10}$"
        user_id:
          type: string
          format: uuid
        username:
          type:
            - string
            - "null"
        display_name:
          type:
            - string
            - "null"
        avatar_url:
          type:
            - string
            - "null"
          format: uri
    ContactMatchesResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ContactMatch"
    EventCollectionNameRequest:
      type: object
      additionalProperties: false
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 60
    SaveEventRequest:
      type: object
      additionalProperties: false
      required:
        - collection_ids
      properties:
        collection_ids:
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            format: uuid
    EventCollection:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          maxLength: 60
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    SavedEvent:
      type: object
      additionalProperties: false
      required:
        - event_id
        - saved_at
        - collection_ids
      properties:
        event_id:
          type: string
          format: uuid
        saved_at:
          type: string
          format: date-time
        collection_ids:
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            format: uuid
    SavedEventLibrary:
      type: object
      additionalProperties: false
      required:
        - collections
        - saved_events
      properties:
        collections:
          type: array
          maxItems: 20
          items:
            $ref: "#/components/schemas/EventCollection"
        saved_events:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/SavedEvent"
    PublicProfile:
      type: object
      additionalProperties: false
      required:
        - user_id
        - username
        - display_name
        - avatar_url
        - city
        - bio
        - interests
      properties:
        user_id:
          type: string
          format: uuid
        username:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 30
          pattern: "^[a-z0-9_]+$"
        display_name:
          type:
            - string
            - "null"
          maxLength: 80
        avatar_url:
          type:
            - string
            - "null"
          format: uri
        city:
          type:
            - string
            - "null"
          maxLength: 120
        bio:
          type:
            - string
            - "null"
          maxLength: 160
        interests:
          type: array
          maxItems: 20
          items:
            type: string
            maxLength: 80
    PublicProfilesResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/PublicProfile"
        next_cursor:
          type:
            - string
            - "null"
    FriendProfile:
      type: object
      additionalProperties: false
      required:
        - user_id
        - username
        - display_name
        - avatar_url
      properties:
        user_id:
          type: string
          format: uuid
        username:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 30
          pattern: "^[a-z0-9_]+$"
        display_name:
          type:
            - string
            - "null"
          maxLength: 80
        avatar_url:
          type:
            - string
            - "null"
          format: uri
    FriendsResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/FriendProfile"
        next_cursor:
          type:
            - string
            - "null"
    ProfileSocialCounts:
      type: object
      additionalProperties: false
      required:
        - friends
        - followers
        - following
      properties:
        friends:
          type: integer
          minimum: 0
        followers:
          type: integer
          minimum: 0
        following:
          type: integer
          minimum: 0
    PublicRelationshipPage:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/FriendProfile"
        next_cursor:
          type:
            - string
            - "null"
    FollowStatus:
      type: object
      additionalProperties: false
      required:
        - viewer_user_id
        - target_user_id
        - following
        - followed_by
        - friends
      properties:
        viewer_user_id:
          type: string
          format: uuid
        target_user_id:
          type: string
          format: uuid
        following:
          type: boolean
        followed_by:
          type: boolean
        friends:
          type: boolean
    UpdateProfileRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        username:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 30
          pattern: "^[a-z0-9_]+$"
        display_name:
          type:
            - string
            - "null"
          maxLength: 80
        avatar_url:
          type:
            - string
            - "null"
          format: uri
          maxLength: 500
        city:
          type:
            - string
            - "null"
          maxLength: 120
        bio:
          type:
            - string
            - "null"
          maxLength: 160
        interests:
          type: array
          maxItems: 20
          items:
            type: string
            maxLength: 80
    OrganizerPaymentProfile:
      type: object
      additionalProperties: false
      required:
        - user_id
        - status
        - provider_account_id
        - created_at
        - updated_at
      properties:
        user_id:
          type: string
          format: uuid
        status:
          $ref: "#/components/schemas/OrganizerPaymentProfileStatus"
        provider_account_id:
          type:
            - string
            - "null"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    UpdateOrganizerPaymentProfileRequest:
      type: object
      additionalProperties: false
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - unverified
            - verified
    OrganizerPaymentProfileStatus:
      type: string
      enum:
        - unverified
        - verified
        - blocked
    PaymentIntent:
      type: object
      additionalProperties: false
      required:
        - id
        - event_id
        - user_id
        - amount_minor
        - currency
        - platform_fee_amount_minor
        - status
        - provider
        - provider_payment_id
        - checkout_url
        - fiscal_receipt_status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        amount_minor:
          type: integer
          minimum: 1
        currency:
          type: string
          minLength: 3
          maxLength: 3
        platform_fee_amount_minor:
          type: integer
          minimum: 0
          description: Platform fee snapshot in minor currency units.
        status:
          $ref: "#/components/schemas/PaymentIntentStatus"
        provider:
          type: string
        provider_payment_id:
          type: string
        checkout_url:
          type: string
        fiscal_receipt_status:
          $ref: "#/components/schemas/FiscalReceiptStatus"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    PaymentIntentStatus:
      type: string
      enum:
        - pending
        - requires_action
        - succeeded
        - failed
        - canceled
    FiscalReceiptStatus:
      type: string
      enum:
        - not_required
        - pending
        - succeeded
        - failed
    PaymentWebhookResponse:
      type: object
      additionalProperties: false
      required:
        - event
        - intent
        - duplicate
      properties:
        event:
          $ref: "#/components/schemas/PaymentWebhookEvent"
        intent:
          anyOf:
            - $ref: "#/components/schemas/PaymentIntent"
            - type: "null"
        duplicate:
          type: boolean
    PaymentWebhookEvent:
      type: object
      additionalProperties: false
      required:
        - id
        - provider
        - provider_event_id
        - event_type
        - payload_hash
        - processed_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
        provider:
          type: string
        provider_event_id:
          type: string
        event_type:
          type: string
        payload_hash:
          type: string
        processed_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    UploadedImageResponse:
      type: object
      additionalProperties: false
      required:
        - id
        - path
        - content_type
        - byte_size
      properties:
        id:
          type: string
          format: uuid
        path:
          type: string
          pattern: ^/images/uploads/[0-9a-fA-F-]{36}$
        content_type:
          type: string
          enum:
            - image/jpeg
            - image/png
            - image/webp
        byte_size:
          type: integer
          minimum: 1
          maximum: 1000000
    ImageSearchResult:
      type: object
      additionalProperties: false
      required:
        - id
        - preview_url
        - image_url
        - source_url
        - photographer
        - photographer_url
        - alt
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 64
        preview_url:
          type: string
          format: uri
        image_url:
          type: string
          format: uri
        source_url:
          type: string
          format: uri
        photographer:
          type: string
          maxLength: 200
        photographer_url:
          type: string
          format: uri
        alt:
          type: string
          maxLength: 500
    ImageSearchResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - page
        - has_more
      properties:
        items:
          type: array
          maxItems: 30
          items:
            $ref: "#/components/schemas/ImageSearchResult"
        page:
          type: integer
          minimum: 1
          maximum: 100
        has_more:
          type: boolean
    CreateEventRequest:
      type: object
      additionalProperties: false
      required:
        - title
        - visibility
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 140
        description:
          type:
            - string
            - "null"
          maxLength: 4000
        description_rich:
          anyOf:
            - $ref: "#/components/schemas/RichTextDocument"
            - type: "null"
        category:
          type:
            - string
            - "null"
          maxLength: 80
        child_age_min_months:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 216
        child_age_max_months:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 216
        image_url:
          description: Deprecated cover URL. Equals the first image_urls item when present.
          deprecated: true
          type:
            - string
            - "null"
          format: uri
          maxLength: 1000
        image_urls:
          description: Ordered event images. The first item is the cover.
          type:
            - array
            - "null"
          maxItems: 3
          items:
            type: string
            format: uri
            maxLength: 1000
            pattern: ^https://
        starts_at:
          type:
            - string
            - "null"
          format: date-time
        ends_at:
          type:
            - string
            - "null"
          format: date-time
        visibility:
          $ref: "#/components/schemas/EventVisibility"
        pricing_mode:
          $ref: "#/components/schemas/EventPricingMode"
        price_amount_minor:
          type:
            - integer
            - "null"
          minimum: 1
        price_currency:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 3
        location:
          anyOf:
            - $ref: "#/components/schemas/EventLocationInput"
            - type: "null"
    UpdateEventRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 140
        description:
          type:
            - string
            - "null"
          maxLength: 4000
        description_rich:
          anyOf:
            - $ref: "#/components/schemas/RichTextDocument"
            - type: "null"
        category:
          type:
            - string
            - "null"
          maxLength: 80
        child_age_min_months:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 216
        child_age_max_months:
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 216
        image_url:
          description: Deprecated cover URL. Equals the first image_urls item when present.
          deprecated: true
          type:
            - string
            - "null"
          format: uri
          maxLength: 1000
        image_urls:
          description: Ordered event images. An empty array removes every image.
          type:
            - array
            - "null"
          maxItems: 3
          items:
            type: string
            format: uri
            maxLength: 1000
            pattern: ^https://
        starts_at:
          type:
            - string
            - "null"
          format: date-time
        ends_at:
          type:
            - string
            - "null"
          format: date-time
        visibility:
          $ref: "#/components/schemas/EventVisibility"
        pricing_mode:
          $ref: "#/components/schemas/EventPricingMode"
        price_amount_minor:
          type:
            - integer
            - "null"
          minimum: 1
        price_currency:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 3
        location:
          anyOf:
            - $ref: "#/components/schemas/EventLocationInput"
            - type: "null"
    EventDetails:
      type: object
      additionalProperties: false
      required:
        - event
        - location
        - current_participant
        - current_invitation
        - participant_count
        - feed_context
      properties:
        event:
          $ref: "#/components/schemas/Event"
        location:
          anyOf:
            - $ref: "#/components/schemas/EventLocation"
            - type: "null"
        current_participant:
          anyOf:
            - $ref: "#/components/schemas/EventParticipant"
            - type: "null"
        current_invitation:
          anyOf:
            - $ref: "#/components/schemas/CurrentEventInvitation"
            - type: "null"
        participant_count:
          type: integer
          minimum: 0
          description: Count of going participant-role attendees, including incognito participation without exposing identities.
        feed_context:
          $ref: "#/components/schemas/FeedContext"
    FeedEventDetails:
      type: object
      additionalProperties: false
      required:
        - event
        - location
        - participant_count
        - feed_context
      properties:
        event:
          $ref: "#/components/schemas/Event"
        location:
          anyOf:
            - $ref: "#/components/schemas/EventLocation"
            - type: "null"
        participant_count:
          type: integer
          minimum: 0
          description: Count of going participant-role attendees, including incognito participation without exposing identities.
        feed_context:
          $ref: "#/components/schemas/FeedContext"
    FeedContext:
      type: object
      additionalProperties: false
      required:
        - friend_attending
        - friend_attendee_count
        - following_organizer
        - viewer_going
      properties:
        friend_attending:
          type: boolean
        friend_attendee_count:
          type: integer
          minimum: 0
          description: Number of mutual friends with public going participation for this event.
        friend_attendees:
          type: array
          items:
            $ref: "#/components/schemas/FeedFriendAttendee"
          description: Up to 24 mutual friends with public going participation, ordered by join time.
        following_organizer:
          type: boolean
        viewer_going:
          type: boolean
        reasons:
          type: array
          items:
            $ref: "#/components/schemas/FeedReason"
          description: Optional explanation chips for why this event appears or ranks in the feed.
    FeedFriendAttendee:
      type: object
      additionalProperties: false
      required:
        - user_id
        - username
        - display_name
        - avatar_url
      properties:
        user_id:
          type: string
        username:
          type:
            - string
            - "null"
        display_name:
          type:
            - string
            - "null"
        avatar_url:
          type:
            - string
            - "null"
    FeedReason:
      type: object
      additionalProperties: false
      required:
        - label
        - tone
      properties:
        label:
          type: string
          minLength: 1
          maxLength: 80
        tone:
          type: string
          enum:
            - accent
            - warm
            - muted
    EventListResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventDetails"
        next_cursor:
          type:
            - string
            - "null"
          format: date-time
    EventMapResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventDetails"
    FeedResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - scope
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/FeedEventDetails"
        next_cursor:
          type:
            - string
            - "null"
          description: Opaque continuation token (timestamp for invited). Pass unchanged; null means end of feed.
        scope:
          type: string
          enum:
            - all
            - friends
            - groups
            - following
            - going
            - invited
    FeedPreferencesResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/FeedPreference"
    FeedPreference:
      type: object
      additionalProperties: false
      required:
        - item_key
        - item_kind
        - target_id
        - saved
        - hidden
        - not_interesting
        - updated_at
      properties:
        item_key:
          type: string
          example: announcement:00000000-0000-0000-0000-000000000000
        item_kind:
          type: string
          enum:
            - announcement
            - post
        target_id:
          type: string
          format: uuid
        saved:
          type: boolean
        hidden:
          type: boolean
        not_interesting:
          type: boolean
        updated_at:
          type: string
          format: date-time
    UpdateFeedPreferenceRequest:
      type: object
      additionalProperties: false
      required:
        - item_key
      properties:
        item_key:
          type: string
          example: post:00000000-0000-0000-0000-000000000000
        saved:
          type: boolean
        hidden:
          type: boolean
        not_interesting:
          type: boolean
    Event:
      type: object
      additionalProperties: false
      required:
        - id
        - organizer_id
        - title
        - description
        - description_rich
        - category
        - child_age_min_months
        - child_age_max_months
        - image_url
        - image_urls
        - starts_at
        - ends_at
        - visibility
        - status
        - date_mode
        - selected_date_option_id
        - pricing_mode
        - price_amount_minor
        - price_currency
        - published_at
        - canceled_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        organizer_id:
          type: string
          format: uuid
        title:
          type: string
          maxLength: 140
        description:
          type:
            - string
            - "null"
          maxLength: 4000
        description_rich:
          anyOf:
            - $ref: "#/components/schemas/RichTextDocument"
            - type: "null"
        category:
          type:
            - string
            - "null"
          maxLength: 80
        child_age_min_months:
          description: Inclusive minimum suitable child age in months; null when the organizer did not declare a child age range.
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 216
        child_age_max_months:
          description: Inclusive maximum suitable child age in months; null when the organizer did not declare a child age range.
          type:
            - integer
            - "null"
          minimum: 0
          maximum: 216
        image_url:
          description: Deprecated cover URL. Equals the first image_urls item when present.
          deprecated: true
          type:
            - string
            - "null"
          format: uri
          maxLength: 1000
        image_urls:
          description: Ordered event images. The first item is the cover.
          type: array
          maxItems: 3
          items:
            type: string
            format: uri
            maxLength: 1000
            pattern: ^https://
        starts_at:
          type:
            - string
            - "null"
          format: date-time
        ends_at:
          type:
            - string
            - "null"
          format: date-time
        visibility:
          $ref: "#/components/schemas/EventVisibility"
        status:
          $ref: "#/components/schemas/EventStatus"
        date_mode:
          $ref: "#/components/schemas/EventDateMode"
        selected_date_option_id:
          type:
            - string
            - "null"
          format: uuid
        pricing_mode:
          $ref: "#/components/schemas/EventPricingMode"
        price_amount_minor:
          type:
            - integer
            - "null"
          minimum: 1
        price_currency:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 3
        published_at:
          type:
            - string
            - "null"
          format: date-time
        canceled_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    RichTextDocument:
      type: object
      additionalProperties: false
      required:
        - version
        - blocks
      properties:
        version:
          type: integer
          const: 1
        blocks:
          type: array
          maxItems: 200
          items:
            $ref: "#/components/schemas/RichTextBlock"
    RichTextBlock:
      type: object
      additionalProperties: false
      required:
        - style
        - spans
      properties:
        style:
          type: string
          enum: [paragraph, heading1, heading2, bullet, numbered, quote]
        spans:
          type: array
          items:
            $ref: "#/components/schemas/RichTextSpan"
    RichTextSpan:
      type: object
      additionalProperties: false
      required:
        - text
      properties:
        text:
          type: string
        bold:
          type: boolean
        italic:
          type: boolean
        link:
          type: string
          format: uri
    EventLocationInput:
      type: object
      additionalProperties: false
      properties:
        title:
          type:
            - string
            - "null"
          maxLength: 200
        address:
          type:
            - string
            - "null"
          maxLength: 200
        city:
          type:
            - string
            - "null"
          maxLength: 200
        instructions:
          type:
            - string
            - "null"
          maxLength: 1000
        latitude:
          type:
            - number
            - "null"
          minimum: -90
          maximum: 90
        longitude:
          type:
            - number
            - "null"
          minimum: -180
          maximum: 180
    EventLocation:
      type: object
      additionalProperties: false
      required:
        - title
        - address
        - city
        - instructions
        - latitude
        - longitude
      properties:
        title:
          type:
            - string
            - "null"
          maxLength: 200
        address:
          type:
            - string
            - "null"
          maxLength: 200
        city:
          type:
            - string
            - "null"
          maxLength: 200
        instructions:
          type:
            - string
            - "null"
          maxLength: 1000
        latitude:
          type:
            - number
            - "null"
        longitude:
          type:
            - number
            - "null"
    EventVisibility:
      type: string
      enum:
        - public
        - private
    EventStatus:
      type: string
      enum:
        - draft
        - published
        - canceled
    EventDateMode:
      type: string
      enum:
        - fixed
        - vote
    EventPricingMode:
      type: string
      enum:
        - free
        - paid
    EventParticipant:
      type: object
      additionalProperties: false
      required:
        - event_id
        - user_id
        - role
        - status
        - participation_visibility
        - created_at
        - updated_at
      properties:
        event_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        role:
          type: string
          enum:
            - organizer
            - participant
        status:
          type: string
          enum:
            - going
            - left
            - waitlisted
        participation_visibility:
          type: string
          enum:
            - public
            - incognito
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    EventParticipantSummary:
      type: object
      additionalProperties: false
      required:
        - user_id
        - role
        - display_name
        - username
        - avatar_url
        - joined_at
      properties:
        user_id:
          type: string
          format: uuid
        role:
          type: string
          enum:
            - organizer
            - participant
        display_name:
          type:
            - string
            - "null"
          maxLength: 80
        username:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 30
          pattern: "^[a-z0-9_]+$"
        avatar_url:
          type:
            - string
            - "null"
          format: uri
        joined_at:
          type: string
          format: date-time
    EventParticipantsResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - participant_count
        - visible_participant_count
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventParticipantSummary"
        participant_count:
          type: integer
          minimum: 1
          description: Includes the organizer plus public and incognito going participants.
        visible_participant_count:
          type: integer
          minimum: 1
          description: Counts identities visible to the current viewer; the organizer also sees incognito participants.
        next_cursor:
          type:
            - string
            - "null"
          description: Opaque cursor for the next page, or null when exhausted.
    JoinEventRequest:
      type: object
      additionalProperties: false
      properties:
        participation_visibility:
          type: string
          default: public
          enum:
            - public
            - incognito
    EventPostThreadsResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventPostThread"
    EventPostRepostHistoryResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventPostRepostHistoryItem"
        next_cursor:
          type:
            - string
            - "null"
          description: Opaque cursor for the next page, or null when the history is exhausted.
    EventPostRepostHistoryItem:
      type: object
      additionalProperties: false
      required:
        - post
        - reposted_at
      properties:
        post:
          $ref: "#/components/schemas/EventPost"
        reposted_at:
          type: string
          format: date-time
    EventPostThreadsBatchResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventPostThreadsBatchItem"
    EventPostThreadsBatchItem:
      type: object
      additionalProperties: false
      required:
        - event_id
        - threads
      properties:
        event_id:
          type: string
          format: uuid
        threads:
          type: array
          items:
            $ref: "#/components/schemas/EventPostThread"
    EventPostThread:
      type: object
      additionalProperties: false
      required:
        - post
        - comments
      properties:
        post:
          $ref: "#/components/schemas/EventPost"
        comments:
          type: array
          items:
            $ref: "#/components/schemas/EventPostComment"
        reasons:
          type: array
          items:
            $ref: "#/components/schemas/FeedReason"
          description: Optional explanation chips for why this thread appears or ranks in the unified feed.
    EventPost:
      type: object
      additionalProperties: false
      required:
        - id
        - event_id
        - author_user_id
        - author
        - kind
        - title
        - body
        - image_url
        - like_count
        - viewer_liked
        - repost_count
        - viewer_reposted
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: string
          format: uuid
        author_user_id:
          type: string
          format: uuid
        author:
          $ref: "#/components/schemas/EventPostAuthor"
        kind:
          type: string
          enum:
            - announcement
            - post
            - important
            - discussion
            - comments
        title:
          type: string
          maxLength: 140
        body:
          type: string
          maxLength: 4000
        image_url:
          type:
            - string
            - "null"
          format: uri
          maxLength: 2048
        like_count:
          type: integer
          minimum: 0
        viewer_liked:
          type: boolean
        repost_count:
          type: integer
          minimum: 0
        viewer_reposted:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    EventPostComment:
      type: object
      additionalProperties: false
      required:
        - id
        - event_id
        - post_id
        - author_user_id
        - author
        - parent_comment_id
        - body
        - created_at
        - updated_at
        - deleted_at
      properties:
        client_message_id:
          type: string
          format: uuid
        voice:
          $ref: "#/components/schemas/VoiceMessage"
        id:
          type: string
          format: uuid
        event_id:
          type: string
          format: uuid
        post_id:
          type: string
          format: uuid
        author_user_id:
          type: string
          format: uuid
        author:
          $ref: "#/components/schemas/EventPostAuthor"
        parent_comment_id:
          type:
            - string
            - "null"
          format: uuid
        body:
          type: string
          maxLength: 2000
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type:
            - string
            - "null"
          format: date-time
    EventPostAuthor:
      type: object
      additionalProperties: false
      required:
        - user_id
        - display_name
      properties:
        user_id:
          type: string
          format: uuid
        display_name:
          type:
            - string
            - "null"
          maxLength: 80
    EventPostLikeState:
      type: object
      additionalProperties: false
      required:
        - post_id
        - like_count
        - viewer_liked
      properties:
        post_id:
          type: string
          format: uuid
        like_count:
          type: integer
          minimum: 0
        viewer_liked:
          type: boolean
    EventPostRepostState:
      type: object
      additionalProperties: false
      required:
        - post_id
        - repost_count
        - viewer_reposted
      properties:
        post_id:
          type: string
          format: uuid
        repost_count:
          type: integer
          minimum: 0
        viewer_reposted:
          type: boolean
    CreateEventPostRequest:
      type: object
      additionalProperties: false
      required:
        - title
        - body
      properties:
        kind:
          type: string
          default: post
          enum:
            - announcement
            - post
            - important
        title:
          type: string
          maxLength: 140
        body:
          type: string
          maxLength: 4000
        image_url:
          type:
            - string
            - "null"
          format: uri
          maxLength: 2048
    CreateEventPostCommentRequest:
      type: object
      additionalProperties: false
      required:
        - body
      properties:
        parent_comment_id:
          type:
            - string
            - "null"
          format: uuid
        body:
          type: string
          maxLength: 2000
    DiscussionInboxResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/DiscussionInboxItem"
        next_cursor:
          type: string
    SetDiscussionMutedRequest:
      type: object
      additionalProperties: false
      required:
        - muted
      properties:
        muted:
          type: boolean
    DiscussionInboxItem:
      type: object
      additionalProperties: false
      required: [event_id, title, preview, activity_at, notifications_muted]
      properties:
        pinned_at:
          type: [string, "null"]
          format: date-time
          description: Per-viewer pin timestamp; null when unpinned. Pins sort first, newest pin first.
        marked_unread:
          type: boolean
          description: Personal reminder only; independent from message read receipts.
        notifications_muted:
          type: boolean
        event_id:
          type: string
          format: uuid
        title:
          type: string
        image_url:
          type: string
        preview:
          type: string
        activity_at:
          type: string
          format: date-time
    DiscussionUnreadCountsResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/DiscussionUnreadCount"
    DiscussionUnreadCount:
      type: object
      additionalProperties: false
      required:
        - event_id
        - unread_count
      properties:
        event_id:
          type: string
          format: uuid
        unread_count:
          type: integer
          minimum: 1
    MarkEventDiscussionReadRequest:
      type: object
      additionalProperties: false
      required:
        - through_comment_id
      properties:
        through_comment_id:
          type: string
          format: uuid
    CreateEventPublicationCommentRequest:
      type: object
      additionalProperties: false
      required:
        - body
      properties:
        body:
          type: string
          maxLength: 2000
    CreateEventDiscussionCommentRequest:
      type: object
      additionalProperties: false
      description: >-
        Text requires a nonempty body. Voice requires voice_id and client_message_id and
        omits body. The upload must belong to this sender and this discussion. Optional
        parent_comment_id must reference a live comment in this same discussion.
        Stable client_message_id retries return the original comment; changed payload is 409.
      anyOf:
        - required:
            - body
        - required:
            - voice_id
            - client_message_id
      properties:
        body:
          type: string
          maxLength: 2000
        voice_id:
          type: [string, "null"]
          format: uuid
        client_message_id:
          type: [string, "null"]
          format: uuid
        parent_comment_id:
          type: [string, "null"]
          format: uuid
    UpdateEventPostCommentRequest:
      type: object
      additionalProperties: false
      required:
        - body
      properties:
        body:
          type: string
          maxLength: 2000
    UpsertEventReviewRequest:
      type: object
      additionalProperties: false
      required:
        - rating
      properties:
        rating:
          type: integer
          minimum: 1
          maximum: 5
        body:
          type:
            - string
            - "null"
          maxLength: 2000
    EventReviewsResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventReview"
    EventReview:
      type: object
      additionalProperties: false
      required:
        - id
        - event_id
        - reviewer_user_id
        - rating
        - body
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: string
          format: uuid
        reviewer_user_id:
          type: string
          format: uuid
        rating:
          type: integer
          minimum: 1
          maximum: 5
        body:
          type:
            - string
            - "null"
          maxLength: 2000
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateReportRequest:
      type: object
      additionalProperties: false
      required:
        - target_type
        - target_id
        - reason
      properties:
        target_type:
          $ref: "#/components/schemas/ReportTargetType"
        target_id:
          type: string
          format: uuid
        reason:
          $ref: "#/components/schemas/ReportReason"
        details:
          type:
            - string
            - "null"
          maxLength: 2000
    Report:
      type: object
      additionalProperties: false
      required:
        - id
        - reporter_user_id
        - target_type
        - target_id
        - reason
        - details
        - status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        reporter_user_id:
          type: string
          format: uuid
        target_type:
          $ref: "#/components/schemas/ReportTargetType"
        target_id:
          type: string
          format: uuid
        reason:
          $ref: "#/components/schemas/ReportReason"
        details:
          type:
            - string
            - "null"
          maxLength: 2000
        status:
          $ref: "#/components/schemas/ReportStatus"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ReportsResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Report"
    ReportTargetType:
      type: string
      enum:
        - event
        - user
        - review
        - event_post
        - event_post_comment
    ReportReason:
      type: string
      enum:
        - spam
        - harassment
        - inappropriate
        - safety
        - other
    ReportStatus:
      type: string
      enum:
        - open
        - reviewing
        - resolved
        - dismissed
    UpdateReportStatusRequest:
      type: object
      additionalProperties: false
      required:
        - status
        - action_type
      properties:
        status:
          $ref: "#/components/schemas/ReportStatus"
        action_type:
          $ref: "#/components/schemas/ModerationActionType"
        note:
          type:
            - string
            - "null"
          maxLength: 2000
    UpdateReportStatusResponse:
      type: object
      additionalProperties: false
      required:
        - report
        - action
      properties:
        report:
          $ref: "#/components/schemas/Report"
        action:
          $ref: "#/components/schemas/ModerationAction"
    ModerationAction:
      type: object
      additionalProperties: false
      required:
        - id
        - report_id
        - moderator_user_id
        - action_type
        - note
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        report_id:
          type: string
          format: uuid
        moderator_user_id:
          type: string
          format: uuid
        action_type:
          $ref: "#/components/schemas/ModerationActionType"
        note:
          type:
            - string
            - "null"
          maxLength: 2000
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ModerationActionType:
      type: string
      enum:
        - none
        - warning
        - content_hidden
        - user_restricted
    UserBlock:
      type: object
      additionalProperties: false
      required:
        - blocker_user_id
        - blocked_user_id
        - created_at
        - updated_at
      properties:
        blocker_user_id:
          type: string
          format: uuid
        blocked_user_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    UserBlockStatus:
      type: object
      additionalProperties: false
      required:
        - is_blocked
      properties:
        is_blocked:
          type: boolean
    CreateEventInvitationRequest:
      type: object
      additionalProperties: false
      oneOf:
        - required:
            - invited_user_id
        - anyOf:
            - required:
                - invited_phone
            - required:
                - email
      properties:
        invited_user_id:
          type: string
          format: uuid
          description: User ID of a mutual friend. Cannot be combined with invited_phone or email.
        invited_phone:
          type:
            - string
            - "null"
        email:
          type:
            - string
            - "null"
          format: email
    CreateEventInviteLinkRequest:
      type: object
      additionalProperties: false
      required:
        - invited_phone
      properties:
        invited_phone:
          type: string
          minLength: 12
          maxLength: 12
          pattern: "^\\+7[0-9]{10}$"
    RSVPEventInvitationRequest:
      type: object
      additionalProperties: false
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - accepted
            - declined
            - tentative
    SetEventDateModeRequest:
      type: object
      additionalProperties: false
      required:
        - mode
      properties:
        mode:
          type: string
          enum:
            - fixed
            - vote
    CreateEventDateOptionRequest:
      type: object
      additionalProperties: false
      required:
        - starts_at
      properties:
        starts_at:
          type: string
          format: date-time
        ends_at:
          type:
            - string
            - "null"
          format: date-time
        note:
          type:
            - string
            - "null"
          maxLength: 500
    EventDateOptionsResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventDateOption"
    EventDateOption:
      type: object
      additionalProperties: false
      required:
        - id
        - event_id
        - proposed_by_user_id
        - starts_at
        - ends_at
        - note
        - selected_at
        - vote_count
        - current_user_voted
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: string
          format: uuid
        proposed_by_user_id:
          type: string
          format: uuid
        starts_at:
          type: string
          format: date-time
        ends_at:
          type:
            - string
            - "null"
          format: date-time
        note:
          type:
            - string
            - "null"
          maxLength: 500
        selected_at:
          type:
            - string
            - "null"
          format: date-time
        vote_count:
          type: integer
          minimum: 0
        current_user_voted:
          type: boolean
          description: Whether the authenticated user has voted for this option.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    EventDateVote:
      type: object
      additionalProperties: false
      required:
        - option_id
        - event_id
        - user_id
        - created_at
        - updated_at
      properties:
        option_id:
          type: string
          format: uuid
        event_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateDirectChatRequest:
      type: object
      additionalProperties: false
      required:
        - user_id
      properties:
        user_id:
          type: string
          format: uuid
    SendDirectChatMessageRequest:
      type: object
      additionalProperties: false
      required:
        - kind
        - client_message_id
      properties:
        kind:
          $ref: "#/components/schemas/ChatMessageKind"
        text:
          type:
            - string
            - "null"
          minLength: 1
          maxLength: 4000
        event_id:
          type:
            - string
            - "null"
          format: uuid
        task_id:
          type:
            - string
            - "null"
          format: uuid
        voice_id:
          type: [string, "null"]
          format: uuid
        client_message_id:
          type: string
          format: uuid
        reply_to_message_id:
          type:
            - string
            - "null"
          format: uuid
      oneOf:
        - $ref: "#/components/schemas/SendDirectChatTextMessageRequest"
        - $ref: "#/components/schemas/SendDirectChatEventShareMessageRequest"
        - $ref: "#/components/schemas/SendDirectChatTaskShareMessageRequest"
        - $ref: "#/components/schemas/SendDirectChatVoiceMessageRequest"
      discriminator:
        propertyName: kind
        mapping:
          voice: "#/components/schemas/SendDirectChatVoiceMessageRequest"
          text: "#/components/schemas/SendDirectChatTextMessageRequest"
          event_share: "#/components/schemas/SendDirectChatEventShareMessageRequest"
          task_share: "#/components/schemas/SendDirectChatTaskShareMessageRequest"
    SendDirectChatTextMessageRequest:
      type: object
      additionalProperties: false
      required:
        - kind
        - text
        - client_message_id
      properties:
        kind:
          type: string
          const: text
        text:
          type: string
          minLength: 1
          maxLength: 4000
        client_message_id:
          type: string
          format: uuid
        reply_to_message_id:
          type:
            - string
            - "null"
          format: uuid
    SendDirectChatEventShareMessageRequest:
      type: object
      additionalProperties: false
      required:
        - kind
        - event_id
        - client_message_id
      properties:
        kind:
          type: string
          const: event_share
        event_id:
          type: string
          format: uuid
        client_message_id:
          type: string
          format: uuid
        reply_to_message_id:
          type:
            - string
            - "null"
          format: uuid
    SendDirectChatTaskShareMessageRequest:
      type: object
      additionalProperties: false
      required:
        - kind
        - task_id
        - client_message_id
      properties:
        kind:
          type: string
          const: task_share
        task_id:
          type: string
          format: uuid
        client_message_id:
          type: string
          format: uuid
        reply_to_message_id:
          type: [string, "null"]
          format: uuid
    MarkDirectChatReadRequest:
      type: object
      additionalProperties: false
      required:
        - through_message_id
      properties:
        through_message_id:
          type: string
          format: uuid
    SendDirectChatVoiceMessageRequest:
      type: object
      additionalProperties: false
      description: >-
        The upload must belong to this sender and direct thread. If an unsent upload expired,
        send returns 409 voice_upload_missing; reupload retained bytes under the original upload
        UUID and retry. Exact retries of an already-deleted message return its tombstone.
      required:
        - kind
        - voice_id
        - client_message_id
      properties:
        kind:
          type: string
          const: voice
        voice_id:
          type: string
          format: uuid
        client_message_id:
          type: string
          format: uuid
        reply_to_message_id:
          type: [string, "null"]
          format: uuid
    VoiceMessage:
      type: object
      additionalProperties: false
      required:
        - id
        - mime_type
        - duration_ms
        - byte_count
        - waveform
        - content_path
      properties:
        id:
          type: string
          format: uuid
        mime_type:
          type: string
          enum:
            - audio/x-caf
        duration_ms:
          type: integer
          minimum: 100
          maximum: 300000
        byte_count:
          type: integer
          minimum: 1
          maximum: 2097152
        waveform:
          type: array
          minItems: 1
          maxItems: 64
          items:
            type: integer
            minimum: 0
            maximum: 255
        content_path:
          type: string
          description: Relative authenticated GET path /voice/uploads/{id}; never a public media URL.
    ChatMessageKind:
      type: string
      description: >-
        Chat clients send X-Meets-Chat-Task-Shares: 1 to receive task_share cards.
        Without this capability header, all chat response surfaces (including inbox
        and reply previews) present task shares as metadata-free text asking the
        viewer to update the app. The header never grants access to a private task.
        Voice-capable clients send X-Meets-Chat-Voice: 1. Without it, voice messages
        and reply previews become metadata-free update prompts.
      enum:
        - voice
        - text
        - event_share
        - task_share
    ChatUserPreview:
      type: object
      additionalProperties: false
      required:
        - user_id
        - display_name
        - avatar_url
      properties:
        user_id:
          type: string
          format: uuid
        display_name:
          type:
            - string
            - "null"
        avatar_url:
          type:
            - string
            - "null"
          format: uri
    ChatEventPreview:
      type: object
      additionalProperties: false
      required:
        - id
        - title
        - image_url
        - starts_at
        - location
        - unavailable
      properties:
        id:
          type:
            - string
            - "null"
          format: uuid
        title:
          type:
            - string
            - "null"
        image_url:
          type:
            - string
            - "null"
          format: uri
        starts_at:
          type:
            - string
            - "null"
          format: date-time
        location:
          type:
            - string
            - "null"
        unavailable:
          type: boolean
          description: True when the referenced event was deleted or is no longer visible to the current user; all preview details are null.
    InvitePrivateTaskParticipantRequest:
      type: object
      additionalProperties: false
      required:
        - user_id
      properties:
        user_id:
          type: string
          format: uuid
    PrivateTaskParticipantPreview:
      type: object
      additionalProperties: false
      required:
        - user_id
        - display_name
        - avatar_url
        - status
      properties:
        user_id:
          type: string
          format: uuid
        display_name:
          type: string
        avatar_url:
          type: [string, "null"]
        status:
          type: string
          enum: [pending, accepted]
    PrivateTaskDetails:
      type: object
      additionalProperties: false
      required:
        - task
        - viewer_status
        - viewer_completed_at
        - viewer_checked_item_ids
        - participants
      properties:
        participants:
          type: array
          description: Visible only to authorized task viewers. Never contains other users' completion or checklist marks.
          items:
            $ref: "#/components/schemas/PrivateTaskParticipantPreview"
        task:
          $ref: "#/components/schemas/PrivateTask"
        viewer_status:
          type: string
          enum:
            - pending
            - accepted
        viewer_completed_at:
          type: [string, "null"]
          format: date-time
        viewer_checked_item_ids:
          type: array
          description: Only the authenticated viewer's marks; empty for pending invitees and when unchecked.
          items:
            type: string
            format: uuid
        completion_request:
          description: Latest completion-state request involving the accepted viewer, including its resolved outcome. Omitted for pending invitees or when no request exists.
          oneOf:
            - $ref: "#/components/schemas/PrivateTaskCompletionRequest"
            - type: "null"
    ChatTaskPreview:
      type: object
      additionalProperties: false
      required:
        - id
        - title
        - starts_at
        - unavailable
      properties:
        id:
          type: [string, "null"]
          format: uuid
        title:
          type: [string, "null"]
        starts_at:
          type: [string, "null"]
          format: date-time
        unavailable:
          type: boolean
          description: When true, all other fields are null. Sharing never grants access.
    ChatMessage:
      type: object
      additionalProperties: false
      required:
        - id
        - thread_id
        - sender_user_id
        - kind
        - text
        - event
        - task
        - client_message_id
        - created_at
        - reply_to_message_id
        - reply
        - is_deleted
        - reactions
      properties:
        voice:
          $ref: "#/components/schemas/VoiceMessage"
        id:
          type: string
          format: uuid
        thread_id:
          type: string
          format: uuid
        sender_user_id:
          type: string
          format: uuid
        kind:
          $ref: "#/components/schemas/ChatMessageKind"
        text:
          type:
            - string
            - "null"
          maxLength: 4000
        event:
          anyOf:
            - $ref: "#/components/schemas/ChatEventPreview"
            - type: "null"
        task:
          anyOf:
            - $ref: "#/components/schemas/ChatTaskPreview"
            - type: "null"
        reply_to_message_id:
          type:
            - string
            - "null"
          format: uuid
        reply:
          anyOf:
            - $ref: "#/components/schemas/ChatMessageReplyPreview"
            - type: "null"
        is_deleted:
          type: boolean
        reactions:
          type: array
          items:
            $ref: "#/components/schemas/ChatMessageReaction"
        client_message_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
    SetDirectChatMutedRequest:
      type: object
      additionalProperties: false
      required:
        - muted
      properties:
        muted:
          type: boolean
    ChatInboxPreferencesPatch:
      type: object
      additionalProperties: false
      anyOf:
        - required: [pinned]
        - required: [marked_unread]
      properties:
        pinned:
          type: boolean
        marked_unread:
          type: boolean
    ChatInboxPreferences:
      type: object
      additionalProperties: false
      required: [pinned_at, marked_unread]
      properties:
        pinned_at:
          type: [string, "null"]
          format: date-time
        marked_unread:
          type: boolean
    ChatThread:
      type: object
      additionalProperties: false
      required:
        - id
        - kind
        - other_user
        - latest_message
        - notifications_muted
        - unread_count
        - created_at
        - updated_at
      properties:
        pinned_at:
          type: [string, "null"]
          format: date-time
          description: Per-viewer pin timestamp; null when unpinned. Pins sort first, newest pin first.
        marked_unread:
          type: boolean
          description: Personal reminder only; independent from message read receipts.
        id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - direct
        other_user:
          $ref: "#/components/schemas/ChatUserPreview"
        latest_message:
          anyOf:
            - $ref: "#/components/schemas/ChatMessage"
            - type: "null"
        notifications_muted:
          type: boolean
          description: Whether the current member has disabled direct message push notifications for this chat.
        unread_count:
          type: integer
          minimum: 0
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ChatThreadPage:
      type: object
      additionalProperties: false
      required:
        - items
        - unread_count
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ChatThread"
        unread_count:
          type: integer
          minimum: 0
        next_cursor:
          type:
            - string
            - "null"
    ChatMessagePage:
      type: object
      additionalProperties: false
      required:
        - items
        - next_cursor
        - peer_read_through_message_id
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ChatMessage"
        next_cursor:
          type:
            - string
            - "null"
        peer_read_through_message_id:
          type:
            - string
            - "null"
          format: uuid
    ChatMessageReaction:
      type: object
      additionalProperties: false
      required:
        - emoji
        - count
        - reacted_by_viewer
      properties:
        emoji:
          type: string
          enum:
            - "❤️"
            - "👍"
            - "😂"
            - "🔥"
        count:
          type: integer
          minimum: 1
        reacted_by_viewer:
          type: boolean
    ChatMessageReplyPreview:
      type: object
      additionalProperties: false
      required:
        - message_id
        - sender_user_id
        - kind
        - text
        - is_deleted
      properties:
        message_id:
          type: string
          format: uuid
        sender_user_id:
          type: string
          format: uuid
        kind:
          $ref: "#/components/schemas/ChatMessageKind"
        text:
          type:
            - string
            - "null"
          maxLength: 4000
        is_deleted:
          type: boolean
    ChatUnreadCountResponse:
      type: object
      additionalProperties: false
      required:
        - unread_count
      properties:
        unread_count:
          type: integer
          minimum: 0
    SendEventChatMessageRequest:
      type: object
      additionalProperties: false
      required:
        - body
      properties:
        body:
          type: string
          minLength: 1
          maxLength: 2000
    EventChatMessagesResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventChatMessage"
    EventChatMessage:
      type: object
      additionalProperties: false
      required:
        - id
        - event_id
        - author_user_id
        - body
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: string
          format: uuid
        author_user_id:
          type: string
          format: uuid
        body:
          type: string
          maxLength: 2000
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    EventChatWebSocketAuthResponse:
      type: object
      additionalProperties: false
      required:
        - status
        - websocket
      properties:
        status:
          type: string
          enum:
            - authorized
        websocket:
          type: string
          enum:
            - not_implemented
    SetEventReminderSettingsRequest:
      type: object
      additionalProperties: false
      required:
        - enabled
        - remind_at
      properties:
        enabled:
          type: boolean
        remind_at:
          type:
            - string
            - "null"
          format: date-time
    EventReminderSettings:
      type: object
      additionalProperties: false
      required:
        - event_id
        - updated_by_user_id
        - enabled
        - remind_at
        - scheduled_at
        - created_at
        - updated_at
      properties:
        event_id:
          type: string
          format: uuid
        updated_by_user_id:
          type: string
          format: uuid
        enabled:
          type: boolean
        remind_at:
          type:
            - string
            - "null"
          format: date-time
        scheduled_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    EventInvitation:
      type: object
      additionalProperties: false
      required:
        - id
        - event_id
        - invited_user_id
        - invited_by_user_id
        - status
        - responded_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        event_id:
          type: string
          format: uuid
        invited_user_id:
          type: string
          format: uuid
        invited_by_user_id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - accepted
            - declined
            - tentative
        responded_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CurrentEventInvitation:
      type: object
      additionalProperties: false
      required:
        - id
        - status
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - accepted
            - declined
            - tentative
    EventInvitationInvitee:
      type: object
      additionalProperties: false
      required:
        - user_id
        - display_name
        - username
        - avatar_url
        - phone
      properties:
        user_id:
          type: string
          format: uuid
        display_name:
          type:
            - string
            - "null"
          maxLength: 80
        username:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 30
          pattern: "^[a-z0-9_]+$"
        avatar_url:
          type:
            - string
            - "null"
          format: uri
        phone:
          type: string
    EventInvitationListItem:
      type: object
      additionalProperties: false
      required:
        - invitation
        - invitee
      properties:
        invitation:
          $ref: "#/components/schemas/EventInvitation"
        invitee:
          $ref: "#/components/schemas/EventInvitationInvitee"
    EventInvitationsResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventInvitationListItem"
    EventInviteLinkStatus:
      type: string
      enum:
        - active
        - accepted
        - revoked
    EventInviteLinkAvailability:
      type: string
      enum:
        - available
        - expired
        - revoked
        - accepted
        - suspended_visibility
    EventInviteLinkMetadata:
      type: object
      additionalProperties: false
      required:
        - id
        - masked_phone
        - status
        - availability
        - expires_at
        - created_at
      properties:
        id:
          type: string
          minLength: 22
          maxLength: 22
          pattern: "^[A-Za-z0-9_-]{22}$"
        masked_phone:
          type: string
          maxLength: 24
          example: "+7 ••• •••-45-67"
        status:
          $ref: "#/components/schemas/EventInviteLinkStatus"
        availability:
          $ref: "#/components/schemas/EventInviteLinkAvailability"
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    EventInviteLinkCreation:
      type: object
      additionalProperties: false
      required:
        - id
        - masked_phone
        - status
        - availability
        - expires_at
        - created_at
        - invite_url
      properties:
        id:
          type: string
          minLength: 22
          maxLength: 22
          pattern: "^[A-Za-z0-9_-]{22}$"
        masked_phone:
          type: string
          maxLength: 24
          example: "+7 ••• •••-45-67"
        status:
          $ref: "#/components/schemas/EventInviteLinkStatus"
        availability:
          $ref: "#/components/schemas/EventInviteLinkAvailability"
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        invite_url:
          type: string
          format: uri
          maxLength: 256
          description: Sensitive no-store capability URL returned only by create.
    EventInviteLinksResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/EventInviteLinkMetadata"
    EventInviteLinkResolution:
      type: object
      additionalProperties: false
      required:
        - meeting_label
        - expires_at
        - availability
      properties:
        meeting_label:
          type: string
          const: Личная встреча
        expires_at:
          type: string
          format: date-time
        availability:
          $ref: "#/components/schemas/EventInviteLinkAvailability"
    EventInviteLinkAcceptance:
      type: object
      additionalProperties: false
      required:
        - event_id
        - invitation
        - participant
      properties:
        event_id:
          type: string
          format: uuid
        invitation:
          $ref: "#/components/schemas/EventInvitation"
        participant:
          $ref: "#/components/schemas/EventParticipant"
    CalendarEntrySource:
      type: string
      enum:
        - manual
        - event
        - task
    CalendarEntryStatus:
      type: string
      enum:
        - active
        - canceled
    CalendarInviteStatus:
      type: string
      enum:
        - pending
        - accepted
        - declined
        - tentative
    CreateCalendarEntryRequest:
      type: object
      additionalProperties: false
      required:
        - title
        - starts_at
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type:
            - string
            - "null"
          maxLength: 2000
        starts_at:
          type: string
          format: date-time
        ends_at:
          type:
            - string
            - "null"
          format: date-time
        timezone:
          type:
            - string
            - "null"
          maxLength: 100
        location_text:
          type:
            - string
            - "null"
          maxLength: 500
    UpdateCalendarEntryRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type:
            - string
            - "null"
          maxLength: 2000
        starts_at:
          type: string
          format: date-time
        ends_at:
          type:
            - string
            - "null"
          format: date-time
        timezone:
          type:
            - string
            - "null"
          maxLength: 100
        location_text:
          type:
            - string
            - "null"
          maxLength: 500
    CalendarEntriesResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        next_cursor:
          type: string
          description: Continue with the same filters until omitted. A full final page can lead to an empty terminal page.
        items:
          type: array
          items:
            $ref: "#/components/schemas/CalendarEntry"
    CreatePrivateTaskRequest:
      type: object
      additionalProperties: false
      required:
        - title
        - timezone
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type:
            - string
            - "null"
          maxLength: 2000
        description_rich:
          oneOf:
            - $ref: "#/components/schemas/PrivateTaskRichTextDocument"
            - type: "null"
          description: When supplied, the server derives description from block text joined by newlines.
        starts_at:
          type:
            - string
            - "null"
          format: date-time
        schedule_kind:
          type: string
          enum: [none, date, date_time]
          description: Defaults to date_time when an older client sends starts_at and omits this field.
        timezone:
          type: string
          minLength: 1
          maxLength: 100
        invited_user_ids:
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            format: uuid
    UpdatePrivateTaskRequest:
      type: object
      additionalProperties: false
      required:
        - title
        - description
        - description_rich
        - starts_at
        - schedule_kind
        - timezone
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: [string, "null"]
          maxLength: 2000
        description_rich:
          oneOf:
            - $ref: "#/components/schemas/PrivateTaskRichTextDocument"
            - type: "null"
        starts_at:
          type: [string, "null"]
          format: date-time
        schedule_kind:
          type: string
          enum: [none, date, date_time]
        timezone:
          type: string
          minLength: 1
          maxLength: 100
    RespondPrivateTaskInvitationRequest:
      type: object
      additionalProperties: false
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - accepted
            - declined
    SetPrivateTaskCompletionRequest:
      type: object
      additionalProperties: false
      required:
        - completed
      properties:
        completed:
          type: boolean
    RespondPrivateTaskCompletionRequest:
      type: object
      additionalProperties: false
      required:
        - accepted
      properties:
        accepted:
          type: boolean
    PrivateTaskCompletionResponse:
      type: object
      additionalProperties: false
      required:
        - user_id
        - status
      properties:
        user_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, accepted, declined]
        responded_at:
          type: [string, "null"]
          format: date-time
    PrivateTaskCompletionRequest:
      type: object
      additionalProperties: false
      required:
        - id
        - task_id
        - requester_user_id
        - desired_completed
        - status
        - created_at
        - responses
      properties:
        id:
          type: string
          format: uuid
        task_id:
          type: string
          format: uuid
        requester_user_id:
          type: string
          format: uuid
        desired_completed:
          type: boolean
        status:
          type: string
          enum: [pending, accepted, declined, canceled]
        created_at:
          type: string
          format: date-time
        resolved_at:
          type: [string, "null"]
          format: date-time
        responses:
          type: array
          items:
            $ref: "#/components/schemas/PrivateTaskCompletionResponse"
    PrivateTaskCompletionMutation:
      type: object
      additionalProperties: false
      required:
        - status
        - completed_at
      properties:
        status:
          type: string
          enum: [applied, pending, declined, canceled]
        completed_at:
          type: [string, "null"]
          format: date-time
        completion_request:
          oneOf:
            - $ref: "#/components/schemas/PrivateTaskCompletionRequest"
            - type: "null"
    SetPrivateTaskOrderRequest:
      type: object
      additionalProperties: false
      required:
        - task_ids
      properties:
        task_ids:
          type: array
          minItems: 1
          maxItems: 200
          uniqueItems: true
          items:
            type: string
            format: uuid
    SetPrivateTaskChecklistItemRequest:
      type: object
      additionalProperties: false
      required:
        - checked
      properties:
        checked:
          type: boolean
    UpdatePrivateTaskDescriptionRequest:
      type: object
      additionalProperties: false
      required:
        - description_rich
        - previous_description
        - previous_description_rich
      properties:
        description_rich:
          oneOf:
            - $ref: "#/components/schemas/PrivateTaskRichTextDocument"
            - type: "null"
        previous_description:
          type: [string, "null"]
          maxLength: 2000
        previous_description_rich:
          oneOf:
            - $ref: "#/components/schemas/PrivateTaskRichTextDocument"
            - type: "null"
    PrivateTaskRichTextDocument:
      type: object
      additionalProperties: false
      description: >-
        Version 1 supported formatting only. The total block text joined with newlines
        is limited to 2000 Unicode characters. IDs must be unique UUIDs, canonicalized
        to lowercase. An empty document normalizes to absent. Checklist completion is
        never stored in shared content.
      required:
        - version
        - blocks
      properties:
        version:
          type: integer
          const: 1
        blocks:
          type: array
          maxItems: 200
          items:
            $ref: "#/components/schemas/PrivateTaskRichTextBlock"
    PrivateTaskRichTextBlock:
      type: object
      additionalProperties: false
      required:
        - id
        - style
        - spans
      properties:
        id:
          type: string
          format: uuid
        style:
          type: string
          enum:
            - paragraph
            - heading1
            - heading2
            - bullet
            - numbered
            - checklist
        spans:
          type: array
          maxItems: 200
          items:
            $ref: "#/components/schemas/PrivateTaskRichTextSpan"
    PrivateTaskRichTextSpan:
      type: object
      additionalProperties: false
      required:
        - text
      properties:
        text:
          type: string
          description: Preserved verbatim; embedded newline, carriage return and Unicode paragraph/line separators are rejected.
        bold:
          type: boolean
        italic:
          type: boolean
        underline:
          type: boolean
        strikethrough:
          type: boolean
    PrivateTask:
      type: object
      additionalProperties: false
      required:
        - id
        - owner_user_id
        - title
        - description
        - starts_at
        - schedule_kind
        - timezone
        - status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        owner_user_id:
          type: string
          format: uuid
        title:
          type: string
          maxLength: 200
        description:
          type:
            - string
            - "null"
          maxLength: 2000
        description_rich:
          $ref: "#/components/schemas/PrivateTaskRichTextDocument"
        starts_at:
          type:
            - string
            - "null"
          format: date-time
        schedule_kind:
          type: string
          enum: [none, date, date_time]
        timezone:
          type: string
          maxLength: 100
        status:
          type: string
          enum:
            - active
            - canceled
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    PrivateTasksPage:
      type: object
      additionalProperties: false
      required: [items, next_cursor]
      properties:
        items:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/PrivateTaskListItem"
        next_cursor:
          type: [string, "null"]
          format: uuid
    PrivateTaskListItem:
      type: object
      additionalProperties: false
      required: [task, viewer_status, viewer_completed_at]
      properties:
        task:
          $ref: "#/components/schemas/PrivateTask"
        viewer_status:
          type: string
          enum: [pending, accepted]
        viewer_completed_at:
          type: [string, "null"]
          format: date-time
        pending_invitation_version:
          type: string
          description: Opaque invitation lineage present only while the viewer is pending.
        viewer_sort_position:
          type: integer
    UnscheduledPrivateTasksResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/UnscheduledPrivateTaskItem"
    UnscheduledPrivateTaskItem:
      type: object
      additionalProperties: false
      required: [task, viewer_status, viewer_completed_at]
      properties:
        task:
          $ref: "#/components/schemas/PrivateTask"
        viewer_status:
          type: string
          enum: [pending, accepted]
        viewer_completed_at:
          type: [string, "null"]
          format: date-time
        pending_invitation_version:
          type: string
          description: Opaque invitation lineage present only while the viewer is pending.
    CalendarEntry:
      type: object
      additionalProperties: false
      required:
        - id
        - owner_user_id
        - source
        - event_id
        - task_id
        - organizer_profile
        - title
        - description
        - starts_at
        - ends_at
        - timezone
        - location_text
        - image_url
        - viewer_completed_at
        - status
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        owner_user_id:
          type: string
          format: uuid
        source:
          $ref: "#/components/schemas/CalendarEntrySource"
        event_id:
          type:
            - string
            - "null"
          format: uuid
        task_id:
          type:
            - string
            - "null"
          format: uuid
        task_schedule_kind:
          type: string
          enum: [date, date_time]
          description: Present only for task projections; unscheduled tasks use the dedicated task list.
        task_sort_position:
          type: integer
          minimum: 0
          description: Current viewer's personal relative position for an accepted task, when explicitly ordered.
        organizer_profile:
          anyOf:
            - $ref: "#/components/schemas/CalendarInviteProfile"
            - type: "null"
        pending_invitation_id:
          type: string
          format: uuid
          description: Present only for a pending invitation projection. Equals event_id or task_id; use that source's existing invitation response endpoint. The row id is a projection identifier, not a mutable calendar entry id.
        pending_invitation_version:
          type: string
          description: Opaque invitation-lineage version, present on pending projections. Changes when the same entity is invited again, independently of event/task edits. Clients must compare the value as a string, not use the entity updated_at to identify an invitation.
        title:
          type: string
          maxLength: 200
        description:
          type:
            - string
            - "null"
          maxLength: 2000
        starts_at:
          type: string
          format: date-time
        ends_at:
          type:
            - string
            - "null"
          format: date-time
        timezone:
          type:
            - string
            - "null"
          maxLength: 100
        location_text:
          type:
            - string
            - "null"
          maxLength: 500
        image_url:
          type:
            - string
            - "null"
          format: uri
          maxLength: 1000
        viewer_completed_at:
          type:
            - string
            - "null"
          format: date-time
        status:
          $ref: "#/components/schemas/CalendarEntryStatus"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateCalendarInviteRequest:
      type: object
      additionalProperties: false
      properties:
        invited_user_id:
          type: string
          format: uuid
          description: Existing low-level invite target. Prefer invited_phone in the web app.
        invited_phone:
          type: string
          description: Phone number of an existing Vstrechi user.
    RespondCalendarInviteRequest:
      type: object
      additionalProperties: false
      required:
        - status
      properties:
        status:
          type: string
          enum:
            - accepted
            - declined
            - tentative
    CalendarInvite:
      type: object
      additionalProperties: false
      required:
        - id
        - entry_id
        - invited_user_id
        - invited_by_user_id
        - status
        - responded_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        entry_id:
          type: string
          format: uuid
        invited_user_id:
          type: string
          format: uuid
        invited_by_user_id:
          type: string
          format: uuid
        status:
          $ref: "#/components/schemas/CalendarInviteStatus"
        responded_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CalendarInviteProfile:
      type: object
      additionalProperties: false
      required:
        - user_id
        - username
        - display_name
        - avatar_url
      properties:
        user_id:
          type: string
          format: uuid
        username:
          type:
            - string
            - "null"
          minLength: 3
          maxLength: 30
          pattern: "^[a-z0-9_]+$"
        display_name:
          type:
            - string
            - "null"
          maxLength: 80
        avatar_url:
          type:
            - string
            - "null"
          format: uri
    CalendarInviteParticipant:
      type: object
      additionalProperties: false
      required:
        - invite
        - profile
      properties:
        invite:
          $ref: "#/components/schemas/CalendarInvite"
        profile:
          anyOf:
            - $ref: "#/components/schemas/CalendarInviteProfile"
            - type: "null"
    CalendarInviteWithEntry:
      type: object
      additionalProperties: false
      required:
        - invite
        - entry
        - inviter_profile
      properties:
        invite:
          $ref: "#/components/schemas/CalendarInvite"
        entry:
          $ref: "#/components/schemas/CalendarEntry"
        inviter_profile:
          anyOf:
            - $ref: "#/components/schemas/CalendarInviteProfile"
            - type: "null"
    CalendarInviteDetails:
      type: object
      additionalProperties: false
      required:
        - id
        - entry_id
        - invited_user_id
        - invited_by_user_id
        - status
        - responded_at
        - created_at
        - updated_at
        - entry
        - inviter_profile
        - participants
      properties:
        id:
          type: string
          format: uuid
        entry_id:
          type: string
          format: uuid
        invited_user_id:
          type: string
          format: uuid
        invited_by_user_id:
          type: string
          format: uuid
        status:
          $ref: "#/components/schemas/CalendarInviteStatus"
        responded_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        entry:
          $ref: "#/components/schemas/CalendarEntry"
        inviter_profile:
          anyOf:
            - $ref: "#/components/schemas/CalendarInviteProfile"
            - type: "null"
        participants:
          type: array
          items:
            $ref: "#/components/schemas/CalendarInviteParticipant"
    CalendarInvitesResponse:
      type: object
      additionalProperties: false
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/CalendarInviteWithEntry"
    SetCalendarReminderSettingsRequest:
      type: object
      additionalProperties: false
      required:
        - enabled
        - remind_at
      properties:
        enabled:
          type: boolean
        remind_at:
          type:
            - string
            - "null"
          format: date-time
        offset_minutes:
          type: array
          maxItems: 5
          description: Preset reminder offsets in minutes before entry start, e.g. 4320, 1440 and 120.
          items:
            type: integer
            minimum: 1
    CalendarReminderSettings:
      type: object
      additionalProperties: false
      required:
        - entry_id
        - updated_by_user_id
        - enabled
        - remind_at
        - offset_minutes
        - scheduled_at
        - created_at
        - updated_at
      properties:
        entry_id:
          type: string
          format: uuid
        updated_by_user_id:
          type: string
          format: uuid
        enabled:
          type: boolean
        remind_at:
          type:
            - string
            - "null"
          format: date-time
        offset_minutes:
          type: array
          items:
            type: integer
            minimum: 1
        scheduled_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    NotificationKind:
      type: string
      enum:
        - social_follow
        - calendar_invite
        - calendar_upcoming
        - event_upcoming
        - event_update
        - event_post_comment
        - event_post_like
        - event_post_mention
        - event_post_important
    RegisterNotificationDeviceTokenRequest:
      type: object
      additionalProperties: false
      required:
        - token
        - environment
      properties:
        local_event_reminders:
          type: boolean
          default: false
          description: Device owns local event reminder scheduling, including its opt-out. Suppresses duplicate event_upcoming APNs alerts on this device.
        token:
          type: string
          minLength: 64
          maxLength: 64
          pattern: "^[0-9A-Fa-f]{64}$"
        environment:
          type: string
          enum:
            - development
            - production
    Notification:
      type: object
      additionalProperties: false
      required:
        - id
        - recipient_user_id
        - actor_user_id
        - kind
        - title
        - body
        - href
        - meta
        - read_at
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        recipient_user_id:
          type: string
          format: uuid
        actor_user_id:
          type:
            - string
            - "null"
          format: uuid
        kind:
          $ref: "#/components/schemas/NotificationKind"
        title:
          type: string
          maxLength: 160
        body:
          type: string
          maxLength: 500
        href:
          type: string
          maxLength: 500
        meta:
          type:
            - string
            - "null"
          maxLength: 160
        read_at:
          type:
            - string
            - "null"
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    NotificationsResponse:
      type: object
      additionalProperties: false
      required:
        - items
        - unread_count
        - next_cursor
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Notification"
        unread_count:
          type: integer
          minimum: 0
        next_cursor:
          type:
            - string
            - "null"
    NotificationUnreadCountResponse:
      type: object
      additionalProperties: false
      required:
        - unread_count
      properties:
        unread_count:
          type: integer
          minimum: 0
    MarkAllNotificationsReadResponse:
      type: object
      additionalProperties: false
      required:
        - updated_count
      properties:
        updated_count:
          type: integer
          minimum: 0
    ErrorResponse:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: string
